Servislerin İsimlere İhtiyacı Yoktur
Nette Framework'ün bağımlılık enjeksiyonu (dependency injection) çözümünü seviyorum. Gerçekten seviyorum. Bu makale, bu tutkuyu paylaşmak ve neden mevcut PHP ekosistemindeki en iyi DI çözümü olduğunu düşündüğümü açıklamak için burada.

(Bu gönderi ilk olarak yazarın blogunda yayınlanmıştır.)
Yazılım geliştirme, sonsuz bir yinelemeli soyutlama sürecidir. Alan modellemesinde gerçek dünyanın uygun soyutlamalarını buluruz. Nesne yönelimli programlamada, sistemdeki farklı aktörler arasındaki sözleşmeleri tanımlamak ve zorlamak için soyutlamalar kullanırız. Sorumlulukları kapsüllemek ve sınırlarını tanımlamak için sisteme yeni sınıflar ekleriz ve ardından tüm sistemi oluşturmak için kompozisyon kullanırız.
Aşağıdaki denetleyiciden kimlik doğrulama mantığını çıkarma dürtüsünden bahsediyorum:
final class SignInController extends Best\Framework\Controller
{
public function action(string $username, string $password): Best\Framework\Response
{
if ($username !== 'admin' || $password !== 'p4ssw0rd!') {
return $this->render(__DIR__ . '/error.latte');
}
$this->signIn(new Identity($username));
return $this->redirect(HomepageController::class);
}
}
Muhtemelen kimlik bilgisi kontrolünün oraya ait olmadığını anlayacaksınız. Denetleyicinin hangi kimlik bilgilerinin geçerli olduğunu belirleme sorumluluğu yoktur – tek sorumluluk prensibine göre, denetleyicinin yalnızca tek bir değiştirme nedeni olmalıdır ve bu neden, kimlik doğrulama sürecinde değil, uygulamanın kullanıcı arayüzü kapsamında olmalıdır.
Bundan bariz yolu izleyelim ve koşulu Authenticator
sınıfına
çıkaralım:
final class Authenticator
{
public function authenticate(string $username, string $password): bool
{
return $username === 'admin' && $password === 'p4ssw0rd!';
}
}
Şimdi tek yapmamız gereken denetleyiciden bu kimlik doğrulayıcıya yetki vermek. Kimlik doğrulayıcıyı denetleyicinin bir bağımlılığı yaptık ve denetleyicinin aniden onu bir yerden alması gerekiyor:
final class SignInController extends Best\Framework\Controller
{
public function action(string $username, string $password): Best\Framework\Response
{
$authenticator = new Authenticator(); // <== kolay, sadece yeni bir tane oluştururum!
if ( ! $authenticator->authenticate($username, $password)) {
return $this->render(__DIR__ . '/error.latte');
}
$this->signIn(new Identity($username));
return $this->redirect(HomepageController::class);
}
}
Bu naif yol işe yarayacaktır. Ancak yalnızca, kimlik doğrulayıcının
kullanıcıların veritabanı tablosunu sorgulamasını gerektirecek daha
sağlam bir kimlik doğrulama uygulanana kadar. Aniden
Authenticator
'ın kendi bağımlılığı vardır, diyelim ki
UserRepository
, bu da belirli ortamın parametrelerine bağlı olan
bir Connection
örneğine bağlıdır. Bu hızla büyüdü!
Her yerde örnekleri manuel olarak oluşturmak, bağımlılıkları
yönetmenin sürdürülebilir bir yolu değildir. Bu yüzden, denetleyicinin
yalnızca Authenticator
'a olan bağımlılığını bildirmesine ve
örneği gerçekten sağlamayı başkasına bırakmasına izin veren
bağımlılık enjeksiyonu modeline sahibiz. Ve bu başka birine bağımlılık
enjeksiyonu konteyneri denir.
Bağımlılık enjeksiyonu konteyneri, uygulamanın baş mimarıdır – sistemdeki herhangi bir servisin bağımlılıklarını çözebilir ve bunları oluşturmaktan sorumludur. DI konteynerleri bugün o kadar yaygındır ki, neredeyse her büyük web framework'ünün kendi konteyner uygulaması vardır ve hatta PHP-DI gibi bağımlılık enjeksiyonuna adanmış ayrı paketler bile vardır.
Biber Yakmak
Seçeneklerin çokluğu sonunda bir grup geliştiriciyi onları birlikte çalışabilir hale getirecek bir soyutlama aramaya motive etti. Ortak bir arayüz zamanla geliştirildi ve sonunda PHP-FIG için aşağıdaki biçimde önerildi:
interface ContainerInterface
{
public function get(string $id): mixed;
public function has(string $id): bool;
}
Bu arayüz, DI konteynerlerinin çok önemli bir özelliğini gösteriyor: İyi bir hizmetkardır, ancak kolayca kötü bir efendi olabilirler. Nasıl kullanılacağını biliyorsanız son derece faydalıdırlar, ancak yanlış kullanırsanız sizi yakarlar. Aşağıdaki konteyneri ele alalım:
final class Container implements ContainerInterface
{
private array $factories = [];
public function __construct(array $parameters)
{
$this->factories['authenticator'] = fn() => new Authenticator($this->get('userRepository'));
$this->factories['userRepository'] = fn() => new UserRepository($this->get('connection'));
$this->factories['connection'] = fn() => new Connection($parameters['database']);
}
public function get(string $id): mixed { /* . . . */ }
public function has(string $id): bool { /* . . . */ }
}
Şimdiye kadar iyi. Uygulama, belirlediğimiz standartlara göre iyi görünüyor: gerçekten uygulamadaki her servisi oluşturabilir ve bağımlılıklarını özyinelemeli olarak çözebilir. Her şey tek bir yerden yönetilir ve konteyner parametreleri bile kabul eder, böylece veritabanı bağlantısı kolayca yapılandırılabilir. Güzel!
Ancak şimdi yalnızca iki ContainerInterface
metodunu
gördüğünüzde, konteyneri şu şekilde kullanmaya yönelebilirsiniz:
final class SignInController extends Best\Framework\Controller
{
public function __construct(
private ContainerInterface $container,
) {}
public function action(string $username, string $password): Best\Framework\Response
{
$authenticator = $this->container->get('authenticator');
//...
}
}
Tebrikler, az önce biberi yaktınız. Başka bir deyişle, konteyner kötü bir efendi haline geldi. Neden böyle?
İlk olarak, rastgele bir servis tanımlayıcısına güveniyorsunuz:
'authenticator'
. Bağımlılık enjeksiyonu,
bağımlılıklarınız konusunda şeffaf olmakla ilgilidir ve yapay bir
tanımlayıcı kullanmak bu kavrama doğrudan aykırıdır: kodun sessizce
konteyner tanımına bağımlı olmasına neden olur. Konteynerdeki bir servis
yeniden adlandırılırsa, bu referansı bulup güncellemeniz gerekir.
Daha da kötüsü, bu bağımlılık gizlidir: dışarıdan ilk bakışta,
denetleyici yalnızca konteyner soyutlamasına bağlıdır. Ancak bir
geliştirici olarak sizin konteynerdeki servislerin nasıl
adlandırıldığı ve authenticator
adlı servisin aslında
Authenticator
'ın bir örneği olduğu hakkında bilgi sahibi
olmanız gerekir. Tüm bunları yeni iş arkadaşınızın öğrenmesi gerekir.
Gereksiz yere.
Neyse ki, çok daha doğal bir tanımlayıcıya başvurabiliriz: servisin tipi. Sonuçta, bir geliştirici olarak sizi ilgilendiren tek şey budur. Konteynerdeki bir servise hangi rastgele dizenin atandığını bilmenize gerek yok. Bu kodun yazılmasının ve okunmasının çok daha kolay olduğuna inanıyorum:
final class SignInController extends Best\Framework\Controller
{
public function __construct(
private ContainerInterface $container,
) {}
public function action(string $username, string $password): Best\Framework\Response
{
$authenticator = $this->container->get(Authenticator::class);
//...
}
}
Maalesef, henüz alevleri kontrol altına alamadık. Hiçbir şekilde. Daha büyük sorun, konteyneri alçakgönüllülükle bir servis bulucu rolüne indirgemenizdir, ki bu büyük bir anti-desendir. Bu, birinden bir atıştırmalık alabilmesi için ona bütün bir buzdolabını getirmek gibidir – ona sadece o atıştırmalığı getirmek çok daha mantıklıdır.
Yine, bağımlılık enjeksiyonu şeffaflıkla ilgilidir ve bu denetleyici hala bağımlılıkları konusunda şeffaf değildir. Kimlik doğrulayıcıya olan bağımlılık, konteynere olan bağımlılığın arkasında dış dünyadan tamamen gizlenmiştir. Bu, kodun okunmasını zorlaştırır. Veya kullanılmasını. Veya test edilmesini! Birim testinde kimlik doğrulayıcıyı taklit etmek (mocking) artık etrafında bütün bir konteyner oluşturmanızı gerektiriyor.
Ve bu arada, denetleyici hala konteyner tanımına oldukça kötü bir
şekilde bağlıdır. Konteynerde kimlik doğrulayıcı servisi yoksa, kod ancak
action()
metodunda başarısız olur, ki bu oldukça geç bir geri
bildirimdir.
Lezzetli Bir Şey Pişirmek
Adil olmak gerekirse, bu çıkmaza girdiğiniz için kimse sizi suçlayamaz. Sonuçta, sadece akıllı geliştiriciler tarafından tasarlanmış ve kanıtlanmış bir arayüzü takip ediyordunuz. Mesele şu ki, tüm bağımlılık enjeksiyonu konteynerleri tanım gereği aynı zamanda servis buluculardır ve desenin aralarındaki tek ortak arayüz olduğu ortaya çıkıyor. Ancak bu, onları servis bulucu olarak kullanmanız gerektiği anlamına gelmez. Aslında, PSR spesifikasyonunun kendisi buna karşı uyarıyor.
DI konteynerini iyi bir servis olarak şu şekilde kullanabilirsiniz:
final class SignInController extends Best\Framework\Controller
{
public function __construct(
private Authenticator $authenticator,
) {}
public function action(string $username, string $password): Best\Framework\Response
{
$areCredentialsValid = $this->authenticator->authenticate($username, $password);
//...
}
}
Yapıcıda, bağımlılık açıkça, net ve şeffaf bir şekilde bildirilir.
Bağımlılıklar artık sınıfın her yerine dağılmış gizli değildir.
Ayrıca zorunludurlar: konteyner, gerekli Authenticator
'ı
sağlamadan SignInController
örneğini oluşturamaz. Konteynerde
kimlik doğrulayıcı yoksa, yürütme action()
metodunda değil,
erkenden başarısız olur. Bu sınıfı test etmek de çok daha kolay hale
geldi, çünkü herhangi bir konteyner kazanı olmadan yalnızca kimlik
doğrulayıcı servisini taklit etmeniz yeterlidir.
Ve bir küçük ama çok önemli detay daha: servisin tipi hakkındaki
bilgiyi gizlice içeri soktuk. Bunun bir Authenticator
örneği
olduğu gerçeği – daha önce örtük ve IDE'ler, statik analiz araçları
veya hatta konteyner tanımına aşina olmayan bir geliştirici tarafından
bilinmeyen – şimdi terfi ettirilen parametrenin tip ipucuna statik olarak
kazınmıştır.
Geriye kalan tek adım, konteynere denetleyiciyi nasıl oluşturacağını öğretmektir:
final class Container implements ContainerInterface
{
private array $factories = [];
public function __construct(array $parameters)
{
$this->factories[SignInController::class] = fn() => new SignInController($this->get(Authenticator::class));
$this->factories[Authenticator::class] = fn() => new Authenticator($this->get(UserRepository::class));
$this->factories[UserRepository::class] = fn() => new UserRepository($this->get(Connection::class));
$this->factories[Connection::class] = fn() => new Connection($parameters['database']);
}
public function get(string $id): mixed { /* . . . */ }
public function has(string $id): bool { /* . . . */ }
}
Konteynerin dahili olarak hala servis bulucu yaklaşımını kullandığını
fark etmiş olabilirsiniz. Ancak, kapsandığı sürece (kelime oyunu amaçlı)
bu sorun değil. Konteyner dışında get
metodu çağrısının
izin verildiği tek yer, uygulamanın giriş noktası olan
index.php
'dedir; burada konteynerin kendisinin oluşturulması ve
ardından uygulamanın yüklenip çalıştırılması gerekir:
$container = bootstrap();
$application = $container->get(Best\Framework\Application::class);
$application->run();
Gizli Mücevher
Ama burada durmayalım, bu iddiayı daha da ileri götürmeme izin verin:
get
metodu çağrısının izin verildiği tek yer giriş
noktasıdır.
Konteyner kodu sadece kablolamadır, montaj talimatlarıdır. Yürütülebilir kod değildir. Bir bakıma önemli değildir. Evet, uygulama için kritik öneme sahiptir, ancak yalnızca geliştirici açısından. Aslında kullanıcıya doğrudan bir değer getirmez ve bu gerçek göz önünde bulundurularak ele alınmalıdır.
Konteynere tekrar bakın:
final class Container implements ContainerInterface
{
private array $factories = [];
public function __construct(array $parameters)
{
$this->factories[SignInController::class] = fn() => new SignInController($this->get(Authenticator::class));
$this->factories[Authenticator::class] = fn() => new Authenticator($this->get(UserRepository::class));
$this->factories[UserRepository::class] = fn() => new UserRepository($this->get(Connection::class));
$this->factories[Connection::class] = fn() => new Connection($parameters['database']);
}
public function get(string $id): mixed { /* . . . */ }
public function has(string $id): bool { /* . . . */ }
}
Bu yalnızca uygulamanın çok küçük ve basit bir bölümünü kapsar. Uygulama büyüdükçe, konteyneri manuel olarak yazmak inanılmaz derecede sıkıcı hale gelir. Daha önce de söylediğim gibi, konteyner sadece bir montaj kılavuzudur – ancak çok karmaşıktır, birçok sayfası, sayısız çapraz referansı ve küçük puntolarla yazılmış bir sürü uyarısı vardır. Onu IKEA tarzı bir kılavuza dönüştürmek istiyoruz; grafiksel, özlü ve montaj sırasında kırılmaması için ÅUTHENTICATÖR'ü halıya koyarken gülümseyen insanların resimleriyle.
İşte burada Nette Framework devreye giriyor.
Nette Framework'ün DI çözümü, YAML'ye benzeyen ancak steroidler üzerinde olan bir yapılandırma dosyası formatı olan Neon'u kullanır. Aynı konteyneri Neon yapılandırması kullanarak şu şekilde tanımlarsınız:
services:
- SignInController
- Authenticator
- UserRepository
- Connection(%database%)
İki dikkate değer şeye dikkat çekeyim: birincisi, servis listesi
gerçekten bir listedir, bir hash haritası değil – anahtar yok, yapay
servis tanımlayıcıları yok. authenticator
yok,
Authenticator::class
bile yok. İkincisi, veritabanı bağlantı
parametreleri dışında hiçbir yerde açıkça herhangi bir bağımlılık
belirtmeniz gerekmez.
Bunun nedeni, Nette Framework'ün otomatik kablolamaya (autowiring)
dayanmasıdır. Bağımlılık enjeksiyonu sayesinde bağımlılığın tipini
yerel bir tip ipucuyla nasıl ifade edebildiğimizi hatırlıyor musunuz? DI
konteyneri bu bilgiyi kullanır, böylece bir Authenticator
örneği istediğinizde, herhangi bir ismi tamamen atlar ve doğru servisi
yalnızca tipine göre bulur.
Otomatik kablolamanın benzersiz bir özellik olmadığını iddia edebilirsiniz. Ve haklı olurdunuz. Nette Framework konteynerini benzersiz kılan şey, PHP tip sistemini kullanmasıdır, oysa diğer birçok framework'te otomatik kablolama hala dahili olarak servis adlarına dayanır. Diğer konteynerlerin yetersiz kaldığı senaryolar vardır. Symfony DI konteynerinde kimlik doğrulayıcı servisini YAML kullanarak şu şekilde tanımlarsınız:
services:
Authenticator: ~
services
bölümünde bir hash haritası vardır ve
Authenticator
biti servis tanımlayıcısıdır. Tilda, YAML'de
null
anlamına gelir, ki Symfony bunu “servis
tanımlayıcısını tipi olarak kullan” şeklinde yorumlar.
Ancak yakında iş gereksinimleri değişir ve yerel veritabanı aramasına
ek olarak LDAP aracılığıyla kimlik doğrulamayı da desteklemeniz gerekir.
İlk adımda, Authenticator
sınıfını bir arayüze
dönüştürür ve orijinal uygulamayı LocalAuthenticator
sınıfına çıkarırsınız:
services:
LocalAuthenticator: ~
Aniden Symfony çaresiz kalır. Bunun nedeni, Symfony'nin tipler yerine
servis adlarıyla çalışmasıdır. Denetleyici hala soyutlamaya doğru bir
şekilde güvenir ve Authenticator
arayüzünü bağımlılığı
olarak listeler, ancak konteynerde Authenticator
adında bir
servis yoktur. Symfony'ye bir ipucu vermeniz gerekir, örneğin bir servis
adı takma adıyla:
services:
LocalAuthenticator: ~
Authenticator: '@LocalAuthenticator'
Nette Framework ise servis adlarına veya ipuçlarına ihtiyaç duymaz. Sizi,
kodda zaten ifade edilmiş olan bilgileri ( implements
yan tümcesi
aracılığıyla) yapılandırmada kopyalamaya zorlamaz. Doğrudan PHP tip
sisteminin üzerine yerleştirilmiştir. LocalAuthenticator
'ın
Authenticator
tipinde olduğunu bilir ve eğer bu arayüzü
uygulayan tek servis buysa, bu arayüzün istendiği yerde, yalnızca şu
yapılandırma satırına dayanarak memnuniyetle otomatik olarak bağlar:
services:
- LocalAuthenticator
Otomatik kablolamaya aşina değilseniz, biraz sihirli görünebileceğini ve ona güvenmeyi öğrenmek için biraz zamana ihtiyacınız olabileceğini kabul ediyorum. Neyse ki, şeffaf ve deterministik bir şekilde çalışır: konteyner bağımlılıkları kesin olarak çözemediğinde, durumu düzeltmenize yardımcı olacak bir derleme zamanı istisnası fırlatır. Bu şekilde, iki farklı uygulamanız olabilir ve yine de hangisinin nerede kullanıldığı üzerinde iyi bir kontrole sahip olabilirsiniz.
Genel olarak, otomatik kablolama bir geliştirici olarak size daha az bilişsel yük bindirir. Sonuçta, yalnızca tipler ve soyutlamalarla ilgilenirsiniz, öyleyse DI konteyneri sizi neden uygulamalar ve servis tanımlayıcılarıyla da ilgilenmeye zorlasın? Daha da önemlisi, neden herhangi bir konteynerle ilgilenesiniz ki? Bağımlılık enjeksiyonu ruhuyla, sadece bağımlılıkları bildirebilmek ve bunları sağlamanın başkasının sorunu olmasını istersiniz. Tamamen uygulama koduna odaklanmak ve kablolamayı unutmak istersiniz. Ve Nette Framework DI bunu yapmanızı sağlar.
Benim gözümde, bu Nette Framework'ün DI çözümünü PHP dünyasında var olan en iyisi yapar. Size güvenilir ve iyi mimari desenleri zorlayan bir konteyner sunar, ancak aynı zamanda yapılandırması ve bakımı o kadar kolaydır ki, hakkında hiç düşünmeniz gerekmez.
Umarım bu gönderi merakınızı uyandırmayı başarmıştır. Github depo'suna ve dokümantasyon'a göz atmayı unutmayın – umarım size sadece buzdağının görünen kısmını gösterdiğimi ve tüm paketin çok daha güçlü olduğunu keşfedeceksiniz.
Yorum göndermek için lütfen giriş yapın