Bağımlılık Enjeksiyonu Nedir?

Bu bölüm size herhangi bir uygulama yazarken izlemeniz gereken temel programlama uygulamalarını tanıtacaktır. Bunlar temiz, anlaşılabilir ve sürdürülebilir 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 maksimum konfor sağlayacaktır, böylece siz mantığın kendisine odaklanabilirsiniz.

Burada göstereceğimiz ilkeler oldukça basittir. Hiçbir şey için endişelenmenize gerek yok.

İlk Programınızı Hatırlıyor musunuz?

Hangi dilde yazdığınızı bilmiyoruz, ancak PHP olsaydı, şöyle bir şeye benzeyebilirdi:

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 fonksiyonun girdi verilerini alması ve bir sonuç döndürmesi, matematik gibi diğer alanlarda da kullanılan, tamamen anlaşılabilir bir kavramdır.

Bir fonksiyonun imzası vardır ve bu imza fonksiyonun adı, parametrelerin listesi ve tipleri ile son olarak geri dönüş değerinin tipinden oluşur. Kullanıcılar olarak biz imzayla ilgileniriz ve genellikle dahili uygulama hakkında bir şey bilmemiz gerekmez.

Şimdi fonksiyon imzasının aşağıdaki gibi olduğunu hayal edin:

function toplami(float $x): float

Tek parametreli bir ekleme mi? Bu çok garip. Peki ya bu?

function toplami(): float

Şimdi bu gerçekten garip, değil mi? Fonksiyon nasıl kullanılıyor?

echo toplami(); // ne yazdırıyor?

Böyle bir koda baktığımızda kafamız karışır. Sadece yeni başlayanlar değil, deneyimli bir programcı bile böyle bir kodu anlamayacaktır.

Böyle bir fonksiyonun içinde gerçekte neye benzeyeceğini merak ediyor musunuz? Toplamları nereden alırdı? Muhtemelen bir şekilde kendi kendine alırdı, belki de şu şekilde:

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 gösterdiğimiz tasarım, birçok olumsuz özelliğin özüdür:

  • fonksiyon imzası toplamlara 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
  • toplamların nereden geldiğini bulmak için koda bakmamız gerekiyordu
  • gizli bağımlılıklar bulduk
  • tam olarak anlaşılabilmesi için bu bağımlılıkların da incelenmesi 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.

Verilere kendilerinin erişmesi için gizli yollar icat etmek yerine, sadece parametreleri aktarın. Kodunuzu kesinlikle iyileştirmeyecek gizli yollar icat etmek için harcayacağınız zamandan tasarruf edeceksiniz.

Her zaman ve her yerde bu kurala uyarsanız, gizli bağımlılıkları olmayan bir 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. 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 profesyonel olarak bağımlılık enjeksiyonu olarak adlandırılır. Ve bu verilere bağımlılıklar denir. Bu sadece sıradan 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 bir araç olan “bağımlılık enjeksiyon konteyneri” ile karıştırmayın, bu tamamen farklı bir şeydir. Kapsayıcılar ile daha sonra ilgileneceğiz.

Fonksiyonlardan Sınıflara

Peki sınıflar nasıl ilişkilidir? Bir sınıf basit bir fonksiyondan daha karmaşık bir birimdir, ancak 1. kural burada da tamamen 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

Veya diğer yöntemler aracılığıyla ya da doğrudan kurucu aracılığıyla:

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. Şimdi pratik örneklere geçelim.

Bir blog gönderisini 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 kaydedecektir. Bunu Nette Database kullanarak uygulamak, bir aksaklık olmasaydı, çocuk oyuncağı olacaktı: Article veritabanı bağlantısını, yani Nette\Database\Connection sınıfının bir nesnesini nereden alacak?

Görünüşe göre birçok seçeneğimiz var. Bir yerdeki statik bir değişkenden alabilir. Ya da veritabanı bağlantısı sağlayan bir sınıftan miras alabilir. Ya da bir singleton'dan yararlanabilir. Ya da Laravel'de kullanılan sözde facade'leri 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.

Yoksa biz mi?

Kural 1: Sana geçirilsin Geçsin: sınıfın ihtiyaç duyduğu tüm bağımlılıklar ona geçmelidir. Çünkü bu kuralı ihlal edersek, gizli bağımlılıklarla dolu kirli bir koda, anlaşılmazlığa giden bir yola girmiş oluruz ve sonuçta bakımı ve geliştirilmesi sancılı 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, üretim mi test mi? Ve nasıl değiştirilebilir?

Kullanıcı save() yönteminin nasıl uygulandığına bakmalı ve DB::insert() yönteminin kullanımını bulmalıdır. Dolayısıyla, bu yöntemin bir veritabanı bağlantısını nasıl elde ettiğini bulmak için daha fazla araştırma yapması gerekir. Ve gizli bağımlılıklar oldukça uzun bir zincir oluşturabilir.

Temiz ve iyi tasarlanmış kodda, hiçbir zaman gizli bağımlılıklar, Laravel cepheleri veya statik değişkenler yoktur. 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, daha da pratik bir yaklaşım kurucu aracılığıyla 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,
		]);
	}
}

Deneyimli bir programcıysanız, Article adresinin bir save() yöntemine sahip olmaması gerektiğini düşünebilirsiniz; tamamen bir veri bileşenini temsil etmeli ve ayrı bir depo kaydetme işlemiyle ilgilenmelidir. Bu mantıklıdır. Ancak bu bizi bağımlılık enjeksiyonu olan konunun kapsamının ve basit örnekler sunma çabasının çok ötesine götürecektir.

Örneğin çalışması için bir veritabanına ihtiyaç duyan bir sınıf yazıyorsanız, veritabanını nereden alacağınızı icat etmeyin, ancak veritabanını geçirin. Ya kurucunun bir parametresi olarak ya da başka bir yöntemle. Bağımlılıkları kabul edin. Bunları sınıfınızın API'sinde kabul edin. Anlaşılabilir ve öngörülebilir bir kod elde edeceksiniz.

Peki ya hata mesajlarını günlüğe kaydeden bu sınıf?

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, yani günlük dosyasının bulunduğu dizin, sınıfın kendisi tarafından sabitten elde edilir.

Kullanım örneğine 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? Çalışması için LOG_DIR sabitinin varlığının gerekli olduğunu tahmin eder miydiniz? Ve farklı bir konuma yazacak 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 anlaşılır, 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şturup 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 geçerli noktalar.

Örnek olarak, haber bültenleri gönderen ve nasıl gittiğini günlüğe kaydeden bir sınıfa bakalı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, dosya yolunun yapıcıda belirtilmesini 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 1 numaralı kuraldır: Bırakın Size Geçsin: sınıfın ihtiyaç duyduğu tüm verileri iletin.

Yani bu, günlüğe giden yolu kurucu aracılığıyla ilettiğimiz ve daha sonra Logger nesnesini oluştururken kullandığımız anlamına mı geliyor?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ BU ŞEKILDE DEĞIL!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Hayır, bu şekilde değil! Yol, NewsletterDistributor sınıfının ihtiyaç duyduğu veriler arasında yer almaz; aslında Logger 'un buna ihtiyacı vardır. Aradaki farkı görüyor musunuz? NewsletterDistributor sınıfının logger'ın kendisine ihtiyacı var. Bu yüzden bunu geçeceğiz:

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 da işlevselliğinin bir parçası olduğu açıkça anlaşılmaktadır. Ve belki de test için kaydediciyi başka bir kaydediciyle değiştirme görevi tamamen önemsizdir. Dahası, Logger sınıfının kurucusu değişirse, bu bizim sınıfımızı etkilemeyecektir.

Kural #2: Senin Olanı Al

Yanlış yönlendirilmeyin ve bağımlılıklarınızın bağımlılıklarını kendinizin geçmesine izin vermeyin. Sadece kendi bağımlılıklarınızı geçirin.

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 hepsinden önemlisi, bu bağımlılıkları başkalarıyla değiştirmek önemsiz olacaktır.

Yeni Aile Üyesi

Geliştirme ekibi, veritabanına yazan ikinci bir logger oluşturmaya karar verdi. Böylece bir DatabaseLogger sınıfı oluşturduk. Yani iki sınıfımız var, Logger ve DatabaseLogger, biri bir dosyaya, diğeri bir veritabanına yazıyor … adlandırma size de garip gelmiyor mu?
Logger 'u FileLogger olarak yeniden adlandırmak daha iyi olmaz mıydı? Kesinlikle evet.

Ama bunu akıllıca yapalım. Orijinal isim altında bir arayüz oluşturalım:

interface Logger
{
	function log(string $message): void;
}

… her iki kaydedicinin de uygulayacağı:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

Ve bu nedenle, logger'ın kullanıldığı kodun geri kalanında hiçbir şeyi değiştirmeye gerek kalmayacaktır. Örneğin, NewsletterDistributor sınıfının kurucusu hala parametre olarak Logger 'a ihtiyaç duymakla yetinecektir. Ve hangi örneği geçireceğimiz bize bağlı olacaktır.

Bu yüzden arayüz isimlerine asla Interface son ekini veya I ön ekini eklemiyoruz. Aksi takdirde kodu bu kadar güzel geliştirmek mümkün olmazdı.

Houston, Bir Sorunumuz Var

İster dosya tabanlı ister veritabanı tabanlı olsun, tüm uygulama boyunca tek bir logger örneğiyle idare edebilir ve bir şeyin günlüğe kaydedildiği her yerde onu basitçe geçirebilirken, Article sınıfı için durum oldukça farklıdır. Gerektiğinde, hatta birden çok kez örneklerini oluşturuyoruz. Kurucusundaki veritabanı bağımlılığı ile nasıl başa çıkılır?

Örnek olarak, bir form gönderdikten sonra bir makaleyi veritabanına kaydetmesi gereken bir denetleyici verilebilir:

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 açıktır: veritabanı nesnesini EditController kurucusuna aktarın ve $article = new Article($this->db) adresini kullanın.

Tıpkı 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. Veritabanını geçmek 2. kurala aykırıdır : senin olanı al. Article sınıf kurucusu değişirse (yeni bir parametre eklenirse), örneklerin oluşturulduğu her yerde kodu değiştirmeniz gerekecektir. Ufff.

Houston, ne öneriyorsun?

Kural #3: Bırakın Fabrika Halletsin

Gizli bağımlılıkları ortadan kaldırarak ve tüm bağımlılıkları argüman olarak geçirerek, daha yapılandırılabilir ve esnek sınıflar elde ettik. Bu nedenle, bizim için bu daha esnek sınıfları oluşturmak ve yapılandırmak için başka bir şeye ihtiyacımız var. Biz 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.

Lütfen fabrikaları kullanmanın belirli bir yolunu tanımlayan ve bu konuyla ilgili olmayan factory method tasarım kalıbı ile karıştırmayın.

Fabrika

Fabrika, nesneleri oluşturan ve yapılandıran bir yöntem veya sınıftır. Article üreten sınıfı ArticleFactory olarak adlandıracağız ve şu şekilde görünebilir:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Kontrolördeki 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şirse, kodun tepki vermesi gereken tek kısmı ArticleFactory 'un kendisidir. Article nesneleriyle çalışan EditController gibi diğer tüm kodlar etkilenmeyecektir.

İşleri gerçekten daha iyi hale getirip getirmediğimizi merak ediyor olabilirsiniz. Kod miktarı arttı ve hepsi şüpheli bir şekilde 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ı büyük ölçüde basitleştirecek birkaç hilesi var. Örneğin, ArticleFactory sınıfı yerine sadece basit bir arayüz yazmanız gerekecek:

interface ArticleFactory
{
	function create(): Article;
}

Ama biz kendimizi aşıyoruz; lütfen sabırlı olun :-)

Özet

Bu bölümün başında size temiz kod tasarlamak için bir süreç göstereceğimize söz vermiştik. Tek gereken sınıflar için

İlk bakışta, bu üç kuralın geniş kapsamlı sonuçları yokmuş gibi görünebilir, ancak kod tasarımına kökten farklı bir bakış açısı getirirler. Buna değer mi? Eski alışkanlıklarını terk edip bağımlılık enjeksiyonunu tutarlı bir şekilde kullanmaya başlayan geliştiriciler, bu adımı profesyonel yaşamlarında çok önemli bir an olarak görüyorlar. Onlar için açık 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 yöntemlere ya da tekillere dayanıyorsa? Bu herhangi bir soruna neden olur mu? Evet, neden olur, hem de çok temel sorunlara.

versiyon: 3.x