Inversion of Control Principle

Mehmed Emre AKDİN
SDTR
Published in
10 min readMay 9, 2022

Herkese çok uzun bir aradan sonra yeniden merhaba,

Umarım iyisinizdir. ve her şey yolundadır. Bugünkü yazımda sizlere IoC(Inversion Of Control) prensibinden bahsetmeye çalışacağım. Yazılımla içli dışlı olan hemen hemen herkes IoC kavramı ile karşılaşmıştır. Hatta bir dakika bu kavram ile karşılaştıysanız eğer, şu kavramlarla da karşılaşmış olma ihtimaliniz yüksek; IoC Container, DI(Dependecy Inversion) , DIP(Dependecy Injection Principle)… Evet aslında bu yazımızda tüm bu konulara detaylıca değineceğiz. Ve örneklerimizi de ASP.NET Core 6 üzerinden vereceğiz. Fakat sadece C# biliyorsanız bile IoC , IoC Container, DI(Dependecy Inversion) , DIP(Dependecy Injection Principle) kavramlarını anlayacağınızı temin edebilirim. Nedense ben şimdiden heyecanlıyım peki ya siz? Hazırsanız başlayalım.

IoC , DIP ,DI, IoC Container Tüm Bunların Hepsi Birbirine Benziyor Nedir Bunlar?

Öncelikle aşağıdaki resmi hafızanıza kayıt etmenizi istiyorum.

Kavramlara önem veren biri olduğumdan öncelikle bu kavramların kavramsal olarak nerede bulunduklarını görmenizi istiyorum. Gördüğünüz üzere IoC(Inversion Of Contol) ve DIP(Dependency Inversion Principle) birer design principle(tasarım ilkesi)’dır. Bunlar sadece tasarım prensibi olduklarından soyutturlar ve yapılacak işlemi detaylandırmazlar. Yani nasıl uygulayacağımız size bağlıdır. DI(Dependency Injection) ise görüldüğü üzere bir design pattern(tasarım deseni) ’ dir. Son olarak IoC Container ise bir framework’tür.

Design pattern ve design principle arasındaki farkı S.O.L.I.D Prensipleri yazımda kaleme almıştım. Yazı içinde aramayın diye aşağıya aynısını yapıştırıyorum:

Not: “Design Principle” yani “Tasarım İlkeleri” yazılım tasarlarken izlememiz gereken soyut prensipleri açıklar. Örneğin : “Değişkenlerinizi kapsülleyerek kullanın!”. Bu bir prensipdir ve geneldir. Platform ve yazılım dillerinden bağımsızdır. Programlamanın her adımında takip edebileceğimiz ilkelerdir. Nasıl uygulayacağınız size bağlıdır.

“Design Patterns” yani “Tasarım Desenleri” ise sürekli olarak ortaya çıkan belirli problemlere başkaları tarafından test edilmiş ve onaylanmış çözümler sunar. Tasarım desenleri bellidir. Ve daha spesifiktir. Tek şart tasarım desenini uygulamak için gerekli koşulların ve şartların olmasıdır. Örneğin : “Singleton Desing Pattern : Bir sınıfın aynı anda sadece tek bir örneği olacak şekilde tasarlanmasıdır”. Bu bir tasarım desenidir ve bir problem için üretilmiştir. Nasıl uygulayacağınıza dair adımlar bellidir.

IoC(Inversion Of Control) Nedir?

Yazılımın ana ilkelerinden biri sınıfların birbiriyle karşılıklı olarak minumum bağımlılığa sahip olması gerektiğidir. IoC(Inversion Of Control) sınıfların birbirine minumum düzeyde yani gevşek bir şekilde bağımlı olmasını (loose coupling) amaçlayan bir prensipdir.

Coupling Nedir?

Bir yazılımdaki modüllerin birbirine olan yakınlığının derecesidir. Yani bir bileşende yapılan değişikliğin diğer bileşenlerde veya modüllerde bir değişiklik ihtiyacını zorlama derecesidir. Tight Coupling(Sıkı Bağlantı) herhangi bir bileşendeki değişikliğin diğer bir bileşende değişiklik gerektirdiği zamandır. Bu bağlantı tipi kodun yeniden test edilebilirliğini azaltarak kodun daha kırılgan ve katı hale gelmesine neden olur. Bu da yazdığımız kodun değiştirilmesi zor olduğu anlamına gelir. Çünkü herhangi bir modüldeki değişiklik diğer modüllerde değişiklik yapılması anlamına geldiğinden hata yapma riskini ve maliyeti arttıracaktır. Loose Coupling(Gevşek Bağlantı) ise herhangi bir bileşende yapılan bir değişikliğin diğer bir bileşende değişiklik gerektirmemesi anlamına gelir. Bu da mevcut koda dokunmadan birçok değişikliğin önünü açabildiğinden Loose Coupling(Gevşek Bağlantı) yazılım daha hızlı geliştirilebilir olmakla beraber daha kolay yeni özellik eklenebilir ve daha yenilenebilir bir alt yapı sunar.

Burada “Inversion Of Control” yani kontrolün ters çevrilmesindeki kontrol kelimesinden kasıt ise bir sınıfın ana sorumluluğundan başka sahip olduğu diğer sorumluluklardır. Bunlar bir nesnenin yaşam döngüsünü oluşturma, çağırma ve yok etme gibi sorumluluklardır. IoC(Inversion Of Contol) sayesinde sınıf, ana görevinin haricindeki bu sorumluluklarla ilgilenmez. Aşağıdaki örneğe bir göz atalım:

Bir şirketimiz ve şirket içi personellerin işe alımını , pozisyonlarını , maaşlarını vb. işlemleri yapabildiğimiz bir yazılımımız olsun.

Not: Örneğimizi temel N-katmanlı mimari üzerinden örnek vereceğimiz için kısaca değinelim:

Şekilde görüldüğü üzere basit bir N-katmanlı mimaride presentation layer, business layer’ı, business layer ise veri tabanına erişmek için data access layer’ı kullanır.

Aşağıda “Dapper Micro ORM” kullanarak personeller için basitçe oluşturulmuş “Data Access Layer” ‘ın “PersonsDataAccess” sınıfına hızlıca bir göz atalım.

Evet basit olması açısından sadece personeli id bazlı getirecek “GetPersonAsync(int id)” ve toplu olarak personelleri getirecek “GetAllPersonAsync()” fonksiyonlarını ekledim. Hemen aşağıda ise bu programın “Business Layer” ‘ında yer alan “PersonsBusiness” sınıfına göz atalım:

“PersonsBusiness” sınıfına göz attığımızda gözümüze ilk çarpan detayın constructor içinde “PersonsDataAccess” sınıfına ait bir nesne oluşturulması olmalıdır. Aslında bu “PersonsBusiness” ve “PersonsDataAccess” sınıfının birbirine sıkı sıkıya bağlanmasıdır diyebiliriz. Yani “PersonsBusiness” sınıfı “PersonsDataAccess” sınıfı olmadan görevini tamamlayamaz. Diğer bir deyişle “PersonsBusiness” sınıfı “PersonsDataAccess” sınıfına bağımlıdır. Bu sebepten dolayı, “PersonsDataAccess” sınıfındaki değişiklikler, “PersonsBusiness” sınıfında değişikliklere yol açacaktır. Örneğin, “PersonsDataAccess” sınıfındaki herhangi bir yöntemi kaldırırsak veya yeniden adlandırırsak, buna göre “PersonsBusiness” sınıfını değiştirmemiz gerekir. Veya “PersonsDataAccess” sınıfının bir örneğini kullanan birden çok sınıf olabilir. İlerde “PersonsDataAccess” sınıfının adını değiştirirsek bu sınıfın nesnelerinin oluşturulduğu her yeri bulmamız ve değiştirmemiz gerekecektir. Veya ilerde “Dapper Micro ORM” değil de “EF Core” ile çalışmak istediğimizde kodumuzda birçok yeri değişmemiz gerekecektir. Ek olarak “PersonsBusiness” sınıfı “PersonsDataAccess” sınıfının bir örneğini oluşturduğundan bağımsız olarak test edilmesi de çok zordur.

IoC prensibini adım adım nasıl uygulayacağımıza geçmeden önce aşağıda bu prensibi nasıl uygulayabileceğimize dair bazı design pattern’ lere göz atalım:

Biz bir birine gevşek bağlantılı( loose coupling) olan sınıfları elde edebilmek için ilk aşamada Factory Design Pattern’i ve ardından Dependency Injection Design Pattern’i kullanacağız.

Şimdi adım adım IoC prensibini nasıl uygulayacağımıza kısaca göz atalım:

1-) Factory Design Pattern İle IoC

Öncelikle Factroy Design Pattern nedir bilmeyenler için kısa bir değinelim:

Bir Factory Pattern veya Factory Method Pattern , Creational Design Pattern’lerden biridir. Adından da anlaşılacağı gibi, Factory Method Pattern, nesneler oluşturmak için fabrikalar gibi davranan sınıfları kullanır. Bu sayede, nesne oluşturma işlemini başka bir sınıfa devretmemizi sağlayarak kodumuzun daha sağlam, daha gevşek ve genişletilmesi daha kolay hale gelmesini sağlar. Ek bir örnek vermeden var olan örneğimiz üzerinde gevşek bağlantılı sınıfları elde etmek IoC’nin ilk adımı olarak Factory Design Pattern ’ i kullanalım:

Şimdi “PersonsBusiness” sınıfındaki nesne oluşturma işlemini DataAccessFactory adında ki sınıfa taşıyalım:

Basit fabrika sınıfımızı oluşturduktan sonra “PersonDataAccess” sınıfının nesnesini elde etmek için “DataAccessFactory” sınıfını “PersonsBusiness” sınıfında kullanalım:

Görüldüğü üzere “PersonsBusiness” sınıfı “PersonDataAccess” sınıfının bir nesnesini direk oluşturmak yerine “DataAccessFactory” sınıfındaki CreatePersonsDataAccess() metodunu kullanıyor. Böylece, “PersonsBusiness” sınıfındaki nesne oluşturma kontrolünü “DataAccessFactory” sınıfı kullanarak tersine çeviriyoruz. Bu tamamen gevşek bağlı(loose coupled) tasarım elde etmedeki ilk adım olmakla beraber IoC’nin basit bir uygulamasıdır. Sadece IoC kullanarak tamamen gevşek bağlı(loose coupled) tasarım elde edemeyeceğimiz için IoC ile birlikte Dependency Injection Design Pattern ve Dependency Inversion Principle’ı kullanmamız gerekiyor.

2-) Dependency Inversion Principle İle IoC

SOLID prensiplerinin sonuncusudur. SOLID Presipleri Yazımda detaylı açıklamasını yaptığım için buraya sadece kısa bir alıntısını koyacağım:

Not: Bu prensibe göre üst seviye modüller(üst seviye sınıflar), alt seviyedeki modüllerde(alt seviye sınıflar) yapılan değişikliklerden etkilenmemelidir. Yani buna bağlı olarak üst seviyedeki modüller ile alt seviyedeki modüller arasında sıkı bir ilişki olmamalıdır. Bu prensip adından da anlaşılacağı üzere “Dependency Inversion Principle” bu sıkı ilişkiyi tersine çevirir. Ve bunu yaparken üst ve alt seviyedeki modüller arasındaki bağımlılığı ikiye böler. Böylelikle her iki modülde soyutlamaya bağlanır. Bu bağımlılığı azaltmak için “abstract(soyut)” sınıflar veya “Interface(arayüz)” ‘ ler kullanılabilir.

Yukarıdaki tanımda üst seviyeli sınıflardan kastımız var olan örneğimiz için “PersonsBusiness”, alt seviye sınıf ise “PersonDataAccess” olmaktadır. “PersonsBusiness” somut sınıfının “PersonDataAccess” somut sınıfına bağlı olmaması, bunun yerine her iki somut sınıfında soyutlamaya bağlı olması yani bir “interface” ‘ e veya “abstract class” ‘ a bağlı olması gerekmektedir.

Şimdi yapmamız gereken her iki somut sınıfı da soyutlamaya bağlamak. Biz bunun için “interface” kullanacağız. “PersonDataAccess” sınıfındaki metodları “interface” içine taşıyarak “interface” ‘ in örneği üzerinden ‘polymorphism’ sayesinde “PersonsBusiness” ‘ın bu metodları kullanmasını sağlayacağız. Hemen aşağıya “interface” ‘ i oluşturalım:

Ardından “PersonDataAccess” sınıfında bu “interface” ‘i implement edelim:

Şimdi “DataAccessFactory” sınıfında dönüş değerini somut “PersonsDataAccess” yerine “IPersonsDataAccess” olarak ayarlayalım:

Son olarak “PersonsBusiness” sınıfını düzenleyelim:

Artık “PersonsBusiness” sınıfı direk olarak somut “PersonsDataAccess” sınıfına bağlı değildir, bunun yerine “IPersonsDataAccess” arabiriminin referansını kullanır. Ve bu sayede, “IPersonsDataAccess” ’i uygulayan farklı bir sınıfı da bu referans üzerinden kolayca kullanabiliriz.

Son olarak da “Controller” tarafında “PersonsBusiness” sınıfını kullanarak nasıl işlem yaptığımızı görelim:

Tüm bu yapılanlara rağmen yine de tamamen gevşek bağlı sınıflar elde edememiş bulunmaktayız. Çünkü “PersonsBusiness” sınıfı hala “IPersonsDataAccess” referansına atama yapmak için “DataAccessFactory” sınıfını kullanmakta. Burada da yardımımıza Dependency Injection yetişiyor.

3-) Dependency Injection Design Pattern İle IoC

“Dependency Injection Design Pattern” ’i uygulamak için metod , property ve constructor olmak üzere üç yöntem mevcuttur. Biz referansımızı constructor’a enjekte ederek yolumuza devam edeceğiz. Şimdi interface referansımızı constructor’a parametre olarak enjekte edelim:

Görüldüğü üzere constructor’a interface’imizin referansını enjekte ettik. bu değişiklik kapsamında “PersonsDataAccess” sınıfında herhangi bir değişiklik yapılmasına gerek yoktur. Controller’daki çağırımı ise şu şekilde değişiyoruz:

“PersonsBusiness” sınıfının örneğini oluştururken constructor çağırımında “PersonsDataAccess” ‘ in nesne örneği ile beraber çağırım yapıyoruz. Büyük projelerde birçok bağımlı sınıf olduğundan bu kalıpları uygulamak zaman alıcı olabilir. Bu yüzden bu işlemi bizim için kolaylaştıracak “IoC Container” ‘ları kullanabiliriz.

4-) IoC Container Kullanma

IoC Container (DI Container), otomatik olarak Dependency Injection pattern’ini uygulamak için bir frameworktür. Nesne oluşturmayı ve yaşam sürelerini yöneterek bağımlılıkları enjekte etmemize olanak tanır. IoC Container’ın işlevini aşağıdaki görsel net bir şekilde açıklamaktadır.

Dependency injection kullanılarak enjekte edilecek olan tüm nesneler IoC Container dediğimiz bir sınıfta tutulurlar. Ve ihtiyaç doğrultusunda bu değerler çağırılır. Aşağıda bazı IoC Framework’ler verilmiştir:

Biz örneğimizde .NET Core içerisinde build-in olarak gelen IoC Container’ı kullanacağız. IoC Contanier içerisine kayıt edilecek değerler üç farklı davranışla kayıt edilebilmektedir. Bu konuya ASP.NET Core 5/6 Mıddleware Ve Request Pıpelıne yazımda değindiğimden dolayı aşağıya üç farklı davranışın açıklamasını bırakıyorum.

Not: Şimdi çok kısa bir şekilde aşağıda verilen basit bir hiyerarşi üzerinden olayı anlatalım:

Yukarıdaki hiyerarşide UsersController, UserManager ve AddressManager’a bağımlıdır. Yani UsersController içinde UserManager ve AddressManager referansı bizim için gereklidir. Ve aynı şekilde AddressManager’da UserManager’a bağımlıdır. Bu gerekli referansları constructor aracılığı ile enjekte edildiğini varsayarak:

Scoped IoC:

Her yeni HTTP request için yeni bir nesne oluşturulur. Sadece bir HTTP request’in tüm kapsamı için aynı nesne sağlanır. Örneğin, yukarıda ki hiyerarşide UsersController’a bir istek gönderilirse DI UsersManager bağımlılığını iki kere çözümleyecektir. Fakat UserManager nesnesini sadece bir kere oluşturacak ve aynı nesneyi hem UserController ve hem de AddressManager için döndürecektir. Request aynı olduğu için aynı scoped olarak kabul edilecek ve dolayısıyla tek bir request için bir tane nesne örneği oluşturulacaktır.

Transient IoC:

Her request’in her talebine karşılık bir nesne üretilir. Yine, yukarıda ki hiyerarşide UsersController’a bir istek gönderilirse, UserManager nesnesi hem UserController hem de AddressManager için farklı nesneler olarak oluşturulacaktır.

Singleton IoC:

Her yeni HTTP isteğinde uygulama bazlı tekil nesne kullanılır ve IoC Contanier’dan yapılan tüm taleplerde aynı nesne döndürülür. Örneğin yukarıdaki hiyerarşide UserController’a yapılan tek bir istek neticesinde UserManger nesnesi hem UserController ve hem de AddressManager için aynı olarak döndürelecek. Ve ikinci istek de yine aynı nesne döndürülecektir.

ASP.Net Core uygulamalarında hizmetlerimizi container’a kaydetmek için IServiceCollection içerisindeki extension metodlar kullanılır. Aslında ASP .NET Core ‘daki IoC yapılanması IServiceCollection interface’inden ibarettir diyebiliriz.

Örneğimize build-in IoC Container’ı uygulayarak devam edeceğiz. IoC Container ‘ ı uygulamadan önce “HomeController” içerinde ki “PersonsBusiness” sınıfının bağımlılığından kurtulmak adına “PersonsBusiness” sınıfındaki metodları da interface’e taşıyalım:

“PersonsBusiness” sınıfının son hali aşağıdaki gibidir:

Görüldüğü üzere “PersonsBusiness” sınıfıda “IPersonsBusiness” interface’ini uygulayarak soyutlamaya bağlanmıştır. Ardından “IPersonsDataAccess” “PersonsBusiness” sınıfının constructor’una enjekte edilmiştir. “PersonsDataAccess” sınıfında ise herhangi bir değişiklik yapmıyoruz fakat hepsini bir arada görmek amacıyla aşağıya bırakıyorum:

Şimdi “HomeController” içerisinde aşağıdaki değişiklikleri yapıyoruz:

Görüldüğü üzere “HomeController” ‘ın constructor’una “IPersonsBusiness” enjekte edilerek “PersonsBusiness” sınıfına olan bağımlılığından kurtulması sağlanmıştır.

Şimdi ise “Program.cs” sınıfında bağımlılıklarımızı container’a kayıt ediyoruz. Ben scoped olarak kayıt edeceğim.

Programımızı koşarak “HomeController” ‘ a istek de bulunduğumuzda “HomeController” ’ ın constructor’ına, container’a kayıt ederken belirttiğimiz “PersonsBusiness” sınıfının bir örneği atanacaktır. Aynı şey “PersonsBusiness” sınıfı içinde gerçekleşerek “PersonsBusiness” sınıfının constructor’una ise “IoC.DataAccessLayer.Concrete.Dapper.PersonsDataAccess” sınıfının bir örneği atanacaktır.

Evet… Şimdi IoC’nin bize sağladığı gevşek bağlı sınıfların keyfini sürmek ister misiniz? Sürelim dediğinizi duyar gibiyim. Örneğimizin ilk halinden bahsederken; “İlerde “Dapper Micro ORM” değil de “EF Core” ile çalışmak istediğimizde kodumuzda birçok yeri değişmemiz gerekecektir.” demiştik. Hatırlıyor musunuz? Hatırlamıyorsanız yazının başına bakarak hızlıca tekrar hatırlayalım. Ve şöyle bir senaryo düşünelim. İleride projemiz oldukça büyüdü. Sorgularımızı yazarken her ne kadar optimize yazsakta sorguların çalışma hızının bizi tatmin etmediğini varsayalım. Şuan ki “Data Access” katmanımız Dapper ile çalışmakta. İlerde EF Core’un Dapper’i geride bıraktığını da senoryumuza eklersek. “Data Access” katmanını EF Core’a taşımak için önümüzde hiç bir engel kalmamış olacak. (Aslında İlk örnek EF Core üzerinden olsaydı daha iyi olurdu😂) O zaman ne duruyoruz. “Data Access” katmanımızı EF Core için tekrardan yazalım.

Önce context sınıfımızı oluşturalım:

Şimdi de “PersonsDataAccess” sınıfımızı EF Core için tekrar yazalım:

Yukarıda görüldüğü üzere “PersonsDataAccess” sınıfının constructor’una “DatabaseContext” sınıfı enjekte edilmiştir. Şimdi ise Program.cs sıfında gerekli ayarlamaları yaparak işlemlerimizi sonlandıralım:

Ardından AddDbContext extension metodunu kullanarak “DataBaseContext” sınıfımızı IOC kapsayıcısına kayıt ediyoruz. Ve artık projemizi EF Core’a taşımak için tek yapmamız gereken “IPersonsDataAccess” ınterface’ı için container’a kayıt işlemi gerçekleştirirken Dapper.PersonsDataAccess sınıfı ile kayıt etmek yerine EntityFramework.PersonsDataAccess sınıfı ile kayıt etmek olacaktır.

IoC, IoC Container, DI(Dependecy Inversion) , DIP(Dependecy Injection Principle kavramlarının loose coupled(gevsek bağlı) sınıflar elde etmemizde ne kadar önemli bir rol oynadığını kavramış olduğunuzu düşünerek yeniden bir yazının daha sonuna gelmiş bulunuyorum. Eğer gözümden kaçan yazım hatası veya anlatım bozukluğu olduysa şimdiden kusuruma bakmayın. Yazım umarım faydalı olmuştur. Okuduğunuz için teşekkürler. Bir sonraki yazıda görüşmek üzere!

Yararlandığım Bazı Kaynaklar:

https://www.section.io/engineering-education/inversion-of-control-principle/

--

--