Swift Protokol Yönelimli ve Nesne Yönelimli Programlama Yaklaşımları

Merhaba arkadaşlar, Swift dersleri serimize Swift Protokollere (Protocols) ön hazırlık mahiyetinde, Protokol Yönelimli Programlama (Protocol Oriented Programming, “POP”) ve Nesne Yönelimli Programlama (Object Oriented Programming, “OOP”) nedir, aralarındaki farklar nelerdir, neden POP diye bir yaklaşıma ihtiyaç duyuldu gibi soruların yanıtlarını vererek devam ediyoruz. Bu aşamada Swift Class ve Struct konulu yazımız ile Swift Extensions konulu dersleri okumanızı tavsiye ederiz. Swift Inheritance yazımızı okumadıysanız onu da okumanız bu dersin anlaşılırlığını artıracağından, okunmasının faydalı olacağını düşünüyorum.

Swift Nesne Yönelimli Programlama

Nesne yönelimli programlama yaklaşımı birçok gelişmiş programlama dilinde bulunmaktadır. Yazılan kodun anlaşılabilir olması projelerin en önemli noktalarından bir tanesidir. OOP yaklaşımı kodun anlaşılırlığını ve takip edilebilmesini kolaylaştırarak yazılımcılar için vazgeçilmez bir yaklaşım haline gelmiştir. OOP yaklaşımının birkaç önemli özelliği şunlardır:

– Oluşturulan nesne ve metotlar proje içerisinde farklı yerde yeniden kullanılabilirler. (Reuseability)
– Proje içerisinde nesneleri takip etmek kolaydır, bu sebeple bir nesne hatalı olduğunda çabucak düzeltilebilir. (Maintainability)
– Her işlem kendi nesne ve fonksiyonlarına sahiptir ve her işlem kendi görevinden sorumludur.  (Modularity)
– Sınıf, struct vb. yapılar kolaylıkla genişletilebilir (Extensions).  (Scalability)
– Oluşturulan her sınıfın, structın vb. bir amacı vardır. Gereksiz bir tanımlama yapılmaz, bu ise projemizi basitleştirir. (Simplicity)

OOP Temel Kavramlar

Nesne yönelimli programlamaya uygun programlama dilleri bazı özelliklere sahiptir. Swift nesne yönelimli programlamanın özelliklerini barındırmaktadır. Bu kavramları kısaca şu şekilde açıklayabiliriz:

Encapsulation (Kapsülleme)

Swift programlama dilinde Access Control (Erişim Kontrolü) ile kapsülleme işlemi gerçekleştirilmektedir. Bir değişkene, sınıfa vb. erişimin nasıl yapılacağını veya yapılamayacağını belirtir. Private, public, internal, open, fileprivate erişim kontrolü olarak kullandığımız anahtar kelimelerdir. Detaylarını Access Control dersinde anlatacağız.

Inheritance (Miras Alma)

Bir sınıfın özelliklerini diğer sınıfa aktarabilmesi olarak açıklayabiliriz. Aktaran sınıf ata, aktarılan ise çocuk sınıf olur ve atasının metodlarına ve özelliklerine erişebilir. Swift Multiple inheritance (Çoklu miras alma) desteği vermemektedir, yani bir sınıfın tek bir ata sınıfı olabilir. Swift Inheritance detaylarına buradan erişebilirsiniz.

Polymorphism (Çok Biçimlilik)

Çok biçimli olma durumu ata sınıfta oluşturduğumuz bir metot gövdesinin çocuklarda farklı şekillerde doldurulabilmesi olarak açıklayabiliriz. Örneğin Araba sınıfı bizim ata sınıfımız olsun. Dizel araba ve benzinli araba ise çocuk sınıflarımız olsun. Ata sınıfta oluşturduğumuz ve depoyuDoldur() metodu dizel araba için motorin, benzinli araba için ise benzin dolduracaktır. Şu şekilde kodlayabiliriz:ü

class Araba{
    
    func depoDoldur(){
        
    }
}

class DizelAraba: Araba{
    
    override func depoDoldur() {
        print("Motorin dolduruluyor")
    }
}

class BenzinliAraba: Araba{
    
    override func depoDoldur() {
        print("Benzin dolduruluyor")
    }
}

Nesne yönelimli programlamanın özelliklerinden yukarıda bahsettik. Şimdi ise protokol yönelimli programlamanın özelliklerine biraz bakalım:

Swift Protokol Yönelimli Programlama

Swift dili nesne yönemli bir yapıya sahiptir ancak nesne yönelimli programlamanın eksikliklerini gidermek için protokollerle uyumlu çalışmaktadır. IOS projeleri yapılırken, nesne yönelimli programlamanın bazı özellikleri yetersiz kalmıştır veya zaman içerisinde yeni ihtiyaçlar ortaya çıkmıştır. Swift bu problemleri protokoller ile çözmüş ve aslında nesne yönelimli programlamanın kanayan yaralarına çare bulmuştur diyebiliriz.

Bu problemleri genel olarak şu şekilde söyleyebiliriz; gereğinden fazla büyüyen view controller (Massive (iri, hantal) View Controllers), referans problemi ve alt sınıf (Subclass) problemi. POP zaman içinde ortaya çıkan bu problemlere çözüm bulmuştur.

Massive View Controllers

Geliştirilen projelerde modülerliği koruyabilmek için bir sınıf bir amaç prensibi benimsenmektedir. Ancak delegate ve dataSource metodlarının bulunması, view objelerinin yönetilmesi, networking ve birçok protokolün uygulandığı bir view controller massive (hantal, iri) olarak adlandırılmaktadır. Bu view controllerin kod bakımından çok uzun olması projeyi yavaşlatacağı gibi, anlaşılırlığı da düşürecektir. Massive View controller probleminin çözümü için MVVM (Model-View-ViewModel) tasarım kalıbı uygulanabilir. MVVM tasarım kalıbını uygulayabilmek için ise POP ihtiyaç duyarız. MVVM tasarım kalıbı view controllerin işinin bir kısmını devralır. View controllerda view kısmı yönetilirken, viewModel sınıfında networking ve business logic kısımları yönetilir. Bu sayede view controller’in kod miktarını düşürerek massive view controller probleminden kurtulmuş oluruz.

Referans Problemi

Swift Class ve Struct yapısı dersini anlatırken iki adet tipten bahsetmiştik (Reference Type ve Value Type). Bu tiplerin hangisini neden kullandığımızı, performansını vb. o dersin içeriğinde öğrenmiştik. Reference type kullanmamak için, Swift programlamana dili value type’lara protokoller yazmamıza olanak sağlamaktadır. Bu sayede referanslar ile iş yapmamış oluruz ve kod karmaşıklığını düşürmüş oluruz, ayrıca referans tipi için gerekli senkronizasyon, reference counting gibi uygulama performansını etkileyebilecek işlemlerden kurtuluruz. Apple dokümantasyonunda sınıf ve inheritance kullanmak yerine struct ve protocol kullanımını önermektedir.

Alt Sınıf Problemi

Alt sınıf problemini açıklarken iki adet şemadan faydalanacağız. Birinci şema Swift Nesne Yönelimli Programlama yaklaşımını, ikinci şema ise Swift Protokol Yönelimli Programlama yaklaşımını temsil edecek ve bu şemalar üzerinden anlaşılırlığı artıracağız.

Oluşturduğumuz şemada hayvanlar adında bir ata sınıfımız var ve bu sınıftan miras alan üç adet çocuk sınıf mevcuttur. Miras alan çocuk sınıflar, ata sınıfın bütün metot, değişken ve sabitlerine erişebilmektedir. Buradaki ata sınıf çocuk sınıflara miras vereceği bütün özellikleri barındırmak durumundadır. Bu durumda şöyle bir tablo ortaya çıkmaktadır: Sincap sınıfından oluşturduğumuz bir nesne ata sınıf içinde bulunan “havadaHareket” boolean parametresine erişebilmektedir. Ata sınıf içinde bulunan bu parametre, aslında karga sınıfından oluşturulan nesnelerin kullanması içindir, ancak ata sınıf içinde bulunduğu için bütün çocuklar bu parametreye erişebilmektedir. Alt sınıf problemimiz burada oluşmaktadır. Alt sınıflar, ata sınıfta bulunan ve kendisinin erişmemesi gereken metot, değişken vb. erişememelidir. Ayrıca ata sınıf, çocuk sınıfların özelliklerine sahip olacağı için çocuk sayısı arttıkça ata sınıfın karmaşıklığı artacaktır. Kodlayarak ortaya çıkan tabloya kodlarımız üzerinde bakalım:

class Hayvanlar {
    
    var karaHayvani = false
    var karadaHareket = false
    var denizHayvani = false
    var denizdeHareket = false
    var havaHayvani = false
    var havadaHareket = false
    var yiyecekStok = 0
    var yiyecekVarMi = false
    
    init() {
        karaHayvani = false
        karadaHareket = false
       
        denizHayvani = false
        denizdeHareket = false
       
        havaHayvani = false
        havadaHareket = false

        yiyecekStok = 0
        yiyecekVarMi = false
    }
   
    func karaHayvaniMi() -> Bool { return karaHayvani }
    func karadaHareketEderMi() -> Bool { return karadaHareket }
    func denizHayvaniMi() -> Bool { return denizHayvani }
    func denizdeHareketEderMi() -> Bool { return denizdeHareket }
    func havaHayvaniMi() -> Bool { return havaHayvani }
    func havadaHareketEderMi() -> Bool { return havadaHareket }

    func karadaHareketEt() {}
    func denizdeHareketEt() {}
    func havadaHareketEt() {}
    func yemekYe(miktar: Int){
        yiyecekStok -= miktar
        
        if yiyecekStok < 0 {
            yiyecekVarMi = false
        }
    }
}


class Sincap: Hayvanlar {
   
    override init() {
        super.init()
        karaHayvani = true
        karadaHareket = true
        yiyecekStok = 30
        yiyecekVarMi = true
    }
    override func karadaHareketEt() { print("Sincap yürümeye başladı") }
}


class Kaplumbaga: Hayvanlar {

    override init() {
        super.init()
        karaHayvani = true
        karadaHareket = true
       
        denizHayvani = true
        denizdeHareket = true
        yiyecekStok = 10
        yiyecekVarMi = true
    }
   
    override func karadaHareketEt() { print("Kaplumbağa yürümeye başladı") }
    override func denizdeHareketEt() { print("Kaplumbağa yüzmeye başladı") }

}


class Karga: Hayvanlar {

    override init() {
        super.init()
        karaHayvani = true
        karadaHareket = true
       
        havaHayvani = true
        havadaHareket = true
        yiyecekStok = 20
        yiyecekVarMi = true
    }
   
    override func karadaHareketEt() { print("Karga yürümeye başladı") }
    override func havadaHareketEt(){ print("Karga uçmaya başladı") }
}

Yukarıda çizdiğimiz şemayı basit şekilde kodladık. Burada herhangi bir çocuk sınıftan oluşturduğumuz nesne ile ata sınıfın özelliklerine erişebiliriz. Burada Sincap sınıfından oluşturduğumuz bir nesne ile “havadaHareket” parametresini true yaparsak sincabımızı gökdelenin tepesinden atıp hadi uç demiş oluruz. 🙂 Şaka bir yana bu yaklaşım sıkça hatalarla karşılaşmamıza neden olabilir ve sınıflar büyüdükçe yönetilmesi zorlaşacaktır. O halde bu sorunu çözüme kavuşturan Swift Protokol Yönelimli Programlamada, bu problemin nasıl yönetildiğine bakalım:

Swift Protokol Yönelimli Programlama ile Alt Sınıf Problemi

Yukarıda nesne yönelimli programlama için oluşturduğumuz şemayı Swift Protokol Yönelimli programlama için güncelleyecek olursak şöyle bir şema ortaya çıkacaktır:

Swift Protokol Yönelimli Programlamadan yararlanarak hazırladığımız yapımızda Hayvanlar adında bir protokolümüz var ve bu protokolde bütün hayvanlar için ortak olan özellikleri ve fonksiyonları tutabiliriz. Eğer bir sınıf, struct vb. protokol eklenirse protokol içindeki bütün metot ve özellikleri alması gereklidir ancak protokolün extension ile ayrılan metot ve özelliklerini almak zorunda değildir, ama istediğinde alabilir. Bunun için en anlaşılır örnek tableview delegate ve dataSource metotları verilebilir. Bir sınıfa tableview delegate ve dataSource protokollerini eklediğimizde bize iki metodu zorunlu tutar; cellForRowAt ve numberOfRowsInSection. Ancak biz istediğimiz takdirde farklı tableview metotlarını çağırabiliriz. Örneğin heightForRowAt veya numberOfSections.

Hayvanlar protokolü için yazdığımız extension ile yemekYe() metodunu eklemek zorunda kalmıyoruz, ancak istediğimiz zaman bu metoda erişebiliriz. Aşağıda yeni şemamızı kodlayarak Swift Protokol yönelimli programlamanın bize ne fayda sağladığını görelim:

protocol Hayvanlar {
    var yiyecekStok: Int {get set}
    var yiyecekVarMi: Bool {get set}
}

extension Hayvanlar{
    
    mutating func yemekYe(miktar: Int){
      self.yiyecekStok -= miktar
      if yiyecekStok < 0 {
          self.yiyecekVarMi = false
      }
    }
}

protocol KaraHayvani: Hayvanlar {
    var karadaHareket: Bool {get}
    func karadaHareketEt()
}

protocol DenizHayvani: Hayvanlar {
    var denizdeHareket: Bool{get}
    func denizdeHareketEt()
}


protocol HavaHayvani: Hayvanlar {
    var havadaHareket: Bool{get}
    func havadaHareketEt()
}


struct Karga:KaraHayvani, HavaHayvani {
    func karadaHareketEt() {
        print("Karga yürümeye başladı")
    }
    
    func havadaHareketEt() {
        print("Karga uçmaya başladı")
    }
    
    var karadaHareket: Bool
    var havadaHareket: Bool
    var yiyecekStok: Int
    var yiyecekVarMi: Bool
}

struct Sincap: KaraHayvani {
    func karadaHareketEt() {
        print("Sincap yürümeye başladı")
    }
    
    var karadaHareket: Bool
    var yiyecekStok: Int
    var yiyecekVarMi: Bool
    
}

struct Kaplumbaga: DenizHayvani, KaraHayvani {
    func denizdeHareketEt() {
        print("Kaplumbaga yüzmeye başladı")
    }
    
    func karadaHareketEt() {
        print("Kaplumbaga yürümeye başladı")
    }
    
    var denizdeHareket: Bool
    var karadaHareket: Bool
    var yiyecekStok: Int
    var yiyecekVarMi: Bool
}

Şemamıza uygun olarak oluşturduğumuz bu yapı sayesinde Karga structından oluşturduğumuz bir nesne hiçbir zaman DenizHayvani protokolünün metot ve özelliklerine erişemez. Her sınıf kendisi ile ilgili alanlara erişir.  Swift protokol yönelimli programlamanın bir başka artısı ise değer tipi olan structlara protokoller yazmaya izin vermesidir. Bu yöntem sayesinde daha önce de belirttiğimiz üzere reference counting ve senkronizasyon ihtiyacı ortadan kalkmış olur ve sadece stack’e erişmemiz gerektiği için uygulama performansından kaybımız olmamaktadır. Hemen örneğini de yapalım:

   var karga = Karga(karadaHareket: true, havadaHareket: true, yiyecekStok: 20, yiyecekVarMi: true)
   karga.yemekYe(miktar: 21)
   karga.karadaHareketEt()                         // Karga yürümeye başladı
        
   print("\(karga.yiyecekStok)")                   // -1
   print("\(karga.yiyecekVarMi)")                  // false
        

Yukarıda karga sınıfımızla eriştiğimiz metotlar ve çıktıları görülebilmektedir. Aşağıda ise karga sınıfımızın erişebildiği metot ve değişkenler görülebilmektedir. Şekilden de görüleceği üzere sadece bizim eklediğimiz protokol içindeki metot ve değişkenlere erişebilmektedir ve alt sınıf problemini çözmüştür.

Swift Protokol Yönelimli Programlamanın Avantaj ve Dezavantajları

Swift protokol yönelimli programlamanın bazı avantajları olduğu gibi dezavantajları da vardır. En başta nesne yönelimli programlamanın yaklaşık 50 yıllık bir tarihi vardır ve yazılımcılar arasında kabul görmüş bir yaklaşımdır. Swift protokol yönelimli programlama ise daha çok yeni bir yaklaşımdır. Swift protokol yönelimli programlama ile çalışmak nesne yönelimli programlamaya göre daha karmaşıktır ve kod akışını takip etmek daha zordur. Ancak protokol yönelimli programlama multithreaded yapıya daha uygun çalışmaktadır. Sınıflarımızın kod uzunluğu fazla olmayacağı için yeni eklemeler yapmak ve test case yazmak daha kolay olacaktır. Her ikisinin de bazı avantaj ve dezavantajları vardır ancak performans açısından protokol yönelimli programlama daha iyidir diyebiliriz.

Özet

Swift Protokol Yönelimli Programlamayı anlattığımız bu dersimizde bir sonraki dersimiz olan protokollerin altyapısını oluşturmaya çalıştık.  Neden böyle bir yaklaşıma ihtiyaç duyduğumuzun cevabını vermeye çalıştık.  Nesne yönelimli programlama ile arasındaki farklardan bahsettik ve yapısal olarak her ikisini de inceledik. Swift Protokol yönelimli programlama tamamıyla nesne yönelimli programlamadan daha iyidir diyemeyiz ancak zaman içinde kullanılırlığını artıracağını  düşünüyorum. Bu dersi burada bitirirken herkese faydalı olmasını umarak, mutluluklar dilerim.

Soru, görüş ve önerilerinizi y0rum kısmından veya soru-cevap kısmından iletebilirsiniz. Sağlıcakla…

Kaynak: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

Mastering Swift 5 – Fifth Edition  by Jon Hoffman

 

124

Ali Hasanoglu

Yorum Yaz

Haftalık Bülten

Mobilhanem'de yayınlanan dersleri haftalık mail almak ister misiniz?