Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Null Object Design Pattern Nedir? Nasıl Uygulanır?

Merhaba,

Bu içeriğimizde; C#, Java, Python vs. gibi OOP dillerinde null olabilen referansların getirisi olan olası hata durumlarıyla kontrol yapılarından ziyade daha tasarımsal olarak başa çıkabilmenin bir yolu olan Null Object Design Pattern‘ı inceliyor olacağız.

Null Object Design Pattern, bir nevi nesnelerin kullanımını ve davranışlarını açıklayan ve null durumlara istinaden ekstradan özel kontrollere gerek kalmaksızın yazılımın akışını sağlayan bir tasarım desenidir. Şimdi gelin bu tasarımın tam anatomisini inceleyelim.

Null Durumlarının Kritikliği?

Bir probleme odaklı hususi algoritma geliştirilirken yahut mimarisel çalışmalar yaparken genellikle kullanılan objelerin null olup olmadığı bizler için önemlidir! Misal vermemiz gerekirse;

    class Obj
    {
        //...
    }

yukarıda tanımlanan ‘Obj’ isimli model üzerinden aşağıdaki referansı değerlendirirsek eğer;

            Obj o = null;

görüldüğü üzere null değer barındırmaktadır. Yani bu referans özünde değersizdir. Daha teknik konuşmak gerekirse bellekte herhangi bir nesneyi işaretlememektedir. Dolayısıyla bizler bu referansa olan ihtiyaca istinaden öncelikli olarak bu referansın bir nesneyi işaret edip etmediğinden emin olmalı ardından kullanıma odaklanmalıyız. Öyle değil mi?

            Obj o = null;
            if (o != null)
            {
                //Eğer o referansı null değilse(yani bir nesneyi referans ediyorsa
                //o zaman işlem yap!
            }

Yukarıdakine koda bakarsanız, çalışma hayatınız boyunca benzerine çok şahit olduğunuza eminim diyebilirim. Peki bizler neden bu kontrolü yapıyoruz? diye sorduğumuzda cevabı basit! Olmayan yani teknik olarak null olan bir referans üzerinden herhangi bir işlem yapmaya çalışmak oldukça tutarsız bir durumdur. İşte bu durumda yazılımsal olarak hataya sebep olacaktır.

Mesela yukarıdaki kodu şöyle kullanırsak;

            Obj o = null;
            o.ToString();

null olan ‘o’ referansı üzerinden(yani olmayan bir nesne üzerinde) bir işlem yapılmaya çalışıldığı için hiç yabancı olmadığımız NullReferenceException hatasıyla karşılacağız. İşte bu yüzden öncelikle bu nesnenin null olup olmadığından emin olmamız gerekmektedir.

İyi de hoca! Bu referansın null olduğu zaten aşikar! dediğinizi duyar gibiyim. Evet, haliyle benim de örnek olsun diye bu referansı kullandığım aşikar 🙂 Keza burada dikkatinizi çekmeye çalıştığım husus bu tarz kontrollerin referanslarla çalışıldığı sürece kaçınılmaz olması üzerinedir. Nihayetinde gerçek dünyada elde edilen objeler genellikle metotlardan ya da propertylerden geldikleri için ilgili objelerin null olup olmamaları aşikarlıktan ziyade meçhul olmaktadır.

Null Object Design Pattern’ın Amacı Nedir?

Yukarıda konuştuğumuz gibi bir referans boş yani null olabilir. Ancak bizler bu null durumu, kodu if/else yapılarına boğmaksızın hiçbir şey yapmadan şeffaf bir şekilde nasıl ele alabiliriz sorusunu sorarak yola çıkacağız! Bunun için Null Object diye bir kavram üreteceğiz.

Null Object; bir referansın null durumunu ifade eden ve herhangi bir davranışı olmayan nötr bir nesnedir.

Böylece bir referansın boş olması durumlarında null yerine varsayılan olarak hiçbir davranış sergilemeyen Null Object’i kullanarak null‘dan kaynaklı olası hatalara karşı if/else kontrollerinden kodu arındırabilecek ve kısmi olarak null durumunu kapsülleyerek akışın temiz bir şekilde seyretmesini mümkün kılabileceğiz.

Null Object Design Pattern; değeri/nesnesi olmayan bir referansın null yerine herhangi bir davranışı olmayan nötr bir nesneyi(Null Object) temsil ederek kodu kontrol yapılarından arındırma stratejisidir.

Null Object Design Pattern Teoride Nasıl Kullanılır?

Null Object Design Pattern Nedir? Nasıl Uygulanır?Bir nesneye Null Object Design Pattern’ı uygulayabilmek için öncelikle bu kontrolün yapılacağı türe ait işbirlikçi(collaborator) sınıflar oluşturmamız gerekmektedir. Neden işbirlikçi sınıf? diye sorarsanız eğer ilgili nesnenin null durumu yerine herhangi bir şey yapmayan ve ihtiyaç duyulmayan, o nesnenin null durumuna karşılık gelen ama yine de bellekte bir karşılığı olan hali olarak düşünebilirsiniz.

Misal, üstteki şemaya göz atarsanız eğer uygulamada kullanılacak esas nesne ‘RealOperation’ nesnesiyken, bu nesnenin null durumunda kullanılacak nesne ise ‘NullOperation’ nesnesidir. ‘RealOperation’ nesnesi içerisinde ‘request’ metodunda gerekli operasyonu/işi gerçekleştirmektedir. Lakin ‘NullOperation’ nesnesi ise birebir ‘RealOperation’ nesnesiyle aynı içeriğe sahip olsa dahi içerisindeki ‘request’ metodunda herhangi bir işlem yürütmemektedir. Yani boştur. Davranışı yoktur. Nötr davranış sergiler. Tek gayesi ‘RealOperation’ nesnesinin null durumunu ifade etmektir.

‘Client’ yapmış olduğu işlem sürecinde ‘RealOperation’ nesnesine ihtiyaç duyduğunda ve ilgili nesnenin üretilmediği ya da olmadığı durumda null yerine ‘NullOperation’ nesnesi elde edecek ve herhangi bir if/else kontrolüne gerek duymaksızın ‘request’ metodunu tetikleyebilecektir. Çünkü artık kâh ‘RealOperation’ kâh ‘NullOperation’ olmak üzere kesinlikle bir nesne gelecektir. Burada her iki nesnenin de client tarafından karşılanabilmesi için ‘AbstractOperation’ ismi verilen base class ile refere edildiğine dikkatinizi çekerim.

Şimdi gelin, Null Object Design Pattern’ın aktörlerini daha net kavramsallaştırarak ele alalım;

  • Abstract Object
    Client’ın kullanacağı arayüzü temsil eder. Real Object ile Null Object’in türeyeceği sınıftır. Yukarıdaki şemada ‘AbstractOperation’a karşılık gelmektedir.
  • Real Object
    Client’ın beklediği davranışı sağlayan somut sınıftır. Şemada ‘RealOperation’a karşılık gelmektedir.
  • Null Object
    Hiçbir şey yapmayan, herhangi bir davranış sergilemeyen somut sınıftır. Real Object’in null halidir. Hiçbir şey yapmamanın tam olarak ne anlama geldiği, client’ın ne tür bir davranış beklediğine bağlıdır. Dolayısıyla hiçbir şey yapmamanın birden fazla yolu olabileceğinden dolayı birden fazla Null Object sınıfı oluşturulabilir. Şemada ‘NullOperation’a karşılık gelmektedir.

Null Object Design Pattern, null durumlarının işlenmesinin client’tan soyutlanması için kullanılır!

Null Object Design Pattern’ın Pratik Örneğini Yapalım

Evet, tüm bu tanımlamalara istinaden konuyu daha da netleştirecek bir örnek yapalım. Örneğimizde bir Repository‘den gelecek olan ‘Employee’ nesnesinin Null Object Design Pattern eşliğinde inşasını inceleyelim.

  • 1. Adım
    İlk olarak client’ın kullanacağı Abstract Object arayüzüne karşılık gelecek olan ‘AbstractEmployee’ modelini oluşturarak başlayalım. Bu model ister interface isterseniz de abstract class olarak tanımlanabilir.

        //Abstract Object
        abstract class AbstractEmployee
        {
            public abstract int Id { get; set; }
            public abstract string Name { get; set; }
            public abstract string Surname { get; set; }
            public abstract void GetFullName();
        }
    
  • 2. Adım
    Ardından bu arayüzden türeyecek olan ‘Employee’ isimli Real Object nesnesini oluşturalım.

        //Real Object
        class Employee : AbstractEmployee
        {
            public override int Id { get; set; }
            public override string Name { get; set; }
            public override string Surname { get; set; }
    
            public override void GetFullName()
                => Console.WriteLine($"{Name} {Surname}");
        }
    
  • 3. Adım
    Ve ‘Employee’ nesnesinin işbirlikçi(collaborator) nesnesi olan ‘NullEmployee’ isimli Null Object nesnesini inşa edelim.

        //Null Object
        class NullEmployee : AbstractEmployee
        {
            private NullEmployee() { }
            static NullEmployee nullEmployee;
            public static NullEmployee Nil => nullEmployee ?? (nullEmployee = new());
    
            public override int Id { get; set; }
            public override string Name { get; set; }
            public override string Surname { get; set; }
            public override void GetFullName()
                => Console.WriteLine("Employee not found!");
        }
    

    Null Object nesnesinin Singleton olarak tasarlandığına dikkatinizi çekerim.

    Ayrıca bu nesnenin ‘Employee’ nesnesiyle aynı içeriğe sahip olduğuna lakin hiçbir şey sergilemediğine de dikkatinizi çekerim…

  • 4. Adım
    Kurgu gereği formaliteden bir repository sınıfı oluşturalım.

        class EmployeeRepository
        {
            //Veri kaynağı misali...
            List<AbstractEmployee> employeeList = new()
            {
                new Employee() { Id = 1, Name = "Ahmet", Surname = "Yıldız" },
                new Employee() { Id = 2, Name = "Mehmet", Surname = "Yıldız" },
                new Employee() { Id = 3, Name = "Sebahattin", Surname = "Yıldız" },
                new Employee() { Id = 4, Name = "Malkoç", Surname = "Yıldız" },
                new Employee() { Id = 5, Name = "Cuma", Surname = "Yıldız" },
            };
    
            public List<AbstractEmployee> GetWhere(Func<AbstractEmployee, bool> method)
                => employeeList.Where(method).ToList();
            public AbstractEmployee GetSingle(Func<AbstractEmployee, bool> method)
                => employeeList.FirstOrDefault(method);
        }
    
  • 5. Adım
    Ve bu repository sınıfını kullanan bir servis oluşturalım.

        class EmployeeService
        {
            readonly EmployeeRepository _employeeRepository;
            public EmployeeService(EmployeeRepository employeeRepository)
                => _employeeRepository = employeeRepository;
            public AbstractEmployee GetEmployeeByName(string name)
                => _employeeRepository.GetSingle(e => e.Name == name).NullCheck();
        }
    

    7. satırın sonunda çağrılan ‘NullCheck’ fonksiyonuna göz atarsanız eğer bu ilgili nesnenin null olup olmama durumuna göre geriye gelen ‘Employee’ nesnesini ya da ‘NullEmployee’ nesnesini döndüren tarafımızca extension hazırlanmış bir fonksiyondur.

  • 6. Adım
    İlgili ‘NullCheck’ extension metodunu hazırlayalım.

        static class Extensions
        {
            public static AbstractEmployee NullCheck(this AbstractEmployee instance)
                => instance ?? NullEmployee.Nil;
        }
    

Görüldüğü üzere Null Object Design Pattern’ın uygulanması oldukça basit. Oluşturulan bu tasarımı aşağıdaki gibi kullandığımızda nasıl sonuç verdiğini gelin hep beraber inceleyelim.

        static void Main(string[] args)
        {
            EmployeeService employeeService = new(new());
            AbstractEmployee e = employeeService.GetEmployeeByName("Ahmet");
            e.GetFullName();
            e = employeeService.GetEmployeeByName("Mehmet");
            e.GetFullName();
            e = employeeService.GetEmployeeByName("Muiddin");
            e.GetFullName();
        }
Null Object Design Pattern Nedir Nasıl Uygulanır

Null Object Design Pattern Örnek Çalışma Çıktısı

Peki biz Null Object Design Pattern ile neyi sağlamış olduk? bu soruyu önceden cevaplandırmaya çalışmış olsam da sanırım tekrar sorduğunuzu duyar gibiyim… O halde yukarıda yapılan tasarımın olmadığı bir senaryoda client’ın nasıl bir kullanıma maruz kalacağını aşağıda zahiren görmenizde fayda olacaktır.
Null Object Design Pattern Nedir Nasıl Uygulanırİşte, ortaya koyulan fark ilgili nesnenin null kontrolü yapılarak kodun aşırı şişirilmesini ortadan kaldırmış olmamızdır.

Null Object Design Pattern’da stratejisinde, nesnenin herhangi bir davranış sergilemeyen null halini oluşturmanın(Null Object), client açısından inşa edilen kodun daha az öngörülebilir bir durum ortaya çıkardığını düşünebilirsiniz. Nihayetinde client null yerine Null Object nesnesinin geleceğini bilemeyebilir yahut herhangi bir dökümandan beslenmediği sürece her daim null‘un gelmeyeceğinin garanti olacağını düşünemeyebilir. İşte böyle bir durumda if/else kontrolü uygulayarak algoritmanın seyrinde farklı hatalara sebebiyet verilebilir. İşte bu durum dökümantasyon ve domain uzmanlarıyla doğru etkileşim neticesinde atlatılabilecek nazik bir haldir. Onun dışında ekstradan yan etki yaratabileceğine dair bir durum yoktur.

C#’da Null Durumlarına Karşı Başka Hangi Refleksler Üretilebilir?
  • Extension Metotlar
    C# programlama dilinde null olan bir referans üzerinden herhangi bir member’a erişim yapılamaz. Yapıldığı taktirde NullReferenceException hatasıyla karşılaşılır. Bu durumdan daha önceki satırlarımızda bahsetmiştik. Lakin null olan bir referans üzerinden extension metotlara erişebilmekteyiz. Bunun nedeni extension metotların static olarak tanımlanarak bir nesneye ihtiyaç duymamalarındandır. Dolayısıyla null referansları kontrol etmek için ayriyetten extension metotlar tercih edilebilir.

    Örneğin;

        static class Extension
        {
            public static bool IsNull<T>(this T instance)
                => instance == null;
            public static bool IsNotNull<T>(this T instance)
                => instance != null;
        }
    

    Ya da bu yapıyı yukarıda gerçekleştirdiğimiz pratik örneğin 6. adımındaki ‘NullCheck’ metodu gibi inşa edebilir ve null durumunda geriye Null Object dönebilirsiniz.

  • Null Conditional Operatörü
    C# programlama dilinde null referanslara karşı if/else yapısıyla yapılan kompleks kontrollerin yerine 6.0 versiyonunda Null Conditional Operators özelliği gelmiş bulunmaktadır. Bu operatör sayesinde if/else kontrollerine gerek kalmaksızın nesneleri aşağıdaki gibi kontrol edebilir ve varsa eğer member’larına erişim sağlayabiliriz.

    var x = {Eleman1}?.{Eleman2}?.{Eleman3}…

    Yukarıda görüldüğü üzere her bir ? operatörü eşliğinde ilgili objenin null olup olmadığı kontrol edilmekte ve eğer null değilse .(nokta) operatörü ile member’ı tetiklenmektedir.

    Örneğin;

        class Obj
        {
            //...
        }
    
            static void Main(string[] args)
            {
                Obj o = null;
                o?.ToString();
            }
    

Null Object Design Pattern her ne kadar adında Design Pattern etiketini taşısada özünde bir tasarım deseni değil Refactoring tekniğidir!

Bazen Null Object’ler aşırı basit ve dummy data olarak görülebilmektedirler. Ancak gerçekte Null Object’ler diğer nesnelerle etkileşime girmeden ne yapılması gerektiğini her zaman tam olarak bildiğinden dolayı esasında çok akıllı nesnelerdir!

Genel Kurallar
  • Null Object sınıfları genellikle singleton olarak uygulanırlar. Çünkü bu sınıflar uygulamada tek bir nesnenin null durumunu ifade edeceklerinden dolayı her ihtiyaç olduğunda tekrar tekrar üretmektense ve genellikle herhangi bir değişecek state’leri olmadığından birçok instance’ı aynı olacağından singleton olarak tasarlamak maliyet açısından daha elverişli olacaktır.
  • Bazı client’lar Null Object’in farklı şekillerde hiçbir şey yapmamasını bekleyebilir. Misal olarak, kimi Null Object loglama yapıyorken kimisi mail gönderebilir. Ya da bir başkası hakkaten hiçbir şey yapmayabilir. İşte bu durumlar için birden fazla Null Object sınıfı gerekli olacaktır.
  • Uygulamalarda demo/deneme sürümleri için ziyaretçiler açısından genellikle Null Object’ler kullanılabilir.
Diğer Kalıplarla İlişkisi
  • Proxy İlişki durumu : Benzerlik
    Proxy, iş yapan gerçek bir nesneye dolaylı olarak kontrollü bir şekilde erişebilmemizi sağlar. Null Object ise gerçek bir nesneyi gizlemez ve ona vekillik yapmaz! Direkt gerçek nesnenin yerini alır.

    Proxy, gerçek bir nesne gibi davranabilmek ve özne olabilmek için mutasyona uğrar lakin Null Object ise gerçek davranışı sağlamak için herhangi bir mutasyona uğramaksızın her zaman hiçbir şey yapmama davranışı sergiler.

    İşte bu durumda Null Object Design Pattern, Proxy Design Pattern’a benziyor olabilir.

  • Strategy İlişki durumu : Özel durumu olabilir
    Strategy, bir işi gerçekleştirmek için farklı yaklaşımlar olarak birkaç concrete strategy nesnesi oluşturur. Bu yaklaşımlardan biri sürekli olarak hiçbir şey yapmamaksa eğer işte o concrete strategy nesnesi özünde bir Null Object nesnesi mahiyetindedir. Bu durumda Null Object Design Pattern, Strategy Design Pattern’ın özel bir durumu olabilir.
  • State İlişki durumu : Özel durumu olabilir
    State tasarım deseninde, her bir concrete state nesnesinde belli başlı metotlar farklı durumlarda iş yapmak için uygulandığından bazı metotlar hiçbir davranış göstermezler. Salt olarak boşturlar. Ancak bir concrete state nesnesi vardır ki o duruma özel tüm metotlar hiçbir şey yapmaksızın inşa edilir. Haliyle bu durumda Null Object Design Pattern, State Design Pattern’ın özel bir durumu olabilir.
Eleştiriler

Null Object Design Pattern, program akışında oluşan hataların normal program yürütmesi gibi görünmesine neden olacağından dolayı dikkat edilmesi gereken tasarımdır. Ayrıca önceki paragraflarda değinmeye çalışıldığı gibi client tarafından ilgili yapıya Null Object tasarımının uygulandığı bilinmeyebilir ve böylece if/else ile null kontrolü uygulanarak mantıksal hatalara sebebiyet verebilme ihtimali vardır. Haliyle bu yönlerinden dolayı hem dikkatli kullanılmalıdır hem de client açısından dökümantasyon ve domain expert’lerle ilişki oldukça önem kazanmaktadır. Ayrıca sadece boş kontrollerden kaçınmak ve kodu daha okunabilir kılmak için bu kalıbı uygulamaya özen gösterilmesi gerekmektedir.

Her şey bir nesnedir! Bir nesnenin yokluğu başka bir nesne tarafından modellenir.

Smalltalk İlkesi

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir