Hizmetlerin isimlere ihtiyacı yoktur
Nette Framework'ün bağımlılık enjeksiyonu çözümünü seviyorum. Gerçekten seviyorum. Bu yazı, bu tutkuyu paylaşmak ve neden günümüz PHP ekosistemindeki en iyi DI çözümü olduğunu düşündüğümü açıklamak için burada.
(Bu yazı ilk olarak yazarın blogunda yayınlanmıştır).
Yazılım geliştirme sonsuz bir yinelemeli soyutlama sürecidir. Etki alanı modellemesinde gerçek dünyanın uygun soyutlamalarını buluruz. Nesne yönelimli programlamada, sistem içindeki çeşitli aktörler arasındaki sözleşmeleri tanımlamak ve uygulamak 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 bileşimi kullanırız.
Kimlik doğrulama mantığını aşağıdaki denetleyiciden çı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 bilgileri kontrolünün oraya ait olmadığını söyleyebilirsiniz. Hangi kimlik bilgilerinin geçerli olduğunu söylemek denetleyicinin sorumluluğunda değildir – tek sorumluluk ilkesi uyarınca, denetleyicinin değiştirmek için yalnızca tek bir nedeni olmalıdır ve bu neden kimlik doğrulama sürecinde değil, uygulamanın kullanıcı arayüzünde olmalıdır.
Bunun için bariz bir yol izleyelim ve koşulu bir 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 controller'dan bu authenticator'a delege etmek. Kimlik doğrulayıcıyı yaptık denetleyicinin bağımlılığıdır ve denetleyicinin aniden bunu bir yerden alması gerekir:
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 yaratacağım!
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 bir
veritabanı kullanıcı tablosunu sorgulamasını gerektiren daha sağlam bir
kimlik doğrulama uygulanana kadar. Authenticator
aniden kendi
bağımlılığına sahip olur, örneğin bir UserRepository
, bu da
belirli ortamın parametrelerine bağlı olan bir Connection
örneğine bağlıdır. Bu hızla tırmandı!
Örnekleri her yerde elle oluşturmak, bağımlılıkları yönetmenin
sürdürülebilir bir yolu değildir. Bu nedenle, denetleyicinin yalnızca bir
Authenticator
adresine olan bağımlılığını beyan etmesine ve
gerçekte bir örnek sağlamanın başka birinin sorunu olmasına izin veren
bağımlılık enjeksiyon modeline sahibiz. Ve bu başkasına bağımlılık
enjeksiyonu konteyneri denir.
Bağımlılık enjeksiyonu konteyneri uygulamanın yüce mimarıdır – sistem içindeki herhangi bir hizmetin bağımlılıklarını nasıl çözeceğini bilir ve bunları oluşturmaktan sorumludur. DI kapsayıcıları günümüzde o kadar yaygındır ki, hemen hemen her büyük web çerçevesinin kendi kapsayıcı uygulaması vardır ve hatta PHP-DI gibi bağımlılık enjeksiyonuna adanmış bağımsız paketler bile vardır.
Biberleri yakmak
Seçeneklerin çokluğu, sonunda bir grup geliştiriciyi bunları birlikte çalışabilir hale getirmek için bir soyutlama aramaya motive etti. Ortak arayüz zaman içinde geliştirilmiş ve sonunda PHP-FIG'e aşağıdaki biçimde önerilmiştir:
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östermektedir: Ateş gibidirler. İyi bir hizmetkârdırlar ancak kolayca kötü bir efendiye dönüşebilirler. Nasıl kullanacağınızı bildiğiniz sürece son derece faydalıdırlar, ancak yanlış kullanırsanız sizi yakarlar. Aşağıdaki kabı düşünün:
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 { /* . . . */ }
}
Buraya kadar her şey yolunda. Uygulama, belirlediğimiz standartlara göre iyi görünüyor: gerçekten de uygulamadaki her hizmeti nasıl oluşturacağını biliyor ve bağımlılıklarını özyinelemeli olarak çözüyor. Her şey tek bir yerden yönetiliyor ve kapsayıcı parametreleri bile kabul ediyor, böylece veritabanı bağlantısı kolayca yapılandırılabiliyor. Çok güzel!
Ancak şimdi, ContainerInterface
adresinin yalnızca iki
yöntemini görünce, konteyneri bu şekilde kullanmak cazip gelebilir:
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 biberlerinizi yaktınız. Başka bir deyişle, kap kötü usta haline geldi. Neden böyle oldu?
İlk olarak, keyfi bir hizmet tanımlayıcısına güveniyorsunuz:
'authenticator'
. Bağımlılık enjeksiyonu tamamen kişinin
bağımlılıkları konusunda şeffaf olmasıyla ilgilidir ve yapay bir
tanımlayıcı kullanmak bu kavrama doğrudan aykırıdır: kodu sessizce
konteynerin tanımına bağımlı hale getirir. Eğer konteynerdeki hizmeti
yeniden adlandırırsanız, bu referansı bulmanız ve güncellemeniz
gerekir.
Ve daha da kötüsü, bu bağımlılık gizlidir: dışarıdan ilk bakışta,
denetleyici yalnızca bir kapsayıcı soyutlamasına bağlıdır. Ancak bir
geliştirici olarak, siz konteynerde hizmetlerin nasıl
adlandırıldığı ve authenticator
adlı bir hizmetin aslında
Authenticator
'un bir örneği olduğu bilgisine sahip
olmalısınız. Yeni meslektaşınız tüm bunları öğrenmek zorunda.
Gereksiz yere.
Neyse ki, çok daha doğal bir tanımlayıcıya başvurabiliriz: hizmet türü. Sonuçta, bir geliştirici olarak önemsediğiniz tek şey budur. Konteynerdeki hizmetin hangi rastgele dizeyle ilişkilendirildiğini bilmenize gerek yok. Bu kodun hem yazılmasının hem de 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);
//...
}
}
Ne yazık ki, alevleri henüz evcilleştiremedik. Azıcık bile değil. Daha büyük sorun, konteyneri bir hizmet bulucu rolüne indirgemenizdir ki bu da büyük bir anti-paterndir. Bu, birisine tek bir atıştırmalık alabilmesi için tüm buzdolabını getirmek gibi bir şey – ona sadece atıştırmalık vermek çok daha makul.
Yine, bağımlılık enjeksiyonu şeffaflıkla ilgilidir ve bu denetleyici bağımlılıkları konusunda hala şeffaf değildir. Bir 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 da kodun okunmasını zorlaştırıyor. Ya da kullanımı. Ya da test etmeyi! Bir birim testinde kimlik doğrulayıcıyı taklit etmek artık onun etrafında bütün bir kapsayıcı oluşturmanızı gerektiriyor.
Bu arada, kontrolör hala konteynerin tanımına bağlıdır ve bunu oldukça
kötü bir şekilde yapar. Eğer kimlik doğrulayıcı hizmeti konteynerde
mevcut değilse, kod action()
metoduna kadar başarısız olmaz, ki
bu oldukça geç bir geri bildirimdir.
Lezzetli bir şeyler pişirmek
Adil olmak gerekirse, bu çıkmaza girdiğiniz için kimse sizi suçlayamaz. Sonuçta, akıllı geliştiriciler tarafından tasarlanan ve kanıtlanan arayüzü takip ettiniz. Mesele şu ki, tüm bağımlılık enjeksiyon kapsayıcıları da tanım gereği hizmet konumlandırıcılardır ve bu modelin gerçekten de aralarındaki tek ortak arayüz olduğu ortaya çıkmıştır. Ancak bu, onları hizmet bulucu olarak kullanmanız gerektiği anlamına gelmez. Aslında, PSR'nin kendisi bu konuda uyarır.
DI konteynerini bu şekilde iyi bir hizmetkâr olarak 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);
//...
}
}
Denetleyici, bağımlılığı açık, net ve şeffaf bir şekilde yapıcıda
beyan eder. Bağımlılıklar artık sınıfın etrafına dağılmış şekilde
gizli değildir. Ayrıca zorlanırlar: kapsayıcı gerekli
Authenticator
sağlamadan bir SignInController
örneği oluşturamaz. Kapsayıcıda kimlik doğrulayıcı yoksa, yürütme
action()
yönteminde değil, erken başarısız olur. Bu sınıfı
test etmek de çok daha kolay hale geldi, çünkü herhangi bir kapsayıcı
boilerplate'i olmadan yalnızca kimlik doğrulayıcı hizmetini taklit etmeniz
gerekiyor.
Ve son bir küçük ama çok önemli ayrıntı daha var: hizmetin türüyle
ilgili bilgileri gizlice ekledik. Daha önce ima edilen ve IDE, statik analiz
araçları ve hatta konteyner tanımından habersiz bir geliştirici tarafından
bilinmeyen bir Authenticator
örneği olduğu gerçeği, artık
tanıtılan parametrenin tip ipucuna statik olarak işlenmiştir.
Geriye kalan tek adım, konteynere kontrolörün nasıl oluşturulacağı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 hizmet bulucu yaklaşımını kullandığını
fark edebilirsiniz. Ancak içeride olduğu sürece (kelime oyunu
amaçlanmıştır) bu sorun değildir. Konteynerin dışında get
yönteminin çağrılmasının kabul edilebilir olduğu tek yer, konteynerin
kendisini oluşturmanız ve ardından uygulamayı getirip çalıştırmanız
gereken uygulamanın giriş noktası olan index.php
adresidir:
$container = bootstrap();
$application = $container->get(Best\Framework\Application::class);
$application->run();
Gizli mücevher
Ancak burada durmayalım, ifadeyi daha da ileri götürmeme izin verin:
get
yönteminin çağrılmasının kabul edilebilir olduğu
tek yer giriş noktasıdır.
Konteynerin kodu sadece kablo tesisatıdır, montaj talimatlarıdır. Yönetici kodu değildir. Bir bakıma önemli değildir. Evet, uygulama için çok önemli olsa da, bu yalnızca geliştiricinin bakış açısından böyledir. Kullanıcıya doğrudan bir değer katmaz ve bu göz önünde bulundurularak ele alınmalıdır.
Konteynere tekrar bir göz atı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, uygulamanın yalnızca çok küçük ve basit bir bölümünü kapsar. Uygulama büyüdükçe, konteyneri elle yazmak inanılmaz derecede sıkıcı hale gelir. Daha önce de söylediğim gibi, konteyner sadece bir montaj kılavuzu – ancak çok sayıda sayfa, sayısız çapraz referans ve çok sayıda küçük harfli uyarı içeren aşırı karmaşık bir kılavuz. Bunu IKEA tarzı bir kılavuza dönüştürmek istiyoruz; grafik, özlü ve montaj sırasında ÅUTHENTICATÖR'ü kırılmaması için halının üzerine koyarken gülümseyen insanların resimlerini içeren bir kılavuz.
İşte Nette Framework burada devreye giriyor.
Nette Framework'ün DI çözümü, YAML'ye benzer ancak steroidli bir yapılandırma dosyası biçimi olan Neon kullanır. Neon yapılandırmasını kullanarak aynı konteyneri bu şekilde tanımlayabilirsiniz:
services:
- SignInController
- Authenticator
- UserRepository
- Connection(%database%)
İki önemli noktaya dikkat çekmek istiyorum: Birincisi, hizmet listesi
gerçekten bir liste, bir karma harita değil – anahtarlar yok, yapay hizmet
tanımlayıcıları yok. authenticator
yoktur ve
Authenticator::class
da yoktur. İkinci olarak, veritabanı
bağlantı parametreleri dışında hiçbir yerde bağımlılıkları açıkça
listelemeniz gerekmez.
Bunun nedeni Nette Framework'ün otomatik bağlantıya dayanmasıdır.
Bağımlılık enjeksiyonu sayesinde, bağımlılığın türünü yerel bir
typehint ile nasıl ifade edebildiğimizi hatırlıyor musunuz? DI konteyneri bu
bilgiyi kullanır, böylece bir Authenticator
örneğine ihtiyaç
duyduğunuzda, herhangi bir ismi tamamen atlar ve yalnızca türüne göre
doğru hizmeti bulur.
Otomatik kablolamanın benzersiz bir özellik olmadığını iddia edebilirsiniz. Haklı da olursunuz. Nette Framework'ün konteynerini benzersiz kılan şey PHP'nin tip sistemini kullanmasıdır, oysa diğer birçok çerçevede otomatik bağlantı hala dahili olarak hizmet adları üzerine kuruludur. Diğer kapsayıcıların yetersiz kaldığı senaryolar vardır. Symfony'nin DI konteynerinde YAML kullanarak kimlik doğrulayıcı hizmetini bu şekilde tanımlarsınız:
services:
Authenticator: ~
services
bölümü bir hash haritasıdır ve
Authenticator
biti bir hizmet tanımlayıcısıdır. Tilde,
Symfony'nin “hizmet tanımlayıcısını türü olarak kullan” şeklinde
yorumladığı YAML'de null
anlamına gelir.
Ancak kısa süre sonra iş gereksinimleri değişir ve yerel veritabanı
aramasına ek olarak LDAP aracılığıyla kimlik doğrulamayı da desteklemeniz
gerekir. İlk adım olarak, Authenticator
sınıfını bir arayüze
dönüştürür ve orijinal uygulamayı bir LocalAuthenticator
sınıfına çıkarırsınız:
services:
LocalAuthenticator: ~
Aniden Symfony'nin hiçbir şeyden haberi olmaz. Bunun nedeni Symfony'nin
türler yerine hizmet adlarıyla çalışmasıdır. Denetleyici hala doğru bir
şekilde soyutlamaya dayanır ve Authenticator
arayüzünü
bağımlılığı olarak listeler, ancak kapsayıcıda adlı
Authenticator
hizmeti yoktur. Symfony'ye bir ipucu vermeniz
gerekir, örneğin bir hizmet adı takma adı kullanarak:
services:
LocalAuthenticator: ~
Authenticator: '@LocalAuthenticator'
Öte yandan Nette Framework, hizmet adlarına veya ipuçlarına ihtiyaç
duymaz. Kodda zaten ifade edilmiş olan bilgileri ( implements
cümlesi aracılığıyla) yapılandırmada çoğaltmaya zorlamaz. PHP'nin tip
sisteminin tam üstüne oturur. LocalAuthenticator
'un *
Authenticator
türünde * olduğunu bilir ve arayüzü uygulayan
tek hizmet olduğu sürece, sadece bu yapılandırma satırı göz önüne
alındığında, arayüzün talep edildiği yerde onu mutlu bir şekilde
otomatik olarak bağlar:
services:
- LocalAuthenticator
Otomatik bağlantıya aşina değilseniz, biraz sihirli gelebileceğini ve ona güvenmeyi öğrenmek için biraz zamana ihtiyacınız olabileceğini kabul ediyorum. Neyse ki, şeffaf ve determinist bir şekilde çalışıyor: konteyner bağımlılıkları net bir şekilde çözemediğinde, durumu düzeltmenize yardımcı olacak bir derleme zamanı istisnası atıyor. Bu şekilde, iki farklı uygulamaya sahip olabilir ve yine de her birinin nerede kullanıldığını iyi bir şekilde kontrol edebilirsiniz.
Genel olarak, otomatik kablolama bir geliştirici olarak size daha az bilişsel yük bindirir. Sonuçta, yalnızca türler ve soyutlamalarla ilgileniyorsunuz, öyleyse neden bir DI kapsayıcısı sizi uygulamalar ve hizmet tanımlayıcılarıyla da ilgilenmeye zorlasın? Daha da önemlisi, neden ilk etapta bir kapsayıcıyı önemsemeniz gerekiyor? Bağımlılık enjeksiyonunun ruhuna uygun olarak, sadece bağımlılıkları beyan edebilmek ve bunları sağlamak için başka birinin sorunu olmak istersiniz. Tamamen uygulama koduna odaklanmak ve kablolamayı unutmak istersiniz. Ve Nette Framework'ün DI'si bunu yapmanıza olanak tanır.
Benim gözümde bu, Nette Framework'ün DI çözümünü PHP dünyasındaki en iyi çözüm haline getiriyor. Size güvenilir ve iyi mimari kalıpları uygulayan bir kapsayıcı sunuyor, ancak aynı zamanda yapılandırması ve bakımı o kadar kolay ki bu konuda hiç düşünmenize gerek kalmıyor.
Umarım bu yazı merakınızı uyandırmayı başarmıştır. Github repository ve docs 'a göz attığınızdan emin olun – 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 öğrenirsiniz.
Yorum göndermek için lütfen giriş yapın