Bağımlılık Enjeksiyonu Nedir?
Bu bölüm size herhangi bir uygulama yazarken izlemeniz gereken temel programlama uygulamalarını tanıtmaktadır. Bunlar temiz, anlaşılabilir ve bakımı yapılabilir kod yazmak için gereken temel bilgilerdir.
Bu kuralları öğrenir ve uygularsanız, Nette her adımda yanınızda olacaktır. Rutin görevleri sizin için halledecek ve sizi olabildiğince rahat ettirecek, böylece siz de mantığın kendisine odaklanabileceksiniz.
Burada göstereceğimiz ilkeler oldukça basittir. Endişelenmenizi gerektirecek bir şey yok.
İlk Programınızı Hatırlıyor musunuz?
Hangi dilde yazdığınızı bilmiyoruz, ancak PHP olsaydı muhtemelen şöyle bir şey olurdu:
function toplami(float $a, float $b): float
{
return $a + $b;
}
echo toplami(23, 1); // 24 yazdırır
Birkaç önemsiz kod satırı, ancak içlerinde çok fazla anahtar kavram gizli. Değişkenler var. Kodun daha küçük birimlere ayrıldığı, örneğin fonksiyonlar. Onlara girdi argümanları iletiyoruz ve onlar da sonuç döndürüyorlar. Eksik olan tek şey koşullar ve döngüler.
Bir fonksiyona girdi aktardığımız ve fonksiyonun bir sonuç döndürdüğü gerçeği, matematik gibi diğer alanlarda da kullanılan son derece anlaşılabilir bir kavramdır.
Bir fonksiyonun adı, parametrelerin listesi ve türleri ile son olarak geri dönüş değerinin türünden oluşan bir imzası vardır. Kullanıcılar olarak biz imzayla ilgileniriz; genellikle dahili uygulama hakkında bir şey bilmemiz gerekmez.
Şimdi bir fonksiyonun imzasının aşağıdaki gibi olduğunu düşünün:
function toplami(float $x): float
Tek parametreli bir ekleme mi? Bu çok garip. Buna ne dersin?
function toplami(): float
Bu gerçekten garip, değil mi? Bu fonksiyonun nasıl kullanıldığını düşünüyorsunuz?
echo toplami(); // ne yazdırıyor?
Böyle bir koda baktığımızda kafamız karışır. Sadece yeni başlayanlar değil, yetenekli bir programcı bile böyle bir kodu anlamayacaktır.
Böyle bir fonksiyonun içinde gerçekte neye benzeyeceğini merak ediyor musunuz? Toplayıcıları nereden bulacak? Muhtemelen onları bir şekilde kendi başına alırdı, bunun gibi:
function toplami(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
İşlevin gövdesinde diğer işlevlere (veya statik yöntemlere) gizli bağlar olduğu ortaya çıkıyor ve eklerin gerçekte nereden geldiğini bulmak için daha fazla araştırmamız gerekiyor.
Bu şekilde değil!
Az önce bize gösterilen tasarım, birçok olumsuz özelliğin özüdür:
- fonksiyon imzası eklentilere ihtiyaç duymuyormuş gibi davranıyordu, bu da kafamızı karıştırıyordu
- fonksiyonun diğer iki sayı ile nasıl hesaplanacağı hakkında hiçbir fikrimiz yok
- ekleri nereye götürdüğünü görmek için kodun içine bakmamız gerekti
- gizli bağları keşfettik
- tam olarak anlamak için bu bağları da keşfetmemiz gerekir
Peki girdileri tedarik etmek toplama işlevinin görevi midir? Elbette değildir. Onun sorumluluğu sadece eklemektir.
Böyle bir kodla karşılaşmak istemeyiz ve kesinlikle yazmak da istemeyiz. Çözüm basit: temellere geri dönün ve sadece parametreleri kullanın:
function toplami(float $a, float $b): float
{
return $a + $b;
}
Kural #1: Sana geçirilsin
En önemli kural şudur: Fonksiyonların veya sınıfların ihtiyaç duyduğu tüm veriler onlara aktarılmalıdır.
Bir şekilde kendi başlarına ulaşmalarına yardımcı olmak için gizli mekanizmalar icat etmek yerine, sadece parametreleri iletin. Kodunuzu kesinlikle iyileştirmeyecek gizli bir yol bulmak için harcadığınız zamandan tasarruf edeceksiniz.
Bu kurala her zaman ve her yerde uyarsanız, gizli bağları olmayan koda doğru yol alırsınız. Sadece yazan için değil, daha sonra okuyan herkes için de anlaşılabilir bir koda doğru. Fonksiyonların ve sınıfların imzalarından her şeyin anlaşılabilir olduğu ve uygulamada gizli sırlar aramaya gerek olmadığı bir kod.
Bu teknik ustalıkla bağımlılık enjeksiyonu olarak adlandırılır. Ve verilere bağımlılıklar denir. Ancak bu basit bir parametre aktarımıdır, başka bir şey değildir.
Lütfen bir tasarım kalıbı olan bağımlılık enjeksiyonunu, tamamen farklı bir şey olan bir araç olan “bağımlılık enjeksiyon konteyneri” ile karıştırmayın. Kapsayıcıları daha sonra tartışacağız.
Fonksiyonlardan Sınıflara
Peki sınıfların bununla ilişkisi nedir? Bir sınıf basit bir fonksiyondan daha karmaşık bir varlıktır, ancak 1. kural burada da geçerlidir. Sadece argümanları aktarmanın daha fazla yolu vardır. Örneğin, bir fonksiyonun durumuna oldukça benzer:
class Matematik
{
public function toplami(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Matematik;
echo $math->toplami(23, 1); // 24
Ya da diğer yöntemleri kullanarak veya doğrudan kurucu tarafından:
class Toplami
{
public function __construct(
private float $a,
private float $b,
) {
}
public function calculate(): float
{
return $this->a + $this->b;
}
}
$toplami = new Toplami(23, 1);
echo $toplami->calculate(); // 24
Her iki örnek de bağımlılık enjeksiyonu ile tamamen uyumludur.
Gerçek Hayattan Örnekler
Gerçek dünyada, sayıları toplamak için sınıflar yazmayacaksınız. Gerçek dünya örneklerine geçelim.
Bir blog makalesini temsil eden bir Article
sınıfımız olsun:
class Article
{
public int $id;
public string $title;
public string $content;
public function save(): void
{
// makaleyi veritabanına kaydedin
}
}
ve kullanım aşağıdaki gibi olacaktır:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
save()
yöntemi makaleyi bir veritabanı tablosuna kaydeder. Bir aksaklık olmasaydı, Nette Database kullanarak bunu uygulamak çocuk oyuncağı olurdu:
Article
veritabanı bağlantısını, yani
Nette\Database\Connection
sınıf nesnesini nereden almalıdır?
Çok fazla seçeneğimiz var gibi görünüyor. Statik bir değişkende bir yerden alabilir. Ya da veritabanı bağlantısını sağlayacak bir sınıftan miras alabilir. Ya da bir singleton'dan yararlanabilir. Ya da Laravel'de kullanılan sözde facades:
use Illuminate\Support\Facades\DB;
class Article
{
public int $id;
public string $title;
public string $content;
public function save(): void
{
DB::insert(
'INSERT INTO articles (title, content) VALUES (?, ?)',
[$this->title, $this->content],
);
}
}
Harika, sorunu çözdük.
Yoksa biz mi?
Kural 1: Sana geçirilsin sınıfın ihtiyaç duyduğu tüm bağımlılıklar ona geçmelidir. Çünkü bunu yapmazsak ve kuralı ihlal edersek, gizli bağlarla dolu kirli koda, anlaşılmazlığa giden yola girmiş oluruz ve sonuçta bakımı ve geliştirilmesi eziyet olan bir uygulama ortaya çıkar.
Article
sınıfının kullanıcısı, save()
yönteminin makaleyi nerede sakladığı konusunda
hiçbir fikre sahip değildir. Bir veritabanı tablosunda mı? Hangisinde, üretimde mi yoksa geliştirmede mi? Ve bu nasıl
değiştirilebilir?
Kullanıcı DB::insert()
yönteminin kullanımını bulmak için save()
yönteminin nasıl
uygulandığına bakmalıdır. Dolayısıyla, bu yöntemin bir veritabanı bağlantısını nasıl sağladığını bulmak için
daha fazla araştırma yapması gerekir. Ve gizli bağlar oldukça uzun bir zincir oluşturabilir.
Gizli bağlar, Laravel cepheleri veya statik değişkenler temiz, iyi tasarlanmış kodda asla bulunmaz. Temiz ve iyi tasarlanmış kodda, argümanlar geçirilir:
class Article
{
public function save(Nette\Database\Connection $db): void
{
$db->query('INSERT INTO articles', [
'title' => $this->title,
'content' => $this->content,
]);
}
}
Daha sonra göreceğimiz gibi, bir kurucu kullanmak daha da pratiktir:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function save(): void
{
$this->db->query('INSERT INTO articles', [
'title' => $this->title,
'content' => $this->content,
]);
}
}
Deneyimli bir programcıysanız, Article
'un save()
yöntemine hiç sahip olmaması
gerektiğini, saf bir veri bileşeni olması gerektiğini ve ayrı bir deponun depolamayla ilgilenmesi gerektiğini düşünüyor
olabilirsiniz. Bu mantıklıdır. Ancak bu bizi bağımlılık enjeksiyonu olan ve basit örnekler vermeye çalıştığımız
konunun çok ötesine götürecektir.
Örneğin, çalışması için bir veritabanına ihtiyaç duyan bir sınıf yazacaksanız, onu nereden alacağınızı bulmayın, ancak size aktarılmasını sağlayın. Belki bir yapıcıya veya başka bir yönteme parametre olarak. Bağımlılıkları beyan edin. Bunları sınıfınızın API'sinde gösterin. Anlaşılabilir ve öngörülebilir bir kod elde edeceksiniz.
Hata mesajlarını günlüğe kaydeden bu sınıfa ne dersiniz?
class Logger
{
public function log(string $message)
{
$file = LOG_DIR . '/log.txt';
file_put_contents($file, $message . "\n", FILE_APPEND);
}
}
Ne düşünüyorsunuz, kurala 1: Sana geçirilsin uyduk mu?
Biz yapmadık.
Anahtar bilgi olan günlük dosyası dizini, sınıf tarafından sabitten elde edilir.
Örnek kullanıma bakın:
$logger = new Logger;
$logger->log('The temperature is 23 °C');
$logger->log('The temperature is 10 °C');
Uygulamayı bilmeden, mesajların nereye yazıldığı sorusuna cevap verebilir misiniz? Bu size LOG_DIR sabitinin varlığının çalışması için gerekli olduğunu düşündürür mü? Ve farklı bir konuma yazan ikinci bir örnek oluşturabilir misiniz? Kesinlikle hayır.
Sınıfı düzeltelim:
class Logger
{
public function __construct(
private string $file,
) {
}
public function log(string $message): void
{
file_put_contents($this->file, $message . "\n", FILE_APPEND);
}
}
Sınıf artık çok daha net, daha yapılandırılabilir ve dolayısıyla daha kullanışlı.
$logger = new Logger('/path/to/log.txt');
$logger->log('The temperature is 15 °C');
Ama umurumda değil!
“Bir Article nesnesi oluşturduğumda ve save() işlevini çağırdığımda, veritabanıyla uğraşmak istemiyorum, sadece yapılandırmada ayarladığım veritabanına kaydedilmesini istiyorum. ”
“Logger kullandığımda, sadece mesajın yazılmasını istiyorum ve nerede olduğuyla uğraşmak istemiyorum. Bırakın global ayarlar kullanılsın. ”
Bunlar doğru yorumlar.
Örnek olarak, haber bültenleri gönderen ve bunun nasıl gittiğini kaydeden bir sınıfı ele alalım:
class NewsletterDistributor
{
public function distribute(): void
{
$logger = new Logger(/* ... */);
try {
$this->sendEmails();
$logger->log('Emails have been sent out');
} catch (Exception $e) {
$logger->log('An error occurred during the sending');
throw $e;
}
}
}
Artık LOG_DIR
sabitini kullanmayan geliştirilmiş Logger
, kurucuda bir dosya yolu gerektirir. Bu
nasıl çözülür? NewsletterDistributor
sınıfı mesajların nereye yazıldığı ile ilgilenmez, sadece onları
yazmak ister.
Çözüm yine kural 1: Sana geçirilsin Geçsin: sınıfın ihtiyaç duyduğu tüm verileri ona iletin.
Yani günlüğe giden yolu kurucuya aktarıyoruz ve daha sonra bunu Logger
nesnesini oluşturmak için
kullanıyoruz?
class NewsletterDistributor
{
public function __construct(
private string $file, // ⛔ BU ŞEKILDE DEĞIL!
) {
}
public function distribute(): void
{
$logger = new Logger($this->file);
Bu şekilde değil! Çünkü yol ** NewsletterDistributor
sınıfının ihtiyaç duyduğu verilere ait değildir;
Logger
. Sınıfın logger'ın kendisine ihtiyacı var. Ve biz de bunu aktaracağız:
class NewsletterDistributor
{
public function __construct(
private Logger $logger, // ✅
) {
}
public function distribute(): void
{
try {
$this->sendEmails();
$this->logger->log('Emails have been sent out');
} catch (Exception $e) {
$this->logger->log('An error occurred during the sending');
throw $e;
}
}
}
Artık NewsletterDistributor
sınıfının imzalarından, günlük tutmanın işlevselliğinin bir parçası
olduğu açıkça anlaşılmaktadır. Ve belki de test amaçlı olarak logger'ı başka bir logger ile değiştirme görevi
oldukça önemsizdir. Dahası, Logger
sınıfının kurucusu değiştirilirse, bunun bizim sınıfımız üzerinde
hiçbir etkisi olmayacaktır.
Kural #2: Sizin Olanı Alın
Yanlış yönlendirilmeyin ve bağımlılıklarınızın parametrelerinin size aktarılmasına izin vermeyin. Bağımlılıkları doğrudan aktarın.
Bu, diğer nesneleri kullanan kodu, kurucularındaki değişikliklerden tamamen bağımsız hale getirecektir. API'si daha doğru olacaktır. Ve en önemlisi, bu bağımlılıkları başkalarıyla değiştirmek önemsiz olacaktır.
Ailenin Yeni Üyesi
Geliştirme ekibi, veritabanına yazan ikinci bir logger oluşturmaya karar verdi. Böylece bir sınıf oluşturduk
DatabaseLogger
. Yani iki sınıfımız var, Logger
ve DatabaseLogger
, biri dosyaya
yazıyor, diğeri veritabanına yazıyor … sizce de bu isimde bir gariplik yok mu?
Logger
adını FileLogger
olarak değiştirmek daha iyi olmaz mıydı? Elbette olurdu.
Ama bunu akıllıca yapalım. Orijinal isim altında bir arayüz oluşturacağız:
interface Logger
{
function log(string $message): void;
}
…her iki kaydedicinin de uygulayacağı:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
Ve bu şekilde, logger'ın kullanıldığı kodun geri kalanında hiçbir şeyin değiştirilmesi gerekmeyecektir. Örneğin,
NewsletterDistributor
sınıfının kurucusu, parametre olarak Logger
'a ihtiyaç duymaktan memnun
olacaktır. Ve ona hangi örneği aktaracağımız bize bağlı olacaktır.
Bu nedenle arayüz isimlerine asla Interface
son ekini veya I
ön ekini vermiyoruz. Aksi
takdirde, bu kadar güzel kod geliştirmek imkansız olurdu.
Houston, Bir Sorunumuz Var
Tüm uygulamada, ister dosya ister veritabanı olsun, bir logger'ın tek bir örneğiyle mutlu olabilir ve bir şeyin
günlüğe kaydedildiği her yerde onu basitçe geçirebilirken, Article
sınıfı söz konusu olduğunda durum
oldukça farklıdır. Aslında, gerektiğinde, muhtemelen birden çok kez bunun örneklerini oluşturuyoruz. Kurucusundaki
veritabanı bağlayıcısıyla nasıl başa çıkılır?
Örnek olarak, bir form gönderdikten sonra bir makaleyi veritabanına kaydetmesi gereken bir denetleyici kullanabiliriz:
class EditController extends Controller
{
public function formSubmitted($data)
{
$article = new Article(/* ... */);
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
Olası bir çözüm doğrudan sunulmaktadır: veritabanı nesnesinin kurucu tarafından EditController
adresine
aktarılmasını sağlayın ve $article = new Article($this->db)
adresini kullanın.
Logger
ve dosya yolu ile ilgili önceki durumda olduğu gibi, bu doğru bir yaklaşım değildir. Veritabanı
EditController
'un değil, Article
'un bir bağımlılığıdır. Bu nedenle veritabanını geçmek 2. kurala senin olanı al aykırıdır. Article
sınıfının kurucusu
değiştirildiğinde (yeni bir parametre eklendiğinde), örneklerin oluşturulduğu tüm yerlerdeki kodun da değiştirilmesi
gerekecektir. Ufff.
Houston, ne öneriyorsun?
Kural #3: Bırakın Fabrika Halletsin
Gizli bağları kaldırarak ve tüm bağımlılıkları argüman olarak ileterek, daha yapılandırılabilir ve esnek sınıflar elde ederiz. Ve böylece bu daha esnek sınıfları oluşturmak ve yapılandırmak için başka bir şeye ihtiyacımız var. Buna fabrikalar diyeceğiz.
Temel kural şudur: Bir sınıfın bağımlılıkları varsa, örneklerinin oluşturulmasını fabrikaya bırakın.
Fabrikalar, bağımlılık enjeksiyonu dünyasında new
operatörü için daha akıllı bir alternatiftir.
Fabrika
Fabrika, nesneleri üreten ve yapılandıran bir yöntem veya sınıftır. Article
üreten sınıfa
ArticleFactory
diyoruz ve şöyle görünebilir:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Kontrol ünitesinde kullanımı aşağıdaki gibi olacaktır:
class EditController extends Controller
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function formSubmitted($data)
{
// fabrikanın bir nesne oluşturmasına izin verin
$article = $this->articleFactory->create();
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
Bu noktada, Article
sınıf kurucusunun imzası değiştiğinde, kodun yanıt vermesi gereken tek kısmı
ArticleFactory
fabrikasının kendisidir. Article
nesneleriyle çalışan EditController
gibi diğer kodlar bundan etkilenmeyecektir.
Şu anda kendimize hiç yardımcı olup olmadığımızı merak ederek alnınıza vuruyor olabilirsiniz. Kod miktarı arttı ve her şey şüpheli derecede karmaşık görünmeye başladı.
Merak etmeyin, yakında Nette DI konteynerine geçeceğiz. Ve bağımlılık enjeksiyonu kullanarak uygulama oluşturmayı son
derece basit hale getirecek bir dizi asa sahiptir. Örneğin, ArticleFactory
sınıfı yerine basit bir arayüz yazmak yeterli olacaktır:
interface ArticleFactory
{
function create(): Article;
}
Ama kendimizi aşıyoruz, bekleyin :-)
Özet
Bu bölümün başında size temiz kod tasarlamak için bir yol göstereceğimize söz vermiştik. Sadece sınıfları verin
- ihtiyaç duydukları bağımlılıklar
- ve doğrudan ihtiyaç duymadıkları şeyleri değil
- ve bağımlılıkları olan nesnelerin en iyi fabrikalarda yapıldığını
İlk bakışta öyle görünmeyebilir, ancak bu üç kuralın geniş kapsamlı etkileri vardır. Kod tasarımına kökten farklı bir bakış açısı getirirler. Buna değer mi? Eski alışkanlıklarını bir kenara bırakıp bağımlılık enjeksiyonunu tutarlı bir şekilde kullanmaya başlayan programcılar, bunu profesyonel yaşamlarında çok önemli bir an olarak görüyorlar. Net ve sürdürülebilir uygulamalar dünyasının kapılarını açmıştır.
Peki ya kod sürekli olarak bağımlılık enjeksiyonu kullanmıyorsa? Ya statik metotlar ya da singletonlar üzerine inşa edilmişse? Bu herhangi bir sorun yaratır mı? Getirir ve bu çok önemlidir.