Loading...

Yazılım ekosistemindeki birçok yazılımcı veya programcı kendisinden talep edileni karşılayan ve çalışabilen bir yazılım ürünü ortaya çıkarabilir. Ancak şöyle bir durum var ki yaptığı işin yani yazdığı kodun her zaman kaliteli olduğu anlamına gelmez. Yazılımda kalite çoğunlukla kişiden kişiye göre değişmektedir. Bazılarına göre çalışıyorsa iyidir düşüncesi varken bazılarında ise görsel olarak ne kadar şık olduğu düşüncesi ağır basmaktadır. İşin aslı tam olarak böyle değil. Yazılımda kalite esneklik, güvenlik, modülerlik gibi birçok konuda incelenebilir. Bir yazılımın bu tarz özelliklere sahip olması için belirli kurallar bütününe göre yazılması gerekir. Bu kurallara sahip olmayan yazılım ürünleri 4 maddeye ayırabileceğimiz sorunlarla karşı karşıya kalacaklardır. Nedir bu sorunlar ?

  1. Rijidite(Esnemezlik): Yazılımda inşa ettiğimiz sistemin/tasarımın değişime karşı koyma isteğidir. İyi bir tasarımda bu isteğin çok az olması beklenir.
  2. Fragility(Kırılganlık): Bir noktada yaptığımız değişiklik sistemin birçok noktasında başımızı ağrıtıyorsa, bu sistem kırılgan bir sistemdir.
  3. Immobilite(Sabitlik): Yazılım sistemleri tak-çalıştır şeklinde tasarlanmalıdır. Yani bir projede yazdığımız bir kodu başka bir projeye de eklediğimizde çalışmalıdır.
  4. Cost(Maliyet): Geliştirme sürecinin hem zaman olarak hem de maddi olarak maliyetinin artmasıdır.

Özetle bu 4 özelliğe sahip bir yazılım tasarımı kötü tasarlanmış bir sistemdir. İşte bu noktada nesneye yönelik programlama prensipleri olan SOLID prensipleri karşımıza çıkıyor. SOLID prensiplerini dikkate alarak yazılan sistemlerde yukarıda bahsettiğim sorunların oluşma ihtimali azalacaktır. O halde çok uzatmadan sırasıyla açıklamaya geçelim.

SOLID Prensipleri

  1. Single Responsibility Principle – Tek Sorumluluk Prensibi
  2. Open/Closed Principle – Açık/Kapalı Prensibi
  3. Liskov Substitution Principle – Liskov Yerine Geçme Prensibi
  4. Interface Segregation Principle – Arayüz Ayırma Prensibi
  5. Dependency Inversion Principle – Bağımlılıkları Tersine Çevirme Prensibi

Single Responsibility Principle – Tek Sorumluluk Prensibi

  • Bir modül/sınıf/method sadece tek bir sorumluluğu/görevi yerine getirmek üzere geliştirilmedir.
  • İlgili modülü/sınıfı/methodu değiştirmek için tek bir nedenimiz olmalıdır.

Örnek:

class Membership {
    func delete() { }
    func login() { }
    func register() { }
    func sendMail() { }
    func sendSMS() { }
    func update() { }
}

Membership sınıfı çok net bir şekilde olması gerekenden çok fazla bir şeyler yapıyor. Bu durumda tek sorumluluk prensibine aykırı bir davranış sergilediği anlamına geliyor. Bu sınıfı tek sorumluluk prensibine uygun hale getirmek için sınıfın üzerindeki gereksiz ve yapmaması gereken işleri parçalayıp ilgili sınıflara böleceğiz. Aşağıdaki tasarım bu prensibe çok daha uygun bir tasarımdır.

class Membership {
    func delete() { }
    func register() { }
    func update() { }
}

class LoginService {
    func login() { }
    func logout() { }
}

class SMSService {
    func send() { }
}

class MailService {
    func send() { }
}

Open/Closed Principle – Açık/Kapalı Prensibi

  • Bir modül/sınıf/method gelişime açık, değişime kapalı olmalıdır.
  • Değişim sadece yeni kodlar ekleyerek yapılmalıdır.

Örnek:

class Draw {
    func circle() { }
    func rectangle() { }
}

class Shape {

}

class Circle: Shape {

}

class Rectangle: Shape {
    
}

Elimizde ekrana daire ve dikdörtgen çizen bir Draw sınıfımız ve bunlarla ilgili Circle ve Rectangle sınıflarımız var. Buradaki tasarım açık/kapalı prensibine aykırı bir tasarımdır. Nedeni ise belirli bir zaman sonra üçgen ve kare sınıflarını eklediğimizde Draw sınıfına yeni fonksiyonlar eklemek zorunda kalacağız.

Doğru bir tasarımda ise Shape adında bir protocol tanımlanıp draw() adında bir fonksiyona sahip olması gerekiyor. Shape protocol’ünden türeyen her bir sınıf kendi draw() fonksiyonunu implemente edebilmelidir.

protocol Shape {
    func draw()
}

class Circle: Shape {
    func draw() {
        print("Circle is drawed")
    }
}

class Rectangle: Shape {
    func draw() {
        print("Rectangle is drawed")
    }
}

class Draw {
    func drawShape(shape: Shape) {
        shape.draw()
    }
}

let draw = Draw()
let circle = Circle()
let rectangle = Rectangle()

draw.drawShape(shape: circle)
draw.drawShape(shape: rectangle)

Liskov Substitution Principle – Liskov Yerine Geçme Prensibi

  • Türetilmiş sınıf nesnelerinin türetilen sınıf yerine geçmesidir.
  • Ana sınıf üzerinde olan özellikler alt sınıflarca da kullanılabilmelidir.

Örnek:

protocol DBService {
    func connect()
    func query()
}

class SQLService: DBService {
    func connect() {
        print("SQL")
    }

    func query() {
        print("SELECT * FROM CUSTOMER")
    }
}

class OracleService: DBService {
    func connect() {
        print("Oracle")
    }

    func query() {
        print("SELECT * FROM PRODUCT")
    }
}

class Connection {
    func connect(with service: DBService) {
        service.connect()
    }
}

let sqlService = SQLService()
let oracleService = OracleService()

let connection = Connection()
connection.connect(with: sqlService)
connection.connect(with: oracleService)

Yukarıdaki örnekte veritabanı işlemleri için bir modül yazılmıştır. Bu tasarımda, Connection sınıfındaki connect(with service: DBService) fonksiyonu DBService türünde aldığı parametereye bu sınıftan türeyen parametre verilmesiyle Liskov prensibini sağlamaktadır.

Interface Segregation Principle – Arayüz Ayırma Prensibi

  • Kısaca protocolleri(arayüzleri) ayırmaktır.
  • Ortak bir protocolü conform(implemente eden) eden sınıflarda, bir sınıf protocoldeki bir özelliği implemente etmek zorunda olmayabilir. Bu durumda protocolleri(arayüzleri) ayırmak gerekmektedir.

Örnek:

protocol LivingSpecifications {
    func breath()
    func eat()
    func run()
    func fly()
}

class Birds: LivingSpecifications{
    func breath() { print("I can") }
    func eat() { print("I can") }
    func run() { print("I can") }
    func fly() { print("I can") }
}

class Human: LivingSpecifications {
    func breath() { print("I can") }
    func eat() { print("I can") }
    func run() { print("I can") }
    func fly() { print("I can't") }
}

class Flower: LivingSpecifications {
    func breath() { print("I can") }
    func eat() { print("I can") }
    func run() { print("I can't") }
    func fly() { print("I can't") }
}

Yukarıdaki örnekte tüm canlılar için LivingSpecifications adlı bir protocol yazıldı. Bu durumda Birds sınıfında tüm özellikler implemente edilmesi gerekiyorken Human sınıfında tüm özelliklerine implemente edilmesi şart değil. Şu anki tasarımda arayüz ayırma prensibini ihlal etmiş bulunmaktayız. Bu sorunu protocollere ayırarak gidereceğiz.

protocol LivingSpecifications {
    func breath()
    func eat()
}

protocol FlightfulSpecifications {
    func fly()
}

protocol SprintingSpecifications {
    func run()
}


class Birds: LivingSpecifications, FlightfulSpecifications, SprintingSpecifications {
    func breath() { print("I can") }
    func eat() { print("I can") }
    func run() { print("I can") }
    func fly() { print("I can") }
}

class Human: LivingSpecifications, SprintingSpecifications {
    func breath() { print("I can") }
    func eat() { print("I can") }
    func run() { print("I can") }
}

class Flower: LivingSpecifications {
    func breath() { print("I can") }
    func eat() { print("I can") }
}

Dependency Inversion Principle – Bağımlılıkları Tersine Çevirme Prensibi

  • Üst/yüksek seviyeli sınıfların alt seviyeli sınıflara doğrudan bağımlılığı olmamalıdır.
  • Çözüm: Doğrudan bağımlılık yerine araya bir protocol yazılmalıdır.

Örnek:

class FileLogger {
    func log() { }
}

class DBLogger {
    func log() { }
}

class LogManager {

    var fileLogger: FileLogger!
    var dbLogger: DBLogger!

    public init(fileLogger: FileLogger, dbLogger: DBLogger) {
        self.fileLogger = fileLogger
        self.dbLogger = dbLogger
    }

    func log(){
        fileLogger.log()
        dbLogger.log()
    }
}

Yukarıdaki örnekte üst seviye sınıfımız LogManager sınıfıdır, alt seviye sınıflar ise DBLogger ve FileLogger sınıflarıdır. Bu durumda çok net bir şekilde üst seviye sınıfımız LogManager, alt seviye sınıflarımıza DBLogger ve FileLogger sınıflarımıza doğrudan bağımlıdır. Çözüm ise üst ve alt seviye sınıf ilişkilerini protocol ile yönetmektir.

protocol Logger {
    func log()
}

class FileLogger: Logger {
    func log() { }
}

class DBLogger: Logger {
    func log() { }
}

class LogManager {

    var logger: Logger!

    init(logger: Logger) {
        self.logger = logger
    }

    func log() {
        logger.log()
    }
}

let dbLogger = DBLogger()
let fileLogger = FileLogger()

let logManager = LogManager(logger: dbLogger)
logManager.log()

Yararlanılan Kaynaklar

  • TAŞDELEN Aykut, UML ve Dizayn Paternleri, Pusula Yayıncılık, 2015