Symfony Bileşenleri ile Gerçek Bağımlılık Enjeksiyonu

Yayınlanan: 2022-03-11

Yüksek performanslı bir PHP çerçevesi olan Symfony2, bileşenlerin DI kapsayıcı için bir bağımlılık enjeksiyon arabirimi sağladığı Dependency Injection Container modelini kullanır. Bu, her bileşenin diğer bağımlılıkları umursamamasını sağlar. 'Kernel' sınıfı, DI kapsayıcısını başlatır ve onu farklı bileşenlere enjekte eder. Ancak bu, DI-konteynerinin Servis Bulucu olarak kullanılabileceği anlamına gelir.

Symfony2 bunun için 'ContainerAware' sınıfına bile sahiptir. Birçoğu, Servis Bulucu'nun Symfony2'de bir anti-kalıp olduğu görüşünde. Şahsen ben katılmıyorum. DI'ye kıyasla daha basit bir kalıptır ve basit projeler için iyidir. Ancak, tek bir projede birleştirilen Servis Bulucu kalıbı ve DI-konteyner kalıbı kesinlikle bir kalıp karşıtıdır.

Symfony Bileşenleri ile Gerçek Bağımlılık Enjeksiyonu

Bu yazıda Service Locator modelini uygulamadan bir Symfony2 uygulaması oluşturmaya çalışacağız. Basit bir kuralı takip edeceğiz: DI-konteyner hakkında sadece DI-container oluşturucusu bilgi sahibi olabilir.

DI-konteyner

Dependency Injection modelinde, DI-container servis bağımlılıklarını tanımlar ve servisler sadece enjeksiyon için bir arayüz verebilir. Dependency Injection hakkında birçok makale var ve muhtemelen hepsini okudunuz. Öyleyse teoriye odaklanmayalım ve sadece temel fikre bir göz atalım. DI 3 tip olabilir:

Symfony'de enjeksiyon yapısı basit konfigürasyon dosyaları kullanılarak tanımlanabilir. Bu 3 enjeksiyon türünün nasıl yapılandırılabileceği aşağıda açıklanmıştır:

 services: my_service: class: MyClass constructor_injection_service: class: SomeClass1 arguments: ["@my_service"] method_injection_service: class: SomeClass2 calls: - [ setProperty, "@my_service" ] property_injection_service: class: SomeClass3 properties: property: "@my_service"

Önyükleme Projesi

Temel uygulama yapımızı oluşturalım. Hazır buradayken, Symfony DI-container bileşenini kuracağız.

 $ mkdir trueDI $ cd trueDI $ composer init $ composer require symfony/dependency-injection $ composer require symfony/config $ composer require symfony/yaml $ mkdir config $ mkdir www $ mkdir src

Composer autoloader'ın src klasöründe kendi sınıflarımızı bulmasını sağlamak için composer.json dosyasına 'autoloader' özelliğini ekleyebiliriz:

 { // ... "autoload": { "psr-4": { "": "src/" } } }

Ve konteyner oluşturucumuzu oluşturalım ve konteyner enjeksiyonlarını yasaklayalım.

 // in src/TrueContainer.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ContainerInterface; class TrueContainer extends ContainerBuilder { public static function buildContainer($rootPath) { $container = new self(); $container->setParameter('app_root', $rootPath); $loader = new YamlFileLoader( $container, new FileLocator($rootPath . '/config') ); $loader->load('services.yml'); $container->compile(); return $container; } public function get( $id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE ) { if (strtolower($id) == 'service_container') { if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior ) { return; } throw new InvalidArgumentException( 'The service definition "service_container" does not exist.' ); } return parent::get($id, $invalidBehavior); } }

Burada Config ve Yaml symfony bileşenlerini kullanıyoruz. Ayrıntıları burada resmi belgelerde bulabilirsiniz. Ayrıca her ihtimale karşı 'app_root' kök yolu parametresini tanımladık. get yöntemi, üst sınıfın varsayılan alma davranışını aşırı yükler ve kapsayıcının "service_container" döndürmesini engeller.

Ardından, uygulama için bir giriş noktasına ihtiyacımız var.

 // in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__));

Bu, http isteklerini işlemek içindir. Konsol komutları, cron görevleri ve daha fazlası için daha fazla giriş noktamız olabilir. Her giriş noktasının belirli hizmetleri alması ve DI-konteyner yapısını bilmesi gerekir. Konteynerden hizmet talep edebileceğimiz tek yer burası. Bu andan itibaren bu uygulamayı yalnızca DI-konteyner yapılandırma dosyalarını kullanarak oluşturmaya çalışacağız.

HttpKernel

HttpKernel (hizmet bulucu sorunu olan çerçeve çekirdeği değil), uygulamanın web bölümü için temel bileşenimiz olacaktır. İşte tipik bir HttpKernel iş akışı:

Yeşil kareler olaylardır.

HttpKernel, İstek ve Yanıt nesneleri için HttpFoundation bileşenini ve olay sistemi için EventDispatcher bileşenini kullanır. Bunları DI kapsayıcı yapılandırma dosyalarıyla başlatmada herhangi bir sorun yoktur. HttpKernel, EventDispatcher, ControllerResolver ve isteğe bağlı olarak RequestStack (alt istekler için) hizmetleriyle başlatılmalıdır.

İşte bunun için konteyner yapılandırması:

 # in config/events.yml services: dispatcher: class: Symfony\Component\EventDispatcher\EventDispatcher
 # in config/kernel.yml services: request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] request_stack: class: Symfony\Component\HttpFoundation\RequestStack resolver: class: Symfony\Component\HttpKernel\Controller\ControllerResolver http_kernel: class: Symfony\Component\HttpKernel\HttpKernel arguments: ["@dispatcher", "@resolver", "@request_stack"]
 #in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' }

Gördüğünüz gibi request servisini oluşturmak için 'factory' özelliğini kullanıyoruz. HttpKernel hizmeti yalnızca Request nesnesini alır ve Response nesnesini döndürür. Ön kontrolörde yapılabilir.

 // in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $HTTPKernel = $container->get('http_kernel'); $request = $container->get('request'); $response = $HTTPKernel->handle($request); $response->send();

Veya yanıt, 'fabrika' özelliği kullanılarak yapılandırmada bir hizmet olarak tanımlanabilir.

 # in config/kernel.yml # ... response: class: Symfony\Component\HttpFoundation\Response factory: [ "@http_kernel", handle] arguments: ["@request"]

Ve sonra onu ön kontrolöre alırız.

 // in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $response = $container->get('response'); $response->send();

Denetleyici çözümleyici hizmeti, denetleyiciyi çözümlemek için İstek hizmetinin özniteliklerinden '_controller' özelliğini alır. Bu nitelikler konteyner yapılandırmasında tanımlanabilir, ancak biraz daha zor görünüyor çünkü basit bir dizi yerine bir ParameterBag nesnesi kullanmamız gerekiyor.

 # in config/kernel.yml # ... request_attributes: class: \Symfony\Component\HttpFoundation\ParameterBag calls: - [ set, [ _controller, \App\Controller\DefaultController::defaultAction ]] request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] properties: attributes: "@request_attributes" # ...

Ve işte defaultAction yöntemine sahip DefaultController sınıfı.

 // in src/App/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; class DefaultController { function defaultAction() { return new Response("Hello cruel world"); } }

Bunların hepsi yerinde olduğunda, çalışan bir uygulamamız olmalıdır.

Bu denetleyici, herhangi bir hizmete erişimi olmadığı için oldukça işe yaramaz. Symfony çerçevesinde, bu sorun bir denetleyiciye bir DI-konteyneri enjekte ederek ve bunu bir servis bulucu olarak kullanarak çözülür. Bunu yapmayacağız. O halde controller'ı bir servis olarak tanımlayalım ve içine request servisini enjekte edelim. İşte yapılandırma:

 # in config/controllers.yml services: controller.default: class: App\Controller\DefaultController arguments: [ "@request"]
 # in config/kernel.yml # ... request_attributes: class: \Symfony\Component\HttpFoundation\ParameterBag calls: - [ set, [ _controller, ["@controller.default", defaultAction ]]] request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] properties: attributes: "@request_attributes" # ...
 #in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' }

Ve denetleyici kodu:

 // in src/App/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class DefaultController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction() { $name = $this->request->get('name'); return new Response("Hello $name"); } }

Artık denetleyicinin istek hizmetine erişimi vardır. Gördüğünüz gibi, bu şema döngüsel bağımlılıklara sahiptir. DI-konteyner, oluşturulduktan sonra ve yöntem ve özellik enjeksiyonlarından önce hizmeti paylaştığı için çalışır. Bu nedenle, denetleyici hizmeti oluşturulurken, istek hizmeti zaten mevcuttur.

İşte nasıl çalıştığı:

Ancak bu, yalnızca istek hizmeti önce oluşturulduğu için çalışır. Ön denetleyicide yanıt hizmeti aldığımızda, istek hizmeti ilk başlatılan bağımlılıktır. Önce controller hizmetini almaya çalışırsak döngüsel bir bağımlılık hatasına neden olacaktır. Yöntem veya özellik enjeksiyonları kullanılarak düzeltilebilir.

Ama başka bir sorun var. DI-container, her denetleyiciyi bağımlılıklarla başlatır. Bu nedenle, ihtiyaç duyulmasalar bile mevcut tüm hizmetleri başlatacaktır. Neyse ki, konteyner tembel yükleme işlevine sahiptir. Symfony DI bileşeni, proxy sınıfları için 'ocramius/proxy-manager' kullanır. Aralarına bir köprü kurmamız gerekiyor.

 $ composer require symfony/proxy-manager-bridge

Ve bunu konteyner oluşturma aşamasında tanımlayın:

 // in src/TrueContainer.php //... use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; // ... $container = new self(); $container->setProxyInstantiator(new RuntimeInstantiator()); // ...

Artık tembel servisleri tanımlayabiliriz.

 # in config/controllers.yml services: controller.default: lazy: true class: App\Controller\DefaultController arguments: [ "@request" ]

Bu nedenle denetleyiciler, yalnızca gerçek bir yöntem çağrıldığında bağımlı hizmetlerin başlatılmasına neden olur. Ayrıca, bir denetleyici hizmeti fiili başlatmadan önce paylaşılacağı için döngüsel bağımlılık hatasını da önler; yine de dairesel referanslardan kaçınmamız gerekiyor. Bu durumda request hizmetindeki controller hizmetini veya controller hizmetindeki request hizmetini enjekte etmemeliyiz. Açıkçası, denetleyicilerde bir istek hizmetine ihtiyacımız var, bu yüzden konteyner başlatma aşamasında istek hizmetinde bir enjeksiyondan kaçınalım. HttpKernel'in bu amaçla bir event sistemi vardır.

yönlendirme

Görünüşe göre farklı istekler için farklı denetleyicilere sahip olmak istiyoruz. Yani bir yönlendirme sistemine ihtiyacımız var. Şimdi symfony yönlendirme bileşenini kuralım.

 $ composer require symfony/routing

Yönlendirme bileşeni, yönlendirme yapılandırma dosyalarını kullanabilen Yönlendirici sınıfına sahiptir. Ancak bu konfigürasyonlar, Route sınıfı için yalnızca anahtar/değer parametreleridir. Symfony çerçevesi, 'ContainerAware' arabirimiyle denetleyicilere kapsayıcı enjekte eden FrameworkBundle'dan kendi denetleyici çözümleyicisini kullanır. Bu tam olarak kaçınmaya çalıştığımız şey. HttpKernel denetleyici çözümleyicisi, sınıf nesnesini, denetleyici nesnesi ve eylem yöntemi dizesiyle dizi olarak '_controller' özniteliğinde zaten varmış gibi döndürür (aslında denetleyici çözümleyici, onu yalnızca bir diziymiş gibi döndürür). Bu yüzden her rotayı bir servis olarak tanımlamalı ve içine bir controller enjekte etmeliyiz. Nasıl çalıştığını görmek için başka bir denetleyici hizmeti ekleyelim.

 # in config/controllers.yml # ... controller.page: lazy: true class: App\Controller\PageController arguments: [ "@request"]
 // in src/App/Controller/PageController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class PageController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction($id) { return new Response("Page $id doesn't exist"); } }

HttpKernel bileşeni, 'kernel.request' olayını kullanan RouteListener sınıfına sahiptir. Tembel denetleyicilerle olası bir yapılandırma:

 # in config/routes/default.yml services: route.home: class: Symfony\Component\Routing\Route arguments: path: / defaults: _controller: ["@controller.default", 'defaultAction'] route.page: class: Symfony\Component\Routing\Route arguments: path: /page/{id} defaults: _controller: ["@controller.page", 'defaultAction']
 # in config/routing.yml imports: - { resource: 'routes/default.yml' } services: route.collection: class: Symfony\Component\Routing\RouteCollection calls: - [ add, ["route_home", "@route.home"] ] - [ add, ["route_page", "@route.page"] ] router.request_context: class: Symfony\Component\Routing\RequestContext calls: - [ fromRequest, ["@request"] ] router.matcher: class: Symfony\Component\Routing\Matcher\UrlMatcher arguments: [ "@route.collection", "@router.request_context" ] router.listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener arguments: matcher: "@router.matcher" request_stack: "@request_stack" context: "@router.request_context"
 # in config/events.yml service: dispatcher: class: Symfony\Component\EventDispatcher\EventDispatcher calls: - [ addSubscriber, ["@router.listener"]]
 #in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' }

Ayrıca uygulamamızda bir URL oluşturucuya ihtiyacımız var. İşte burada:

 # in config/routing.yml # ... router.generator: class: Symfony\Component\Routing\Generator\UrlGenerator arguments: routes: "@route.collection" context: "@router.request_context"

URL oluşturucu, denetleyiciye ve işleme hizmetlerine enjekte edilebilir. Şimdi bir temel uygulamamız var. Diğer herhangi bir hizmet, yapılandırma dosyasının belirli denetleyicilere veya olay göndericiye enjekte edilmesiyle aynı şekilde tanımlanabilir. Örneğin, Twig ve Doctrine için bazı konfigürasyonlar burada.

Dal

Twig, Symfony2 çerçevesindeki varsayılan şablon motorudur. Birçok Symfony2 bileşeni, onu herhangi bir adaptör olmadan kullanabilir. Bu nedenle, uygulamamız için bariz bir seçimdir.

 $ composer require twig/twig $ mkdir src/App/View
 # in config/twig.yml services: templating.twig_loader: class: Twig_Loader_Filesystem arguments: [ "%app_root%/src/App/View" ] templating.twig: class: Twig_Environment arguments: [ "@templating.twig_loader" ]

doktrin

Doctrine, Symfony2 çerçevesinde kullanılan bir ORM'dir. Başka herhangi bir ORM kullanabiliriz, ancak Symfony2 bileşenleri zaten birçok Docrine özelliğini kullanabilir.

 $ composer require doctrine/orm $ mkdir src/App/Entity
 # in config/doctrine.yml parameters: doctrine.driver: "pdo_pgsql" doctrine.user: "postgres" doctrine.password: "postgres" doctrine.dbname: "true_di" doctrine.paths: ["%app_root%/src/App/Entity"] doctrine.is_dev: true services: doctrine.config: class: Doctrine\ORM\Configuration factory: [ Doctrine\ORM\Tools\Setup, createAnnotationMetadataConfiguration ] arguments: paths: "%doctrine.paths%" isDevMode: "%doctrine.is_dev%" doctrine.entity_manager: class: Doctrine\ORM\EntityManager factory: [ Doctrine\ORM\EntityManager, create ] arguments: conn: driver: "%doctrine.driver%" user: "%doctrine.user%" password: "%doctrine.password%" dbname: "%doctrine.dbname%" config: "@doctrine.config"
 #in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' } - { resource: 'twig.yml' } - { resource: 'doctrine.yml' }

Ek açıklamalar yerine YML ve XML eşleme yapılandırma dosyalarını da kullanabiliriz. Sadece 'createYAMLMetadataConfiguration' ve 'createXMLMetadataConfiguration' yöntemlerini kullanmamız ve bu yapılandırma dosyalarıyla bir klasöre yol belirlememiz gerekiyor.

Her kontrol cihazına ihtiyaç duyulan her hizmeti ayrı ayrı enjekte etmek hızla çok can sıkıcı hale gelebilir. Bunu biraz daha iyi hale getirmek için DI-container bileşeninin soyut hizmetleri ve hizmet mirası vardır. Böylece bazı soyut kontrolörler tanımlayabiliriz:

 # in config/controllers.yml services: controller.base_web: lazy: true abstract: true class: App\Controller\Base\WebController arguments: request: "@request" templating: "@templating.twig" entityManager: "@doctrine.entity_manager" urlGenerator: "@router.generator" controller.default: class: App\Controller\DefaultController parent: controller.base_web controller.page: class: App\Controller\PageController parent: controller.base_web
 // in src/App/Controller/Base/WebController.php namespace App\Controller\Base; use Symfony\Component\HttpFoundation\Request; use Twig_Environment; use Doctrine\ORM\EntityManager; use Symfony\Component\Routing\Generator\UrlGenerator; abstract class WebController { /** @var Request */ protected $request; /** @var Twig_Environment */ protected $templating; /** @var EntityManager */ protected $entityManager; /** @var UrlGenerator */ protected $urlGenerator; function __construct( Request $request, Twig_Environment $templating, EntityManager $entityManager, UrlGenerator $urlGenerator ) { $this->request = $request; $this->templating = $templating; $this->entityManager = $entityManager; $this->urlGenerator = $urlGenerator; } } // in src/App/Controller/DefaultController // … class DefaultController extend WebController { // ... } // in src/App/Controller/PageController // … class PageController extend WebController { // ... }

Form, Command ve Assets gibi başka birçok yararlı Symfony bileşeni vardır. Bağımsız bileşenler olarak geliştirildiler, bu nedenle DI-konteyner kullanarak entegrasyonları bir sorun olmamalı.

Etiketler

DI-konteyner ayrıca bir etiket sistemine sahiptir. Etiketler Compiler Pass sınıfları tarafından işlenebilir. Event Dispatcher bileşeni, olay dinleyici aboneliğini basitleştirmek için kendi Derleyici Geçişine sahiptir, ancak EventDispatcher sınıfı yerine ContainerAwareEventDispatcher sınıfını kullanır. O yüzden kullanamayız. Ancak olaylar, rotalar, güvenlik ve başka herhangi bir amaç için kendi derleyici geçişlerimizi uygulayabiliriz.

Örneğin, yönlendirme sistemi için etiketler uygulayalım. Şimdi bir rota tanımlamak için config/routes klasöründeki bir rota yapılandırma dosyasında bir rota hizmeti tanımlamamız ve ardından bunu config/routing.yml dosyasındaki rota toplama hizmetine eklememiz gerekiyor. Tutarsız görünüyor çünkü yönlendirici parametrelerini bir yerde ve bir yönlendirici adını başka bir yerde tanımlıyoruz.

Tag sistemi ile bir tag içerisinde sadece bir rota adı tanımlayabilir ve bu rota servisini bir etiket ismi kullanarak rota koleksiyonuna ekleyebiliriz.

DI kapsayıcı bileşeni, fiili başlatmadan önce kap yapılandırmasında herhangi bir değişiklik yapmak için derleyici geçiş sınıflarını kullanır. Öyleyse yönlendirici etiket sistemi için derleyici geçiş sınıfımızı uygulayalım.

 // in src/CompilerPass/RouterTagCompilerPass.php namespace CompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class RouterTagCompilerPass implements CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { $routeTags = $container->findTaggedServiceIds('route'); $collectionTags = $container->findTaggedServiceIds('route_collection'); /** @var Definition[] $routeCollections */ $routeCollections = array(); foreach ($collectionTags as $serviceName => $tagData) $routeCollections[] = $container->getDefinition($serviceName); foreach ($routeTags as $routeServiceName => $tagData) { $routeNames = array(); foreach ($tagData as $tag) if (isset($tag['route_name'])) $routeNames[] = $tag['route_name']; if (!$routeNames) continue; $routeReference = new Reference($routeServiceName); foreach ($routeCollections as $collection) foreach ($routeNames as $name) $collection->addMethodCall('add', array($name, $routeReference)); } } }
 // in src/TrueContainer.php //... use CompilerPass\RouterTagCompilerPass; // ... $container = new self(); $container->addCompilerPass(new RouterTagCompilerPass()); // ...

Şimdi konfigürasyonumuzu değiştirebiliriz:

 # in config/routing.yml # … route.collection: class: Symfony\Component\Routing\RouteCollection tags: - { name: route_collection } # ...
 # in config/routes/default.yml services: route.home: class: Symfony\Component\Routing\Route arguments: path: / defaults: _controller: ["@controller.default", 'defaultAction'] tags: - { name: route, route_name: 'route_home' } route.page: class: Symfony\Component\Routing\Route arguments: path: /page/{id} defaults: _controller: ["@controller.page", 'defaultAction'] tags: - { name: route, route_name: 'route_page' }

Gördüğünüz gibi, rota koleksiyonlarını hizmet adı yerine etiket adına göre alıyoruz, bu nedenle rota etiket sistemimiz gerçek yapılandırmaya bağlı değil. Ayrıca, herhangi bir toplama hizmetine 'add' yöntemiyle rotalar eklenebilir. Derleyici geçişleri, bağımlılıkların yapılandırmalarını önemli ölçüde basitleştirebilir. Ancak DI kapsayıcısına beklenmeyen bir davranış ekleyebilirler, bu nedenle argümanları, yöntem çağrılarını veya sınıf adlarını değiştirmek gibi var olan mantığı değiştirmemek daha iyidir. Etiketleri kullanarak yaptığımız gibi var olanın üzerine yenisini ekleyin.

Sarmak

Artık yalnızca DI kapsayıcı desenini kullanan bir uygulamamız var ve yalnızca DI kapsayıcı yapılandırma dosyaları kullanılarak oluşturulmuş. Gördüğünüz gibi, bu şekilde bir Symfony uygulaması oluşturmanın ciddi bir zorluğu yok. Ve tüm uygulama bağımlılıklarınızı basitçe görselleştirebilirsiniz. İnsanların DI-konteynerini hizmet bulucu olarak kullanmalarının tek nedeni, hizmet bulucu kavramının anlaşılmasının daha kolay olmasıdır. Ve hizmet bulucu olarak kullanılan DI konteynerli büyük bir kod tabanı muhtemelen bu nedenin bir sonucudur.

Bu uygulamanın kaynak kodunu GitHub'da bulabilirsiniz.