Programlamanın en temel prensibi, kendini tekrar etmemektir, prosedür/fonksiyon, yani altprogramlara zaten bu yüzden subroutine denmiştir, yani rutin tekrar tekrar yapılan işlemler...
Önceden yazılmış bir kodu "copy-paste" etmemeli, "cut-paste" ederek bir alt rutine taşımalı veya nesneye yönelik programlama tekniğinde (OOP) bir üst sınıfa taşımalıyız.
Böylelikle kodu merkezi şekilde yeniden kullanılır hale getirebiliriz.
Bu prensibin prosedürel dillerde uygulama alanı, genel altrutinler yani kütüphane fonksiyonları tasarlamak ile sınırlıdır, ancak OOPde sınıfların üst sınıflardan türemesi
ve türeyen sınıflarda fonksiyonların üst sınıftakini ezebilmesi ve türeyen sınıfın üst sınıf yerine kullanılabilmesi anlamında varolan polimorfizm (çok biçimlilik) özelliği
nedeniyle nesneye yönelik programlama dillerinde daha etkin olarak uygulanabilir. Bundan sonraki kısımların anlaşılabilmesi için OOP temellerinin, yani polimorfizmin bilinmesi gerekiyor,
ki burada anlatmayacağım.
Şimdi ise nesneye yönelik tasarımın ilk ve en önemli prensibini ele alalım.
OCP - Open Closed Principle (Açık Kapalı Prensibi)
Bir sınıf genişlemeye açık ama değişikliğe kapalı olmalıdır.
Çünkü bir sınıfta değişiklik yapmak, buna bağlı bulunan (bundan türeyen veya bunu kullanan) diğer tüm sınıfları da buna uygun hale getirmeyi veya en azından yeniden derlemeyi, versiyon atlatmayı ve müşteriye yeniden yüklemeyi gerektirir.
if-else/case-switch karar yapısı bu ilkeye terstir, çünkü bu yapıya yeni bir case (hal) eklenmesi gerektiğinde sınıftaki kodu değiştirmek gerekir. Bu yapı yerine interface veya soyut sınıf ve bunu çeşitli caseler için implemente eden somut sınıflar kullanılırsa, varolan sınıflar değişmeden sadece yeni bir somut sınıf eklenerek istenen genişleme yapılabilir.
Örneğin aşağıdaki yapıyı ele alırsak:
class Servis
{
void IslemYap(int p)
{
if(p == 1)
Console.WriteLine("1. hal için birtakım işlemler...");
else if(p == 2)
Console.WriteLine("2. hal için birtakım işlemler...");
}
}
class Client
{
static void Main(string[] args)
{
Servis s = new Servis();
s.IslemYap(2);
}
}
|
Yukarıdaki yapı yerine aşağıdaki şu yapı kullanılırsa:
abstract class Servis
{
abstract void IslemYap()
{
}
}
class Servis1 : Servis
{
void IslemYap()
{
Console.WriteLine("1. hal için birtakım işlemler...");
}
}
class Servis2 : Servis
{
void IslemYap()
{
Console.WriteLine("2. hal için birtakım işlemler...");
}
}
class Client
{
static void Main(string[] args)
{
Servis s = new Servis2();
s.IslemYap();
}
}
|
Bu şekilde, Servis kütüphanesine p==3 hali, herhangi bir sınıfta değişiklik yapmadan kolaylıkla yeni bir somut sınıf eklenerek şu şekilde gerçekleştirilebilir:
class Servis3 : Servis
{
void IslemYap()
{
Console.WriteLine("3. hal için birtakım işlemler...");
}
}
|
Böylelikle Servis sınıf kütüphanemize dokunmadan, production (üretim) da denen, müşteri ortamına sadece yeni Servis3 sınıfının yüklenmesi yeterlidir. Bu aynı zamanda, pratikte programların bakımını ve yeni ilaveleri çok zorlaştıran içiçe birçok if else yapılarından da bizi kurtararak programları okunur ve rahat takip edilebilir hale getirir ve programların bug oranını, bakım maliyetini düşürerek müşteriye daha hızlı destek verilmesini de sağlar. Pratikte bütün if else switch yapılarının bu şekle getirilmesi de maliyetli bir iş olmakla beraber, en azından çok kullanılan bu yüzden çok az değişiklik yapılması gereken altyapı ve ana kütüphane fonksiyonlarında bu prensibin sıkı sıkıya uygulanması çok faydalıdır. Zaten bu bir tasarım prensibidir, iş bittikten sonra sağlıksız bir kodu dönüştürmek de genellikle kodu sıfırdan yazmak kadar maliyetlidir. Bu prensip, farklı şekillerde ileride bahsedeceğim design patternlarda da (tasarım desenleri veya tasarım kalıpları) tekrar tekrar karşımıza çıkacak.
Şimdi de nesneye yönelik tasarımın en önemli ikinci prensibini ele alalım.
DIP - Dependency Inversion Principle (Bağımlılığı Tersine Çevirme Prensibi)
Aslında DIP (Dependency Inversion Principle yani Bağımlılığı Tersine Çevirme Prensibi), OCPnin
(Açık Kapalı Prensibinin) direk bir sonucudur. DIP prensibine göre, ileride değişmesi muhtemel somut sınıflara direkt erişmek yerine, bu sınıflara arayüz veya soyut sınıflar üzerinden erişmemiz
gerekir. Bu yolla somut sınıflara ve bunların eriştiği tüm diğer somut sınıflara olan bağımlılığı yokedebiliriz. DIP prensibine aykırı yapılar, birbirine bağımlı sınıflar doğurur, ki bu da temelde OCP, yani değişmeden genişleyebilme prensibine
terstir. Çünkü bir sınıfta yapılacak olan değişiklik, diğer tüm sınıflarda değişiklik yapmayı gerektirebilir. Zaten OCPyi anlatırken geliştirdiğimiz örneğin son hali de DIPa uygundu, çünkü hatırlarsanız orada geliştirdiğimiz Servis1,
Servis2, ... sınıflarına bir soyut sınıf (arayüz de olabilirdi) aracılığıyla erişerek ServisN sınıflarına bağımlılığı yokederek varolan kodun değişmeden genişleyebilmesini sağlamıştık. Şimdi DIPın isminin açılımında da yeralan, bağımlılıkları tersine çevirmekten tam olarak ne kastedildiğini anlayabilmek için, önceki OCP örneğimizden daha kapsamlı bir örnek verelim.
Örneğin, A sınıfının B1 ve B2 sınıflarına, B1 sınıfının C1, C2 sınıflarına, B2 sınıfının C1, C3 ve C4 sınıflarına erişim yaptığı (yani bu sınıfları kullandığı) aşağıdaki örneği ele alalım:
class A
{
void IslemYap()
{
Console.WriteLine("A sınıfı için birtakım işlemler...");
}
void BaskaIslemYap(B1 b1, B2
b2)
{
Console.WriteLine("A sınıfı için
başka birtakım işlemler...");
b1.IslemYap();
b2.IslemYap();
}
}
class B1
{
void IslemYap()
{
Console.WriteLine("B1 sınıfı için birtakım işlemler...");
}
void BaskaIslemYap(C1 c1,
C2 c2)
{
Console.WriteLine("B1 sınıfı için
başka birtakım işlemler...");
c1.IslemYap();
c2.IslemYap();
}
}
class B2
{
void IslemYap()
{
Console.WriteLine("B2 sınıfı için birtakım işlemler...");
}
void BaskaIslemYap(C1 c1,
C3 c3, C4 c4)
{
Console.WriteLine("B2 sınıfı için
başka birtakım işlemler...");
c1.IslemYap();
c3.IslemYap();
c4.IslemYap();
}
}
class C1
{
void IslemYap()
{
Console.WriteLine("C1 sınıfı için birtakım işlemler...");
}
}
class C2
{
void IslemYap()
{
Console.WriteLine("C2 sınıfı için birtakım işlemler...");
}
}
class C3
{
void IslemYap()
{
Console.WriteLine("C3 sınıfı için birtakım işlemler...");
}
}
class C4
{
void IslemYap()
{
Console.WriteLine("C4 sınıfı için birtakım işlemler...");
}
}
|
Bu örnekteki sınıfların birbirine erişimini ve buna bağlı olarak ortaya çıkan bağımlılık zincirini kısaca şöyle gösterebiliriz:
A->B1->C1
->C2
->B2->C1
->C3
->C4
Bu gösterimde -> işareti okun gittiği yöne olan bağımlılığı gösterir, yani A sınıfı, B1, B2ye, B1 sınıfı C1 ve C2ye, B2 sınıfı C1, C3 ve C4e, yani dolaylı yoldan A sınıfı malesef B1, B2, C1, C2, C3, C4 sınıflarının hepsine bağımlı haldedir. Bu sınıflardan herhangi birinde yapılacak bir değişiklik direkt olarak Ayı etkiler ve muhtemelen A sınıfının değiştirilmesini veya yeniden derlenmesini gerektirir. Görüldüğü gibi, A sınıfının bağımlılıkları bir sarmaşığın kolları gibi, erişebildiği tüm sistemi kaplıyor.
Bu durum, prosedürel dillerde prosedür/fonksiyonların birbirlerini çağırdığı ve en tepedeki prosedür/fonksiyonun(muhtemelen main() fonksiyonunun), daha aşağı seviyede çağırılan tüm diğer prosedür/fonksiyonlara bağımlı hale geldiği durum ile çok benzerdir. Peki bu bağımlılık zinciri (bağımlılık ağacı de denebilir) ve burada herhangi bir sınıfın tetikleyeceği bir zincirleme kazayla nasıl başa çıkabiliriz? Bunun cevabı, erişen sınıf ile erişilen sınıf arasına soyut sınıf/arayüz yerleştirerek, somut sınıflar üzerine varolan bağımlılığın, soyut sınıf/arayüzlere dönüştürülmesidir.
Yani, önceki örneğimizdeki şu bağımlılıklar:
A->B1->C1
->C2
->B2->C1
->C3
->C4
Şuna dönüşür:
A->IB1<-B1->IC1<-C1
->IC2<-C2
->IB2<-B2->IC1<-C1
->IC3<-C3
->IC4<-C4
Bu yeni halinde A sınıfı sadece IB1 ve IB2 arayüzlerine bağımlıdır, yani tüm B1, B2, C1, C2, C3, C4 sınıflarına olan bağımlılığı ortadan kalkmıştır. Yeni durumda B1 sınıfıda yalnızca implemente ettiği IB1 arayüzüne ve erişim yaptığı IC1 ve IC2 arayüzlerine bağımlıdır. Gördüğümüz gibi tepeden aşağıya tüm bağımlılık tersyüz edilmiş, her somut sınıf sadece onu çevreleyen arayüzlere bağımlı hale gelmiştir, ve arayüzler değişmediği sürece (ki arayüzlerin değişmesine genelde ihtiyaç da olmaz, değişikliğe implementasyonlarda ihtiyaç duyulur) bu bağımlılıkların sisteme hiçbir etkisi yoktur.
Yeni haliyle kodumuzu yazacak olursak:
class A
{
void IslemYap()
{
Console.WriteLine("A sınıfı için birtakım işlemler...");
}
void BaskaIslemYap(IB1 b1,
IB2 b2)
{
Console.WriteLine("A sınıfı için
başka birtakım işlemler...");
b1.IslemYap();
b2.IslemYap();
}
}
interface IB1
{
void IslemYap();
void BaskaIslemYap(IC1
c1, IC2 c2);
}
class B1 : IB1
{
void IslemYap()
{
Console.WriteLine("B1 sınıfı için birtakım işlemler...");
}
void BaskaIslemYap(IC1
c1, IC2 c2)
{
Console.WriteLine("B1 sınıfı için
başka birtakım işlemler...");
c1.IslemYap();
c2.IslemYap();
}
}
interface IB2
{
void IslemYap();
void BaskaIslemYap(IC1
c1, IC3 c3, IC4 c4);
}
class B2 : IB2
{
void IslemYap()
{
Console.WriteLine("B2 sınıfı için birtakım işlemler...");
}
void BaskaIslemYap(IC1
c1, IC3 c3, IC4 c4)
{
Console.WriteLine("B2 sınıfı için
başka birtakım işlemler...");
c1.IslemYap();
c3.IslemYap();
c4.IslemYap();
}
}
interface IC1
{
void IslemYap();
}
class C1 : IC1
{
void IslemYap()
{
Console.WriteLine("C1 sınıfı için birtakım işlemler...");
}
}
interface IC2
{
void IslemYap();
}
class C2 : IC2
{
void IslemYap()
{
Console.WriteLine("C2 sınıfı için birtakım işlemler...");
}
}
interface IC3
{
void IslemYap();
}
class C3 : IC3
{
void IslemYap()
{
Console.WriteLine("C3 sınıfı için birtakım işlemler...");
}
}
interface IC4
{
void IslemYap();
}
class C4 : IC4
{
void IslemYap()
{
Console.WriteLine("C4 sınıfı için birtakım işlemler...");
}
}
|
Aslında A sınıfının da bir yerlerden çağrılacağını düşünürsek bunun için de IA gibi bir arayüz yazılabilir. Örnekte gördüğümüz bütün arayüzler birer yalıtım duvarıdır. Soyut sınıflar ve Arayüzler, OCP prensibinde gördüğümüz gibi, değişmeden genişleyebilir yapılar oldukları
için, sistemin en sabit yapıtaşlarıdır. Bu yüzden, bu tür tasarlanmış sistemlerde soyut sınıf ve arayüzlere olan bağımlılıklar zararsızdır, yani üst yapıyı etkilemez.
Adapter ve Factory Method Tasarım Kalıpları ile Bağımlılığı Yoketmek
Yukarıdaki son örnekte gördüğümüz B1 ve B2 sınıfları, aslında bir anlamda ICn arayüzlerini IBn arayüzlerine çeviren birer adaptör gibi görülebilir. C1, C2, C3, C4 sınıflarının ve IC1, IC2, IC3, IC4 arayüzlerinin bizim kontrolümüz dışında bir kütüphane olduğunu ve arasıra tasarımının değiştiğini düşünelim. Bu kötü bir kütüphane tasarımı veya zorunluluk gereği, örneğin yeni ve gelişimini tamamlamamış bir teknoloji veya bir donanıma bağımlılık nedeniyle olabilir. Zaman içinde değişkenlik gösteren bu C katmanını bizim altyapımıza uyarlamak için B katmanındaki sınıflar gibi sınıflara ihtiyacımız olur, bu kullanım sık sık karşımıza çıktığı için buna literatürde adapter deseninin nesne biçimi (adapter pattern - object form) denmiştir. Adapter deseni iki biçimde karşımıza çıkar, bu örnekteki nesne biçiminin dışında, bir de sınıf biçimi vardır. Adına nesne biçimi denen kullanımda, B sınıfı IB arayüzüzünü implemente eder ve C sınıfını (veya IC arayüzünü veya soyut sınıfını) kullanır. Sınıf biçimindeki kullanımda ise, B sınıfı hem IB arayüzüzünü hem de IC arayüzünü implemente ederek IC arayüzünü
veya soyut sınıfını kullanır.
Önemli bir ikinci nokta da, dikkat ederseniz örneğimizdeki A, B, C sınıflarımızda hiç bir sınıftan nesne yaratmadık, nesneler metodlarımıza parametre olarak geçildi. Eninde sonunda bu altyapıyı kullanacak bir sınıfta, parametre olarak aktarılacak olan bu sınıflardan nesenelerin yaratılması gerekiyor, ancak nesneleri new operatörüyle yaratılırsak, yarattığımız somut sınıfa bağımlılık ortaya çıkar.
Yani, yazımızın ilk başlarında OCP için verdiğimiz örneğe benzeyen aşağıdaki
örneği incelersek:
interface IServis
{
void IslemYap();
}
class Servis1 : IServis
{
void IslemYap()
{
Console.WriteLine("Servis1 için birtakım işlemler...");
}
}
class Servis2 : IServis
{
void IslemYap()
{
Console.WriteLine("Servis2 için birtakım işlemler...");
}
}
class Client
{
static void Main(string[] args)
{
IServis s = new Servis2();
s.IslemYap();
}
}
|
Bu örnekte Client sınıfımız ilk satırda new işlemiyle Servis2 somut sınıfına (ve tabii IServis arayüzüne) bağımlı hale geliyor. Halbuki biz sadece IServis arayüzüne bağımlı kalmak istiyoruz. İşte bu devrede creational patterns (yaratım desenleri) devreye girer. Bu desenler
Singleton, Factory Method, Abstract Factory ve Builder desenleridir. Bunlardan Singleton şu anki konumuzun dışında olduğu için ona şu an değinmeyeceğim. Nesnenin sınıfına bağımlı kalmadan nesneyi yaratmak için kullanabileceğimiz desenlerden ilki,
Factory Method patterndir (fabrika metodu deseni). Bu desen çok basit olarak, nesneyi üretme işini başka bir fonksiyona veya sınıfa havale etmektir.
Örneğin yukarıdaki kodu şöyle yazalım:
interface IServis
{
void IslemYap();
}
class Servis1 : IServis
{
void IslemYap()
{
Console.WriteLine("Servis1 için birtakım işlemler...");
}
}
class Servis2 : IServis
{
void IslemYap()
{
Console.WriteLine("Servis2 için birtakım işlemler...");
}
}
class ServisFactory
{
IServis Servis1Yarat()
{
return new Servis1();
}
IServis Servis2Yarat()
{
return new Servis2();
}
}
class Client
{
static void Main(string[] args)
{
ServisFactory sf = new ServisFactory();
IServis s = sf.Servis2Yarat();
s.IslemYap();
}
}
|
Aslında bu örnekte sadece bağımlılık zincirinde araya yeni bir halka, yani yeni bir sınıf daha ekledik, gerçekte bağımlılığı yok etmedik. Bağımlılığı tam olarak kaldırmak için tek yapılabilecek şey,
Factory Methodlarda, yani örnekteki Servis1Yarat() ve Servis2Yarat() metodlarında Servis1 ve Servis2 sınıf isimlerini hardcode şeklinde yazmamak, yani nesneyi reflection kullanarak sınıf ismini konfigürasyondan, yani dosya veya veritabanından okuyarak yaratmaktır.
Özellikle, başkalarının kullanımına yönelik sınıf kütüphaneleri tasarlayan veya böyle kütüphaneleri kullanan herkesin kafasında, kullanılan yapılara dair oluşabilecek soru işaretlerinin, bu yazı ile azalacağını umuyor, iyi çalışmalar diliyorum. Daha kapsamlı bilgi edinmek için, http://www.objectmentor.com adresinden Robert Martinin bu konulardaki makalelerine ulaşabilirsiniz.
Makale:
OOP'nin Temel Prensipleri ve Factory Method ile Adaptor Tasarım Desenleri Yazılım Mühendisliği Koray Dakan
|