Dependency Injection Nedir?
Bu bölüm, tüm uygulamaları yazarken uymanız gereken temel programlama uygulamalarını size tanıtacaktır. Bunlar temiz, anlaşılır ve sürdürülebilir kod yazmak için gerekli temellerdir.
Bu kuralları benimser ve uygularsanız, Nette her adımda size yardımcı olacaktır. Rutin görevleri sizin için halledecek ve mantığın kendisine odaklanabilmeniz için size maksimum rahatlık sağlayacaktır.
Burada göstereceğimiz prensipler oldukça basittir. Korkacak bir şey yok.
İlk Programınızı Hatırlıyor musunuz?
Hangi dilde yazdığınızı bilmiyoruz, ancak PHP olsaydı, muhtemelen şöyle görünürdü:
function soucet(float $a, float $b): float
{
return $a + $b;
}
echo soucet(23, 1); // 24 yazdırır
Birkaç önemsiz kod satırı, ancak içlerinde çok sayıda anahtar kavram gizli. Değişkenlerin var olduğu. Kodun, örneğin fonksiyonlar gibi daha küçük birimlere ayrıldığı. Onlara girdi argümanları ilettiğimiz ve sonuçları döndürdükleri. Sadece koşullar ve döngüler eksik.
Fonksiyona girdi verilerini iletmemiz ve bir sonuç döndürmesi, matematikte olduğu gibi diğer alanlarda da kullanılan mükemmel anlaşılır bir kavramdır.
Bir fonksiyonun, adını, parametrelerinin ve türlerinin bir özetini ve son olarak dönüş değerinin türünü içeren bir imzası vardır. Kullanıcılar olarak bizi ilgilendiren imzadır; genellikle iç uygulama hakkında hiçbir şey bilmemize gerek yoktur.
Şimdi fonksiyon imzasının şöyle göründüğünü hayal edin:
function soucet(float $x): float
Tek parametreli bir toplam mı? Bu garip… Peki ya şöyle?
function soucet(): float
Bu gerçekten çok garip, değil mi? Fonksiyon nasıl kullanılır?
echo soucet(); // ne yazdırır acaba?
Böyle bir koda baktığımızda kafamız karışırdı. Sadece bir başlangıç seviyesindeki kişi anlamazdı, yetenekli bir programcı bile böyle bir kodu anlamazdı.
Böyle bir fonksiyonun içinde nasıl görüneceğini merak ediyor musunuz? Toplanacak sayıları nereden alır? Muhtemelen onları bir şekilde kendi başına elde ederdi, belki şöyle:
function soucet(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
Fonksiyon gövdesinde, diğer global fonksiyonlara veya statik metotlara gizli bağlantılar keşfettik. Toplanacak sayıların gerçekten nereden geldiğini bulmak için daha fazla araştırmamız gerekiyor.
Bu Yol Yanlış!
Az önce gösterdiğimiz tasarım, birçok olumsuz özelliğin özüdür:
- fonksiyon imzası, toplanacak sayılara ihtiyaç duymuyormuş gibi davrandı, bu da kafamızı karıştırdı
- fonksiyonun başka iki sayıyı nasıl toplayacağını hiç bilmiyoruz
- toplanacak sayıları nereden aldığını bulmak için koda bakmak zorunda kaldık
- gizli bağlantılar keşfettik
- tam olarak anlamak için bu bağlantıları da incelemek gerekiyor
Ve girdileri elde etmek gerçekten toplama fonksiyonunun görevi mi? Tabii ki değil. Sorumluluğu sadece toplama işleminin kendisidir.
Böyle bir kodla karşılaşmak istemiyoruz ve kesinlikle yazmak istemiyoruz. Çözüm basit: temellere geri dönün ve sadece parametreleri kullanın:
function soucet(float $a, float $b): float
{
return $a + $b;
}
Kural 1: Size İletilmesini Sağlayın
En önemli kural şudur: fonksiyonların veya sınıfların ihtiyaç duyduğu tüm veriler onlara iletilmelidir.
Onlara bir şekilde kendi başlarına ulaşabilecekleri gizli yollar icat etmek yerine, parametreleri basitçe iletin. Kodunuzu kesinlikle iyileştirmeyecek gizli yollar icat etmek için gereken zamandan tasarruf edeceksiniz.
Bu kuralı her zaman ve her yerde uygularsanız, gizli bağlantıları olmayan bir koda giden yoldasınız demektir. Sadece yazar tarafından değil, ondan sonra okuyacak herkes tarafından anlaşılır olan bir koda. Fonksiyonların ve sınıfların imzalarından her şeyin anlaşılabildiği ve uygulamada gizli sırları aramaya gerek olmayan bir koda.
Bu tekniğe profesyonel olarak dependency injection denir. Ve bu verilere bağımlılıklar denir. Aslında, bu sadece parametre iletmedir, başka bir şey değil.
Lütfen bir tasarım deseni olan dependency injection ile bir araç olan, yani tamamen farklı bir şey olan "dependency injection container"ı karıştırmayın. Konteynerleri daha sonra ele alacağız.
Fonksiyonlardan Sınıflara
Peki bunun sınıflarla ne ilgisi var? Bir sınıf, basit bir fonksiyondan daha karmaşık bir bütündür, ancak Kural 1 burada da tamamen geçerlidir. Sadece argümanları iletmenin daha fazla yolu vardır. Örneğin, bir fonksiyona oldukça benzer şekilde:
class Matematika
{
public function soucet(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Matematika;
echo $math->soucet(23, 1); // 24
Veya diğer metotlarla ya da doğrudan yapıcı ile:
class Soucet
{
public function __construct(
private float $a,
private float $b,
) {
}
public function spocti(): float
{
return $this->a + $this->b;
}
}
$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24
Her iki örnek de dependency injection ile tamamen uyumludur.
Gerçek Hayat Örnekleri
Gerçek dünyada, sayıları toplamak için sınıflar yazmayacaksınız. Pratik örneklere 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 kaydedeceğiz
}
}
ve kullanımı şöyle olacaktır:
$article = new Article;
$article->title = 'Kilo Verme Hakkında Bilmeniz Gereken 10 Şey';
$article->content = 'Her yıl milyonlarca insan ...';
$article->save();
save()
metodu makaleyi bir veritabanı tablosuna kaydeder. Nette
Database kullanarak uygulamak çocuk oyuncağı olurdu, ancak bir engel var: Article
veritabanı
bağlantısını, yani Nette\Database\Connection
sınıfının nesnesini nereden alacak?
Görünüşe göre birçok seçeneğimiz var. Statik bir değişkenden alabilir. Veya veritabanı bağlantısını sağlayan bir sınıftan miras alabilir. Veya singleton olarak adlandırılanı kullanabilir. Veya Laravel'de kullanılan facades olarak adlandırılanları kullanabilir:
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.
Ya da çözmedik mi?
Kural 1: Size İletilmesini Sağlayın hatırlayalım: sınıfın ihtiyaç duyduğu tüm bağımlılıklar ona iletilmelidir. Çünkü kuralı ihlal edersek, gizli bağlantılarla dolu, anlaşılmaz, kirli bir koda giden yola girmiş oluruz ve sonuç, bakımı ve geliştirilmesi acı verici olacak bir uygulama olur.
Article
sınıfının kullanıcısı, save()
metodunun makaleyi nereye kaydettiğini bilmiyor. Bir
veritabanı tablosuna mı? Hangisine, canlıya mı yoksa test olanına mı? Ve bu nasıl değiştirilebilir?
Kullanıcı, save()
metodunun nasıl uygulandığına bakmalı ve DB::insert()
metodunun
kullanımını bulmalıdır. Bu yüzden, bu metodun veritabanı bağlantısını nasıl elde ettiğini daha fazla
araştırmalıdır. Ve gizli bağlantılar oldukça uzun bir zincir oluşturabilir.
Temiz ve iyi tasarlanmış kodda asla gizli bağlantılar, Laravel facades veya statik değişkenler bulunmaz. Temiz ve iyi tasarlanmış kodda argümanlar iletilir:
class Article
{
public function save(Nette\Database\Connection $db): void
{
$db->query('INSERT INTO articles', [
'title' => $this->title,
'content' => $this->content,
]);
}
}
Daha da pratik olanı, ileride göreceğimiz gibi, yapıcı ile olacaktır:
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,
]);
}
}
Eğer deneyimli bir programcıysanız, muhtemelen Article
sınıfının hiç save()
metoduna sahip olmaması gerektiğini, tamamen bir veri bileşeni olması gerektiğini ve kaydetme işleminin ayrı bir depo
tarafından yapılması gerektiğini düşünüyorsunuzdur. Bu mantıklı. Ancak bu bizi dependency injection konusunun çok
ötesine ve basit örnekler verme çabasının dışına çıkarırdı.
Faaliyeti için örneğin bir veritabanı gerektiren bir sınıf yazıyorsanız, onu nereden alacağınızı düşünmeyin, size iletilmesini sağlayın. Belki yapıcı veya başka bir metodun parametresi olarak. Bağımlılıkları kabul edin. Onları sınıfınızın API'sinde kabul edin. Anlaşılır ve öngörülebilir bir kod elde edeceksiniz.
Peki ya 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);
}
}
Sizce Kural 1: Size İletilmesini Sağlayın uyduk mu?
Uymadık.
Anahtar bilgi, yani günlük dosyasının bulunduğu dizin, sınıf tarafından kendi başına bir sabitten elde ediliyor.
Kullanım örneğine bakın:
$logger = new Logger;
$logger->log('Sıcaklık 23 °C');
$logger->log('Sıcaklık 10 °C');
Uygulamayı bilmeden, mesajların nereye yazıldığı sorusunu cevaplayabilir miydiniz? Çalışması için
LOG_DIR
sabitinin varlığının gerekli olduğunu düşünür müydünüz? Ve başka bir yere yazacak ikinci bir
örnek oluşturabilir miydiniz? 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 şimdi çok daha anlaşılır, yapılandırılabilir ve dolayısıyla daha kullanışlı.
$logger = new Logger('/path/to/log.txt');
$logger->log('Sıcaklık 15 °C');
Ama Bu Beni İlgilendirmiyor!
„Bir Article nesnesi oluşturup save() ç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 nereye yazılacağıyla ilgilenmek istemiyorum. Global ayar kullanılsın.“
Bunlar doğru yorumlar.
Örnek olarak, bültenleri dağıtan ve sonucunu günlüğe kaydeden bir sınıf göstereceğiz:
class NewsletterDistributor
{
public function distribute(): void
{
$logger = new Logger(/* ... */);
try {
$this->sendEmails();
$logger->log('E-postalar gönderildi');
} catch (Exception $e) {
$logger->log('Gönderim sırasında bir hata oluştu');
throw $e;
}
}
}
Artık LOG_DIR
sabitini kullanmayan geliştirilmiş Logger
, yapıcısında dosya yolunun
belirtilmesini gerektiriyor. Bunu nasıl çözeceğiz? NewsletterDistributor
sınıfı mesajların nereye
yazıldığıyla hiç ilgilenmiyor, sadece onları yazmak istiyor.
Çözüm yine Kural 1: Size İletilmesini Sağlayın: sınıfın ihtiyaç duyduğu tüm verileri ona iletiyoruz.
Yani bu, Logger
nesnesini oluştururken kullanacağımız günlük yolunu yapıcı aracılığıyla ileteceğimiz
anlamına mı geliyor?
class NewsletterDistributor
{
public function __construct(
private string $file, // ⛔ BU ŞEKİLDE DEĞİL!
) {
}
public function distribute(): void
{
$logger = new Logger($this->file);
Bu şekilde değil! Çünkü yol, NewsletterDistributor
sınıfının ihtiyaç duyduğu veriler arasında
değildir; bunlara Logger
ihtiyaç duyar. Farkı anlıyor musunuz? NewsletterDistributor
sınıfı, logger'ın kendisine ihtiyaç duyar. Bu yüzden onu ileteceğiz:
class NewsletterDistributor
{
public function __construct(
private Logger $logger, // ✅
) {
}
public function distribute(): void
{
try {
$this->sendEmails();
$this->logger->log('E-postalar gönderildi');
} catch (Exception $e) {
$this->logger->log('Gönderim sırasında bir hata oluştu');
throw $e;
}
}
}
Şimdi NewsletterDistributor
sınıfının imzalarından, işlevselliğinin bir parçası olarak günlüklemenin
de olduğu açıktır. Ve logger'ı başka biriyle değiştirmek, örneğin test için, tamamen önemsizdir. Ayrıca,
Logger
sınıfının yapıcısı değişirse, bunun sınıfımız üzerinde hiçbir etkisi olmayacaktır.
Kural 2: Sadece Size Ait Olanı Alın
Kafanızın karışmasına izin vermeyin ve bağımlılıklarınızın bağımlılıklarını size iletmeyin. Sadece kendi bağımlılıklarınızı size iletin.
Bu sayede, diğer nesneleri kullanan kod, yapıcılarındaki değişikliklerden tamamen bağımsız olacaktır. API'si daha doğru olacaktır. Ve en önemlisi, bu bağımlılıkları başkalarıyla değiştirmek önemsiz olacaktır.
Aileye Yeni Üye
Geliştirme ekibinde, veritabanına yazan ikinci bir logger oluşturma kararı alındı. Bu yüzden DatabaseLogger
sınıfını oluşturacağız. Yani iki sınıfımız var, Logger
ve DatabaseLogger
, biri dosyaya
yazıyor, diğeri veritabanına… Bu isimlendirmede size garip gelen bir şey yok mu? Logger
ı
FileLogger
olarak yeniden adlandırmak daha iyi olmaz mıydı? Kesinlikle evet.
Ama bunu akıllıca yapacağız. Orijinal ad altında bir arayüz oluşturacağız:
interface Logger
{
function log(string $message): void;
}
… her iki logger da bunu uygulayacak:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
Ve bu sayede, logger'ın kullanıldığı kodun geri kalanında hiçbir şeyi değiştirmeye gerek kalmayacak. Örneğin,
NewsletterDistributor
sınıfının yapıcısı, parametre olarak Logger
gerektirmesinden hala memnun
olacaktır. Ve hangi örneği ona ileteceğimiz bize kalmış olacak.
Bu nedenle arayüz adlarına asla Interface
sonekini veya I
önekini vermeyiz. Aksi takdirde,
kodu bu kadar güzel bir şekilde geliştirmek mümkün olmazdı.
Houston, Bir Problemimiz Var
Tüm uygulamada, ister dosya tabanlı ister veritabanı tabanlı olsun, tek bir logger örneğiyle idare edebilir ve onu bir
şeylerin günlüğe kaydedildiği her yere basitçe iletebilirken, Article
sınıfı durumunda durum oldukça
farklıdır. Örneklerini ihtiyaca göre, hatta birden çok kez oluştururuz. Yapıcısındaki veritabanı bağımlılığıyla
nasıl başa çıkılır?
Örnek olarak, bir form gönderildikten sonra makaleyi veritabanına kaydetmesi gereken bir denetleyici (controller) hizmet edebilir:
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 kendini gösteriyor: veritabanı nesnesini yapıcı aracılığıyla EditController
'a iletelim
ve $article = new Article($this->db)
kullanalım.
Önceki Logger
ve dosya yolu örneğinde olduğu gibi, bu doğru bir yaklaşım değildir. Veritabanı
EditController
'ın değil, Article
'ın bir bağımlılığıdır. Bu nedenle veritabanını iletmek Kural 2: Sadece Size Ait Olanı Alın aykırıdır. Article
sınıfının yapıcısı değiştiğinde (yeni bir parametre eklendiğinde), örneklerin oluşturulduğu tüm yerlerdeki kodu da
değiştirmek gerekecektir. Ufff.
Houston, ne önerirsin?
Kural 3: Fabrikaya Bırakın
Gizli bağlantı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 ettik. Ve dolayısıyla, bu daha esnek sınıfları bizim için oluşturacak ve yapılandıracak başka bir şeye ihtiyacımız var. Buna fabrikalar diyeceğiz.
Kural şudur: Bir sınıfın bağımlılıkları varsa, örneklerinin oluşturulmasını bir fabrikaya bırakın.
Fabrikalar, dependency injection dünyasında new
operatörünün daha akıllı bir alternatifidir.
Lütfen fabrikaların belirli bir kullanım şeklini tanımlayan ve bu konuyla ilgisi olmayan factory method tasarım deseniyle karıştırmayın.
Fabrika
Fabrika, nesneleri üreten ve yapılandıran bir metot veya sınıftır. Article
üreten sınıfı
ArticleFactory
olarak adlandıracağız ve örneğin şöyle görünebilir:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Denetleyicideki kullanımı şöyle olacaktır:
class EditController extends Controller
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function formSubmitted($data)
{
// fabrikanın nesneyi oluşturmasına izin veriyoruz
$article = $this->articleFactory->create();
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
Bu noktada Article
sınıfının yapıcısının imzası değişirse, buna tepki vermesi gereken tek kod parçası
ArticleFactory
fabrikasının kendisidir. Article
nesneleriyle çalışan diğer tüm kodlar, örneğin
EditController
, bundan hiçbir şekilde etkilenmeyecektir.
Belki şimdi kendinize hiç yardımcı olup olmadığımızı merak ederek alnınıza vuruyorsunuzdur. Kod miktarı arttı ve her şey şüpheli bir şekilde karmaşık görünmeye başladı.
Endişelenmeyin, birazdan Nette DI konteynerine geleceğiz. Ve dependency injection kullanan uygulamalar oluşturmayı son
derece basitleştiren birçok numarası var. Örneğin, ArticleFactory
sınıfı yerine sadece bir arayüz yazın yeterli olacaktır:
interface ArticleFactory
{
function create(): Article;
}
Ama acele ediyoruz, biraz daha bekleyin :-)
Özet
Bu bölümün başında, temiz kod tasarlamak için bir prosedür göstereceğimize söz vermiştik. Sınıflara sadece şunları yapmak yeterlidir:
- ihtiyaç duydukları bağımlılıkları iletin
- ve doğrudan ihtiyaç duymadıklarını iletmeyin
- ve bağımlılıkları olan nesnelerin en iyi fabrikalarda üretildiği
İlk bakışta öyle görünmeyebilir, ancak bu üç kuralın geniş kapsamlı sonuçları vardır. Kod tasarımına kökten farklı bir bakış açısına yol açarlar. Buna değer mi? Eski alışkanlıklarını bırakan ve tutarlı bir şekilde dependency injection kullanmaya başlayan programcılar, bu adımı profesyonel yaşamlarında önemli bir an olarak görürler. Onlara açık ve sürdürülebilir uygulamaların dünyasını açtı.
Peki ya kod tutarlı bir şekilde dependency injection kullanmıyorsa? Statik metotlara veya singleton'lara dayanıyorsa ne olur? Bu herhangi bir sorun yaratır mı? Getirir ve çok temeldir.