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:

  1. ihtiyaç duydukları bağımlılıkları iletin
  2. ve doğrudan ihtiyaç duymadıklarını iletmeyin
  3. 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.

versiyon: 3.x