Welcome to My Blog 👋

Java, Spring Framework, Microservices, Docker, Kubernetes, AWS and Others 🚀
Follow Me

Günlük hayatta kullandığımız bir çok cihazın nasıl çalıştığı konusunda bilgi sahibi olmayız. Bize sunulan arayüzleri yani komutları kullanarak cihazlarda istediğimiz işlemleri yapabiliriz. Cihaz üreticilerinin yaptıkları güncellemelerden etkilenmeden cihazları kullanmaya devam edebiliriz. Yazılım sistemlerinde de bir nesne başka bir nesneye işlem yaptıracağı zaman o nesnenin nasıl çalıştığını bilmemelidir. Çünkü eğer bir nesne diğer nesnenin nasıl çalıştığını bilirse aralarında sıkı bağ olur ve bu durum koddaki bağımlılığı arttırır. Bağımlılık ise kodda güncelleme yapmayı zorlaştırır. Bu duruma çözüm getiren kalıp Command Patern'ıdır. Bu patern ile karmaşık işler yapan nesneler kendilerini kullanacak nesnelere arayüzler sağlar. İş yaptıracak nesneler bu arayüzler ile işin nasıl yapıldığını bilmeden o işi yaptırabilirler.
Örneğin ATM cihazlarının nasıl çalıştığını bilmeden ekran ve klavyeyi kullanarak makineye istediğimiz işlemleri yaptırabiliriz. Burada klavye, ekran ve diğer girişler bize sunulan arayüzlerdir.

Uygulama
  • Client command arayüzlerini kullanarak işlemlerini yaptırır.
  • Receiver nesnesine (ATM) arayüzler tarafından işlemler yaptırılır.
  • Invoker nesnesi (Klavye) command arayüzlerini barındırır. Kullanıcıya makineyi kullanması için arayüzler sağlar.




public class ATM{

     public void giris() {
         //işlemler
     }
     
     public void sil() {
         //işlemler
     }
     
     public void onayla() {
         //işlemler
     }
}

public interface Command {
    public void execute();
}

public class GirisCommand implements Command {
    private ATM atm;
    public void execute () {
        atm.giris();
    }
} 

public class SilCommand implements Command {
    private ATM atm;
    public void execute () {
        atm.sil();
    }
}

public class OnaylaCommand implements Command {
    private ATM atm;
    public void execute () {
        atm.onayla();
    }
}

public class Klayve {
    private Command command;
    
    public void setCommand (Command command) {
        this.command = command;
    }
    
    public void tusaBas () {
        command.execute();
    }
}

public class Test {
    public static void main() {
        Klavye klavye = new Klavye();
        Atm atm = new ATM();
        Command girisCommand = new GirisCommand(atm);
        Command silCommand = new SilCommand(atm);
        klavye.setCommand(girisCommand);
        klavye.tusaBas();
        klavye.setCommand(silCommand);
        klavye.tusaBas();
    }
}

Chain Of Responsibility patern'ı bir dizi işlemi o işlemin karmaşıklığını bilmeden gevşek bağlı bir şekilde yapmamızı sağlar. Bu şekilde istemcide bir değişiklik yapmadan yapılan işlem değiştirilebilir. Yapılacak işlem birden fazla nesnenin kullanılmasını gerektiriyor ve bu nesnelerin yaptıkları işlemlerin belli bir sırası var ise bu pattern kullanılır. Yani istemcinin tek bir işlem için birden fazla nesneyi bilmesi ve onları bu işlem için sırasıyla çalıştırması yerine bu pattern ile işlem tanımlanır ve nesneler kendi aralarında iletişim kurarak bu işlemi gerçekleştirirler.
Örnek verecek olursak masaüstü programlamada bir pencere içerisinde bir başka penceremizin olduğunu ve onun içinde bir resim olduğunu düşünürsek o resme tıklanma event'ini o resim, içinde bulunduğu pencere ve o pencerenin içinde bulunduğu pencere yakalamak isteyebilir. Bunun için bu event içinde bulunduğu tüm view elemanlarına hiyerarşik bir şekilde gönderilir. Bu işlem sırasında bir zincir şeklinde her view içinde bulunduğu view elemanını bilir.

Uygulama
  • İstekleri karşılayacak ve bir zincir şeklinde işleyecek sınıflar için bir arayüz olan Handler arayüzü tanımlanır. Bu arayüzün istekleri karşılayacak handle metodu yazılır.
  • İstekleri yerine getirecek sınıflar Handler arayüzünü implement ederler.
  • Client nesne Handler arayüzünü kullanarak istekleri yerine getirecek sınıfları kullanır.




public interface Handler {
    public void handleRequest();
}

public class HandlerConcrete1 implements Handler {
    Handler handler = new HandlerConcrete2();
    public void handleRequest() {
        //işlemler
        handler.handleRequest();
    }
}

public class HandlerConcrete2 implements Handler {
    public void handleRequest() {
        //işlemler
    }
}

public class Client {
    public static void main () {
        Handler handler = new HandlerConcrete1();
        handler.handleRequest();
    }
}



Nesneye yönelik programlama tekniğinde yazılımımızı nesneler halinde tasarlarız. Bu nesneler aralarında iletişim halindedirler. Bu iletişim ile bu nesneler arasında bir bağımlılık oluşur. Yazılım sistemlerinde bağımlılık istenmeyen bir durumdur. Çünkü bağımlılık demek yazılım sistemindeki bir değişikliğin yapılmasının zorlaşması demektir. Sistem büyüdükçe bağımlılıklar o kadar artarki değişiklik yapılması imkansız bir hale gelebilir. Bu problemi çözmek için mediator pattern'ı kullanılabilir. Bu pattern ile iki nesne birbirine bağımlı olmadan birbirini bilmeden birbiri ile iletişim kurabilir. Bunu sağlayan yapı, iletişim kuracak iki nesne arasına bir nesne daha koyarak iletişimi bu nesne üzerinden yapar.
Örneğin bir havalimanında pisti kullanacak uçaklar birbirleri ile iletişim kurmazlar. Çünkü böyle bir durumda inanılmaz bir karmaşıklık ortaya çıkar. Tüm uçaklar sadece kuleyle iletişim kurarak pistin kullanımı sekronize edilir.

Uygulama
  • Birbirleri ile iletişim kuracak nesneler mediator interface'ini barındırırlar ve kullanırlar.
  • İletişime özel mediator sınıfları tanımlanır ve mediator interface'i implement edilir.
  • Bir nesne diğer nesneler ile iletişim kurmak istediğinde mediator interface'ini kullanarak özel tanımlanan metotlar üzerinden iletişim kurar.




public class IstanbulKule implements Kule{
    public void ucakEkle (Ucak ucak) {
        ucaklar.add(ucak);
    }
    public void ucakCikar (Ucak ucak) {
        ucaklar.remove(ucak);
    }
    public void haberVer (Ucak ucak, String mesaj) {
        ucak.mesajAl(mesaj);
    }
}

public interface Kule {
    ArrayList<Ucak> ucaklar;
    public void ucakEkle(Ucak ucak);
    public void ucakCikar(Ucak ucak);
    public void haberVer(Ucak ucak, String mesaj);
}

public class UcakModel1 {
    public Kule kule = new IstanbulKule();
    public void haberAl (String mesaj) {
        System.out.println("UcakModel1 Haber alındı." + mesaj);
    }
}

public class UcakModel2 {
    public void haberAl (String mesaj) {
        System.out.println("UcakModel2 Haber alındı. " + mesaj);
    }
}

public interface Ucak () {
    public void haberAl(String mesaj);
}

public class Test {
    public static void main () {
        UcakModel1 ucak1 = new UcakModel1();
        UcakModel2 ucak2 = new UcakModel2();
        ucak1.kule.haberVer(ucak2, "Pisti kullanıyorum");
    }
}



Bu pattern türkçe olarak gözlemci anlamına gelmektedir. İsminden de anlaşılacağı gibi temelde yaptığı için bir başka nesneyi gözlemektir. Daha detaylı anlatacak olursak bir nesnenin verileri bir başka nesneye bağlı olabilir. Bağlı olduğu nesnedeki verilerin değişiminden etkilenebilir. Bu durumda bağlı olduğu nesneyi gözlemlemesi gerekir ve o nesnede değişiklik olduğunda kendini güncellemesi gerekir. Örnek verecek olursak MVC pattern'ında ekranda bulunan view nesneleri model nesnelerine bağlıdırlar. Model nesnelerinde yapılacak bir değişiklikte view nesneleri otomatik olarak güncellenir ve ekrana yansır. 
Bu pattern ile aradaki bağımlılık gevşek bağlıdır. Bağımlılığı sıkı bağlı şekilde yapmak yazılımsal anlamda çok büyük problemlere yol açabilir. Bu yüzden bu pattern'ı kullanarak birbirlerine bağımlı olan nesneleri gevşek bağlı şekilde sekronize edebiliriz.
Java'nın içerisinde bu pattern'a ait hazır bir yapı bulunmaktadır. java.util paketinde bulunan Observer adındaki interface ve Observable adındaki sınıf ile bu pattern kullanılabilir.

Uygulama
  • ConcreteSubject bizim model nesnemizdir. Yani bağımlı olduğumuz nesnedir. Bu nesne üzerinde yapılan bir değişiklikten haberimiz olsun istiyoruz.
  • ConcreteSubject nesnemize implement ettiğimiz Subject interface'i bu nesneyi izlememiz için gerekli metotları bize sağlar. Bu metotlar nesneyi izlemek isteyen nesnelerin kayıt olabilecekleri attach metodu, kayıtlarını kaldırabilecekleri detach metodu ve değişiklik durumunda kayıtlı bulunan nesnelere gönderilecek mesaj için notify metodudur.
  • ConcreteSubject yani izlediğimiz nesne Observer interface'ini kullanarak bir değişikliklik olduğunda bu interface'in update metodunu çağırır. Bu interface update edeceğimiz nesneyi içerisinde barındıran bir arayüz olur dolayısıyla güncellemek istediğimiz nesnede bir update işlemi yapmış oluruz.
  • Eğer izleme olayı çift yönlü olsun istersek yani gözlemci nesnede bir değişiklik olduğunda model nesnesi de güncellensin istersek çift yönlü bağlama yapmamız gerekir.




public class ConcreteSubject implements Subject{
    private ArrayList<Observer> observers;

     public void attach (Observer observer) {
         observer.add(observer);
     }
     
     public void detach (Observer observer) {
         observer.remove(observer);
     }
     
     public void notify () {
         for (Observer observer : observers) {
             observer.update();
         }
     }
}

public interface Subject {
    public void attach ();
    public void detach ();
    public void notify ();
}

public class ConcreteObserver implements Observer () {
    ConcreteSubject concreteSubject;
    
    public void update () {
        //örneğin ekrandaki veriyi güncelleri
    }
}

public interface Observer {
    public void update ();
}

Iterator design pattern'ı nesnelerin içerdikleri koleksiyon (array, list, map, set...) yapılarının türlerinden bağımsız olarak bu koleksiyon yapıları üzerinde işlem yapmamızı sağlar. Örneğin bir şirketin departmanlarına ait nesnelerde o departmanlarda çalışan kişilerin nesneleri tutuluyor olsun. Her departman bu nesneleri farklı koleksiyon yapıları ile tutabilir. Bu pattern ile departmanların uyguladıkları koleksiyon yapılarından bağımsız departmanlarda çalışan kişilere aynı yöntem ile erişebiliriz.
Java dili içerisinde bu pattern uygulanmış bir şekilde bulunur. iterator ve iterable interface'leri bu pattern için yazılmıştır. Ayrıca bu pattern'ın uygulandığı özel for döngüsü de Java dilinde mevcuttur.

Uygulama
  • Bir iterator interface'i yaratılır. Bu interface koleksiyon yapısı üzerinde gezinmek için next ve back metotlarını içermelidir. Ayrıca koleksiyon üzerindeki nesnelere erişmek için bir getItem metodu içermelidir.
  • Uygulayacağımız her koleksiyon yapısı için bir iterator sınıfı yazmalıyız ve iterator interface'ini bu sınıfa implement etmeliyiz. Bu implement ile gelen metotları koleksiyona özel şekilde doldurmalıyız.
  • Bir iterable interface'i yazarak iterator pattern'ını kullanacak olan sınıflara bir arayüz sağlamalıyız. Bu interface'in içine getIterator metodu yazarak bu interface'i uygulayan sınıfların koleksiyonları için uygun iterator sınıfını dönmelerini sağlamalıyız.


public interface Iterable {
     public Iterator getIterator();
}

public interface Iterator {
    public Object getItem();
    public boolean next();
    public boolean back();
}

public IKDepartman implements Iterable {
    private Calisan[] calisanlar;
    public ArrayIterator getIterator () {
        return new ArrayIterator(calisanlar);
    }
}

public class Test {
    public static void main () {
        IKDepartman ikDepartman = new IKDepartman();
        Iterator iterator = ikDepartman.getIterator();
        while(iterator.next) {
            System.out.println(iterator.getItem().toString);
        }
    }
}

public class ArrayIterator implements Iterator {
    private Object[] array;
    private int index = 0;
    public ArrayIterator (Object[] array) {
        this.array = array;
    }
    public Object getItem() {
        return array[index];
    }
    public boolean next () {
        if(index < array.length) {
            return true;
        }
        return false;
    }
    public boolean back () {
        if(index - 1 >= 0 && index - 1 < array.length) {
            return true;
        }
        return false;
    }
}

Bu design pattern, yaratılan nesnelerin üretimini o nesneyi kullanan client'lardan gizlemek amacı taşır. Örneğin günlük hayatta kullandığımız bir çok ürünün nasıl ve nerede üretildiğini bilmeyiz. Ürünün üretim yeri veya üretim şeklinde yapılan değişikliklerden etkilenmeyiz. O ürünleri satın alırız ve kullanırız. Bu pattern ile de nesnenin nasıl ve nerede üretildiği client'lardan gizlenerek nesne üretiminin esnekliği arttırılabilir. Ayrıca ürün grupları ile çalışırken bu design pattern'ı kullanırız çünkü client'ın ürüne bağımlılığı olsun istemeyiz. Client'ların ürünlere bağımlılığının olması daha sonra kodda değişiklik yapmayı zorlaştırır. Bu pattern ile client istediği ürün grubundan istediği nesneyi bağımlı olmadan yaratabilir.

Uygulama
  • Client için bir fabrika interface'i üzerinden nesne üretimi yapılır.
  • Her ürün grubu için bir fabrika sınıfı yazılır ve genel fabrika interface'i implement edilir.
  • Client istediği ürün grubundan nesne yaratırken bu nesnenin nasıl ve nerede üretildiğini bilmez. Böylelikle üretilen ürünler ile doğrudan bir bağımlılığı olmaz.
Aşağıdaki örnekte client bir window nesnesi yaratacağı zaman fabrika interface'i üzerinden doğrudan window nesnesinin türüne bağımlı olmadan istediği window nesnesini yaratabilir. Yeni bir window türü ekleneceği zaman kodda herhangi bir değişiklik yapmaya gerek kalmadan sadece bir fabrika sınıfı ekleyerek kolaylıkla kod değişikliklere uyumlu hale getirilebilir.





Bu patern ile kendi içerisinde alt parçalar barındıran karmaşık nesnelerin yaratılışındaki problemlere çözüm getirilmiştir. Yazılım sistemlerinde alt parçalardan oluşan ve bu parçalara göre değişik biçimler alabilen nesneler var olabilmektedir. Bu nesnelerin yaratılması karmaşık ve problemli bir iştir. Bu patern ile bu durumda karşılaşılabilecek problemlere çözüm getirilmiştir ve bir standart oluşturulmuştur.
Örneğin evimizde kullandığımız bilgisayarlar böyle nesnelere örnek gösterilebilir. Bilgisayarlar ihtiyaç duyulan özelliklere göre farklı alt parçalardan oluşur. Bilgisayarlarımızda ses işiyle uğraşan biri için çok gelişmiş bir ses kartı bulundurabildiği gibi, video işiyle uğraşan biri için gelişmiş bir ekran kartı bulunabilir. Bu örnekte olduğu gibi ihtiyaca göre nesneyi oluşturan yapı builder pattern'nıdır.

Uygulama
  • Bir builder internface'i ile farklı nesneleri build edecek sınıflar için bir arayüz oluşturulur. Bu interface'in bir build metodu olur.
  • Oluşturulacak her nesne için bir build sınıfı oluşturulur ve builder interface'i implement edilir. Bu build sınıfı ile nesne ve onun alt parçaları oluşturulur.
  • Bir director sınıfı oluşturularak builder üzerinden nesne yaratılması yönetilir.


public class Client{

     public static void main(String []args){
        Builder builder1 = new Builder1();
        Director.constructor(builder1);
        Object1 obj1 = builder1.getObject();
        Builder builder2 = new Builder2();
        Director.constructor(builder2);
        Object1 obj2 = builder2.getObject();
     }
}

public class Object1 {
    private ArrayList<String> parts = new ArrayList<>();
    
    public void add (String part) {
        parts.add(part);
    }
}

public class Director {
    public static void constructor (Builder builder) {
        builder.buildPart();
    }
}

public interface Builder {
    public void buildPart();
    public void getObject();
}

public class Builder1 implements Builder {
    private Object1 obj = new Object1();
    
    public void buildPart() {
        obj.add("özellik3");
        obj.add("özellik1");
    }
    
    public void getObject () {
        return obj;
    }

public class Builder2 implements Builder {
    private Object1 obj = new Object1();
    
    public void buildPart() {
        obj.add("özellik1");
        obj.add("özellik2");
    }
    
    public void getObject () {
        return obj;
    }
} 

Object Pool patern'ı prototype patern'ına benzerlik göstermektedir. Prototype patern'ında olduğu gibi yaratılması maliyetli nesnelerin yönetimi konusundaki probleme çözüm getirir. Prototype patern'ından farklı olarak bu patern çok kullanıcıya hizmet veren ağır iş yükü altındaki uygulamalar için kullanışlıdır. Yaratılması maliyetli nesnelerden belli bir sayıda daha önceden yaratılarak bir havuzda tutulur. Daha sonra bu nesneyi kullanmak isteyen istemciler bu sınıf üzerinden nesneye erişim sağlarlar. Daha sonra nesne ile işleri bittiğinde nesne havuza geri koyularak ciddi bir performans artışı sağlanır.
Bu patern'ı uygularken dikkat edilmesi gereken bir konu havuzda tutulacak nesne sayısıdır. Havuzda gereğinden fazla nesne tutmak belleğin kullanımını ciddi şekilde arttıracağı için performans kaybına yol açabilir.

Uygulama
  • Nesneleri havuzda tutacak bir havuz nesnesi yaratılır. Bu nesne tek bir tane olması gerektiği için singleton patern'ini uygulayacak şekilde oluşturulur.
  • Havuz nesnesi üzerine istemciler için nesne referansı alabilecek ve nesne referansını havuza geri bırakabilecekleri metotlar yazılır.



public class ObjectPool {
         
         private const int POOL_SIZE = 10;
         protected static ObjectPool objPool;
         private List<UsedObject> objects;
         
         protected ObjectPool () {
             objects = new ArrayList<>();
         }
         
         public static ObjectPool getPool () {
             if (objPool == null) {
                 objPool = new ObjectPool();
                 for(int i = 0 ; i < POOL_SIZE ; i++) {
                     UsedObject obj = new UsedObject();
                     objects.add(obj);
                 }
             }
             return objPool;
         }
         
         public UsedObject getObject () {
             for(UsedObject obj : objects) {
                 if(!obj.isActive()) {
                     return obj;
                 }
             }
             return null;
         }
         
         public void releaseObject (UseOject obj) {
             obj.setActive(false);
         }
         
         

public class UsedObject{

     private boolean isActive;
     
     public UsedObject () {
         isActive = false;
     }
     
     public void setActive(boolean isActive) {
         this.isActive = isActive;
     }
     
     public boolean isActive () {
         return isActive;
     }
}

public class Client{

     public static void main(String []args){
        // Nesnenin referansına erişilir
        UsedObject obj = ObjectPool.getPool().getObject();
        //Nesne ile ilgili işlemler yapılır
        //...
        //Nesne havuza geri bırakılır
        ObjectPool.getPool().releaseObject(obj);
     }
}

Bu patern'ın ana amacı yaratılması maliyetli nesnelerin tekrar tekrar sıfırdan yaratılması yerine bir prototip nesne üzerinden yaratılmasıdır. Yaratılması maliyetli nesnelerden kasıt örneğin dış bir servise veya veritabanına bağımlı bilgi içeren bir nesnenin her seferinde yaratılması sistem dışına yapılacak çağrıları gerektirir ki bu da maliyetli bir işlemdir. Bu gibi durumlarda bir kere oluşturulmuş prototip nesnesi üzerinden klonlama işlemi ile nesne üretilir ve sistemde gereksiz zaman kaybı önlenir.

Uygulama
  • Patern'ı uygulayacağımız sınıfa Cloneable interface'ini implement edilir.
  • clone metodu override ederek ihtiyacımıza uygun yazılır.
  • Patern'ın uygulandığı sınıfın bir nesnesine ihtiyaç duyulduğu yerlerde clone metodu ile nesne yaratılır.


public class Client{

     public static void main(String []args){
        Database nesne1 = new Database();
        nesne1.setDigerBilgiler("özel bilgi nesne1");
        Database nesne2 = (Database) nesne1.clone();
        nesne2.setDigerBilgiler("özel bilgi nesne2");
     }
     
     public class Database implements java.lang.Cloneable {
         
         private String bilgi;
         private String digerBilgiler;
         
         public Database () {
             bilgi = "genel bilgi";
         }
         
         public Object clone() throws CloneNotSupportedException {
             return new Database();
         }
         
         public setDigerBilgiler (String bilgi) {
            digerBilgiler = bilgi;       
         }
         
     }
}

Bu patern nesne yaratılmasına yönelik bir yöntem sunar. Sistemlerimizdeki bazı nesnelerin bağımlılıklarından dolayı diğer nesneler gibi direkt olarak yaratılması doğru olmamaktadır. Örneğin bir tabloya ait satır ile ilgili işlemleri yapacak olan sınıfın tabloya doğrudan bağımlılığı vardır. Bu yüzden bu satır sınıfınız tablo sınıfı üzerinden yaratılması gerekmektedir. Aksi durumunda yaratılan satır sınıfının tablo bilgisi konusunda problemler ortaya çıkabilir.

Uygulama
  • Sınıfın yapıcı metodu private veya protected yapılır. Tercihen protected olmalıdır.
  • Sınıfın nesnesini yaratacak olan metot bağımlı olan sınıfın bir metodu olarak tanımlanır.


public class Table{
    
    protected Table () {
        
    }

    public Row createRow () {
        return new Row();
    }
}

public class Row{
    
    protected Row() {
        
    }
}






Bazı nesnelerin uygulama yaşam döngüsü içerisinde hep aynı şekilde aynı işi yapması gerekebilir. Bu durumlarda nesnenin birden fazla referansını yaratmak gereksizdir. Bu problemin çözümü Singleton Design Pattern'dır. Singleton ile nesnenin referansı bir kez yaratılır ve bu sınıfı kullanmak isteyen diğer sınıflar her zaman aynı nesneyi kullanırlar.

Uygulama
  • Singleton olacak sınıf içerisinde private static olarak sınıfın referansını tutan bir değişken tanımlanır.
  • Sınıfın yapıcı metodu private veya protected yapılır. Tercihen protected olmalıdır.
  • Yapıcı metot yerine nesneyi kullanacak sınıflara erişim için bir public metot yaratılır. Bu metotta private static olan nesnenin referansı döndürülür. Eğer uygulama içinde daha önce hiç yaratılmamışsa bu metot içerisinde kontrol edilerek yaratılır.

public class Singleton{

     private Singleton singleton;
     
     protected Singleton () {
         
     }
     
     public static Singleton createSingleton () {
         if(singleton == null) {
             singleton = new Singleton();
         }
         return singleton;
     }
}

Anti Pattern'lar aynı Design Pattern'lar gibi zamanla kalıplaşmış yapılardır. Design Pattern'larından farkı bir problemin nasıl olması gerektiğini değil nasıl olmaması gerektiğini anlatmasıdır. Bu yönüyle Design Pattern'larının zıttıdır diyebiliriz. Bazı Anti Pattern örnekleri;

  • Magic Pushbutton : Frontend elemanların kodları arasına iş mantığını sağlayan kodların yazılması ile oluşur.
  • Functional Decompozition : Birbirinden alakasız statik fonksiyonları tek bir sınıfa toplanması ile oluşan bir anti pattern'dır.
  • Database as IPC : Farklı uygulamalar arasındaki iletişimin bir veritabanı üzerinden yapılması ile oluşur.
  • Swiss Army Knife : Kullanılmayan, gereksiz kod parçalarının bulunduğu yazılımlarda görünür.
  • Busy Waiting : Zaman alan işlemleri aynı thread üzerinde yaparak uygulamanın yanıt vermemesi durumunu oluşturan kodlara denir.
  • Soft Coding : İş mantığını konfigürasyon dosyası gibi dosyalarda kodlanmasına denir.
  • Lasagna Coding : Gereğinden fazla katmandan oluşan kodlara denir.
  • Spagetti Coding : Soyutlama gibi tasarım prensipleri uygulamadan yazılmış karmaşık kodlara denir.
  • God Object : Birden fazla amacı olan çok fazla kod bulunan sınıflara denir.
  • Error Hiding : Uygulamada oluşan hataları handle eden ve bu hataları loglamayarak üstünü kapatmaya çalışan yazılımlarda görünür.
  • Copy Paste Programing : Hız kazandırdığı düşüncesiyle yapılan ancak daha sonradan çok fazla soruna yol açabilecek bir tekniktir.

Design Pattern yani dizayn paternleri programlamada sürekli karşılaşılan problemlere en iyi çözümü sağlayan kalıplaşmış yöntemlerdir denilebilir. Bu yüzden programlama dilinden bağımsız yapılardır. Ben yazılarımda dizayn paternlarını anlatırken Java dilini kullanıyor olacağım ancak bu yapılar tüm nesneye yönelik programlarda kullanılabilir.
Dizayn paternlarının amacı programcının sürekli karşılaşılan problemler için çözüm aramamasını sağlamak ve daha önceden yapılan ve yanlış olduğu görülen yöntemler denememesini sağlamaktır. Bu yapılar zamanla kalıplaşmıştır ve en doğru çözüm olduğu konusunda hemfikir olunmuştur.
Dizayn paternları kendi içinde mantıksal olarak ayrılır. Aşağıdan bu paternlar ile ilgili yazdığım yazılara ulaşabilirsiniz.

Creational Pattern'lar (Nesne Yaratılışına Ait Patern'lar)


Structural Pattern'lar (Yapısal Patern'lar)


Behavioral Pattern'lar (Davranışsal Patern'lar)



Yazılım sistemlerinde değişiklik kaçınılmaz bir durumdur. Yazılan kod her zaman değişmek zorunda kalır. Bu yüzden yazdığımız kodu kolay değişebilir şekilde yazmalıyız. Aksi takdirde yazdığımız kod çok kısa bir zaman sonra çöp olacaktır. Bu problemi çözümü nesne yönelimli programlamada polimorfizm'dir. Polimorfizm ile sistemimizde bir değişiklik yapmak zorunda kaldığımızda mevcut kodları değiştirmeden sadece yeni kodlar ekleyerek problemlerimizi çözebiliriz. Bu işlemin amacı daha önceden yazılmış kodun bağımlılıklarından kurtulmaktır. Çünkü bir kodu değiştirmek her ne kadar kolay gözüküyor olursa olsun o kodun sistemimizde bağımlı olduğu yerleri düşündüğümüzde yani etkilediği diğer kod parçalarını düşündüğümüzde işin içinden çıkılamaz bir durum oluşur. Bu yüzden polimorfizm kullanarak sistemlerimizi tasarlamalıyız.

Polimorfizm kısaca, yazılımda uyumluluğu yani geçmişte yazılan kodların, gelecekte yazılacak olan kodları desteklemesini sağlayan bir nesne yönelimli programlama tekniğidir.

Polimorfizm iki şekilde gerçekleştirilebilir.
  • Soyut sınıf ve saf sanal fonksiyonlar kullanılarak.
  • Interface yani arayüzler kullanılarak.

Aşağıdaki iki örnekte de sisteme yeni bir şekil eklendiği zaman Ciz sınıfında veya sistemin herhangi bir yerinde bir kod değişikliği yapmamıza gerek olmaz.

Soyut Sınıf Örneği


public class Ciz{

     public void sekilCiz(Sekil sekil){
         sekil.sekilCiz();
     }
}

public class Sekil {
    
    public void sekilCiz(){
        
    }
}

public class Kare extens Sekil {
    
    public void sekilCiz(){
        
    }
}

public class Daire extens Sekil {
    
    public void sekilCiz() {
        
    }
}

Interface Örneği

public class Ciz{

     public void sekilCiz(Sekil sekil){
         sekil.sekilCiz();
     }
}

public interface Sekil {
    
    public void sekilCiz();
}

public class Kare implements Sekil {
    
    public void sekilCiz(){
        
    }
}

public class Daire implements Sekil {
    
    public void sekilCiz() {
        
    }
}


Bir yazılım birden fazla şekilde yazılabilir ancak her çalışan kod gerçekten bir yazılım sayılmaz. Örneğin inşaat mühendisleri bir binayı tasarlarken sadece binayı yapmayı düşünmezler. Tüm olası durumlarda o binanın ayakta durması için gerekli olan şeyleri düşünüp ona göre binayı inşa etmek zorundadırlar. Yazılım mühendislerininde böyle bir zorunluluğu vardır çünkü her ne kadar kod çalışıyor olsa bile iyi tasarlanmamış bir kodun ömrü çok kısa olacaktır. Bu yüzden yazılımı yazmadan önce o yazımı iyi tasarlamak gerekir. Aksi halde işin içinden çıkamayacak problemlerle karşılaşılır ve kod bir çöpe dönüşür. Bazı yazılım prensipleri;

  • Yazılımda olacak paydaşları doğru bir şekilde çıkararak bunları yazılıma aktarmamız gerekir. Gerçek dünya nesneleri olarak düşünerek bunların yazılımdaki karşılıkları olan sınıfları çıkarmalıyız. Bunu yaparken her bir paydayı ayırmalıyız. Örneğin müşteri, satıcı olan ve hatta bu tiplerin alt türlerinin de bulunduğu bir yazılımda tüm bu paydaşlar için tek bir sınıf yaparsak daha sonraki herhangi bir değişiklikte işin içinden çıkılamaz bir karmaşıklık oluşur. Bunun için buraki tüm paydaşları amacına göre tek tek ayırarak sistemi tasarlamamız gerekir.
  • Bir sınıfı tasarladığımızda o sınıfın üyeleri yani özellik ve fonksiyonları mantıksal açıdan birbirlerine yakın olmalı ve benzer bir amaç için var olmalıdırlar. Yani aynı sınıfta birbirinden amaç olarak uzak olan iki üye olmamalıdır. Böyle bir durum var ise muhtemelen yanlış bir tasarım yapıyoruz demektir.
  • Bir sınıfı tasarladığımızda o sınıfın tek bir amacı olmalıdır. Birden fazla amaca hizmet eden sınıflar kötü tasarımı gösterirler. Yazılımda bir değişiklik olacağı zaman muhtemelen bir amaca yönelik olur. Eğer sınıflarımızı doğru bir şekilde tanımlarsak herhangi bir ihtiyaçta çok az kod değişimi yaparak belki de hiç kod değiştirmeden sadece biraz kod ekleyerek problemi çözebiliriz.
  • Tasarladığımız sınıflar genellikle birbirleri ile etkileşim içerisinde olurlar. Bu kaçınılmaz bir durumdur ancak bu bağımlılıklar kodda bir değişiklik yapmak istediğimizde karşılaştığımız en büyük problemdir. Bu yüzden sınıfların arasında mümkün olduğunca az bağımlılık yaratmalıyız ve yarattığımız bağımlılıkların seviyesini mümkün olduğunca düşürmeliyiz. Bu seviyeyi düşürmenin en güzel yolu da polimorfizm'dir.
  • Tasarladığımız sınıflarda yeniden kullanılabilirliği düşünmeliyiz. Örneğin bir sınıfa eklediğimiz özellikler başka sınıflarda da kullanılabilecek genel özellikler ise bu özellikleri tekrar tekrar yazmak yerine yeniden kullanılabilirliği sağlayarak genelleştirmeli ve tüm sınıflarımızda kullanmalıyız. Böylelikle genel yapıda bir değişiklik yapmak istediğimizde tüm sınıflarda değişiklik yapmak yerine tek bir noktada değişiklik yapabiliriz.
  • Tasarladığımız sistemlerde değişimin olmaması imkansızdır. Yazılım sistemlerinde değişim kaçınılmaz bir durumdur. Bu yüzden sistemimizi buna göre tasarlamak zorunda olmalıyız. Sistemimizi buna göre tasarlarken dikkat etmemiz gereken nokta bir değişiklikte kod değişikliği yapmak yerine sisteme yeni kodlar eklemek olmalıdır. Çünkü sınıflar arasındaki bağımlılıktan dolayı yapılacak bir kod değişiminin etkisi çok büyük olacaktır. Bu yüzden kod değişimi yerine kod eklemesi yapmalıyız ve sistemimizi buna göre tasarlamalıyız. Bunu yapmanın en kolay yolu polimorfizm'dir.

Örnek 

Aşağıdaki örnekte kötü bir tasarım örneğini görüyoruz. Bu sisteme yeni bir şekil eklediğimizde Ciz sınıfı içerisindeki metodu değiştirerek if bloğu içerisine bir koşul daha yazmamız gerekecektir. (EnumDegeri ile mantıksal olarak bir enum varmış gibi düşünülmüştür. Burada EnumDeğeri yerine mantıksal bir parametre veya buna benzer bir yapı olabilir önemli olan bu tür kullanımın yanlışlığının gösterilmesi olduğu için enum koda eklenmemiştir.)

public class Ciz{

     public void sekilCiz(EnumDegeri){
         if(EnumDegeri == Kare) {
             Kare kare = new Kare();
             kare.kareCiz();
         } else if(EnumDegeri == Daire) {
             Daire daire = new Daire();
             daire.daireCiz();
         } 
     }
}

public class Sekil {
    
    public void sekilCiz(){
        
    }
}

public class Kare extens Sekil {
    
    public void kareCiz(){
        
    }
}

public class Daire extens Sekil {
    
    public void daireCiz() {
        
    }
}

Bu problemi çözerek daha iyi bir tasarım yapmak için sistemimizi yeni bir şekil eklendiğinde hiç bir kod değişikliği yapılması gerektirmeyecek şekilde tasarlamamız gerekmektedir. Bu tasarım polimorfizm ile gerçekleştirilir. Bu şekilde sistemimize yeni bir şekil eklendiği zaman Ciz sınıfı içerisinde hiç bir değişiklik yapmaya gerek kalmadan sistemimiz çalışacaktır.


public class Ciz{

     public void sekilCiz(Sekil sekil){
         sekil.sekilCiz();
     }
}

public class Sekil {
    
    public void sekilCiz(){
        
    }
}

public class Kare extens Sekil {
    
    public void sekilCiz(){
        
    }
}

public class Daire extens Sekil {
    
    public void sekilCiz() {
        
    }
}

Bir yazılım sistemi çok farklı şekillerde yazılabilir. Bu noktada farklı yaratan yazılımın kalitesidir. Yazılımın kalitesi çoğu zaman performans ile ölçülsede bu tek başına yeterli değildir. Çünkü yazılımlar bir kere yazılan kod parçaları değildir. Yazılımlar sürekli değişir ve yazılımların bu değişimlere ayak uydurması gerekir. Eğer bir yazılım değişim karşısında problem yaratıyorsa o yazılım performansı ne kadar iyi olursa olsun o yazılım kaliteli bir yazılım değildir. Aşağıdaki maddeler kaliteli olmayan bir yazılım başlıca nedenleridir ve bu nedenler kötü yapılmış bir tasarım sonucu ortaya çıkmaktadır.
  • Bir yazılımın sisteminin değişime karşı gösterdiği direnç ne kadar yüksekse o yazılım sisteminin kalitesi o kadar kötüdür. Yani değişen ihtiyaçlar doğrultusunda bir yazılımda değişiklik yapılması ihtiyacı olduğunda karşılaşılan problemler ne kadar çoksa o yazılım tasarımı da o kadar kötüdür. Bu yüzden iyi bir yazılım tasarlanırken esneklik çok önemlidir.
  • Yazılımın modüleritesi yani yeniden kullanılabilirliği ne kadar düşükse yazılım tasarımı da o kadar kötüdür. Yazılım parçaları modüler şekilde tasarlanmalı ve başka sistemlerde de kullanılabilmelidir. Bu şekilde yazılımın parçaları daha kolay değiştirilebilir ve anlaşılabilir.
  • Yazılım sisteminde değişiklik yapılması gerektiğinde bu değişiklikler yazılımın tasarımını değiştirmeye zorluyorsa o yazılımın tasarımı kötü bir tasarımdır.