S.O.L.I.D. Principles with Example (Swift & Dart)

 

S.O.L.I.D. Principles

To create understandable, readable, and testable code that many developers can collaboratively work on.


The following five concepts make up our SOLID principles:

  1. Single Responsibility

  2. Open/Closed

  3. Liskov Substitution

  4. Interface Segregation

  5. Dependency Inversion

1. Single Responsibility Principle

A class should have one and only one reason to change, meaning that a class should have only one job.


Problem: 

class Invoice {

    let String: Book

    let price: Float

    let quantity: Int

    let discountRate: Int

    let taxRate: Float

    var total: Float = 0.0

    

    init(book: Book, price: Float, quantity: Int, discountRate: Int, taxRate: Float) {

        self.book = book

    Self.price = price

        self.quantity = quantity

        self.discountRate = discountRate

        self.taxRate = taxRate

        self.total = self.calculateTotal()

    }


    public func calculateTotal() -> Float {

        let price = ((book.price - book.price * discountRate) * quantity)

        let priceWithTaxes = Float(price) * (1.0 + taxRate)

        return priceWithTaxes

    }


    public func printInvoice() {

        print("\(quantity) x \(book.name)         \(book.price)$")

        print("Discount Rate: \(discountRate)")

        print("Tax Rate: \(taxRate)")

        print("Total: \(total)")

    }


    public func saveToFile(filename: String) {

    // Creates a file with given name and writes the invoice

    }

 

As per above example Invoice is a model class. It also contains some fields about invoicing and 3 methods:

  • calculateTotal method, which calculates the total price,

  • printInvoice method, which should print the invoice to the console, and

  • saveToFile method, responsible for writing the invoice to a file.

Solutions:

Swift Code

class InvoicePrinter {

    

    let invoice: Invoice

    

    init(invoice: Invoice) {

        self.invoice = invoice

    }


    public func printInvoice() {

        print("\(invoice.quantity) x  \(invoice.book.name)   \(invoice.book.price)  $");

        print("Discount Rate: \(invoice.discountRate)")

        print("Tax Rate: \(invoice.taxRate)")

        print("Total: \(invoice.total)$");

    }

}

class InvoicePersistence {


    let invoice: Invoice


    public init(invoice: Invoice) {

        self.invoice = invoice

    }


    public func saveToFile(filename: String) {

        // Creates a file with given name and writes the invoice

    }

DART Code:

class Invoice {

  String book;

  double price;

  int quantity;

  int discountRate;

  double taxRate;

  double total = 0;


  Invoice(

      this.book, this.price, this.quantity, this.discountRate, this.taxRate) {

    total = this.price * this.quantity;

  }

}


class InvoicePrinter {

  final Invoice invoice;


  InvoicePrinter(this.invoice);


  printInvoice() {

    print("${invoice.book} * ${invoice.quantity} = ${invoice.total}");

    print("Discount Rate: ${invoice.discountRate}");

    print("Tax Rate: ${invoice.taxRate}");

    print("Total: ${invoice.total}");

  }

}


class InvoicePersistence {

  final Invoice invoice;


  InvoicePersistence(this.invoice);


  saveToFile(String filename) {

    // Creates a file with given name and writes the invoice

  }

}

2 Open/Closed Principle

Objects or entities should be open for extension but closed for modification.


If we understand with the above example InvoicePersistence responsibility is to saveToFile. And now we also need to add saveToDatabase.

Problem: 

class InvoicePersistence {

    let invoice: Invoice


    public init(invoice: Invoice) {

        self.invoice = invoice

    }


    public func saveToFile(filename: String) {

        // Creates a file with given name and writes the invoice

    }


    public func saveToDatabase(filename: String) {

        // Creates a file with given name and writes the invoice

    }

Solution: Swift Code

protocol InvoicePersistence {

    func save(filename: String)

}


class FilePersistence: InvoicePersistence {

    let invoice: Invoice


    public init(invoice: Invoice) {

        self.invoice = invoice

    }


    public func save(filename: String) {

        // Creates a file with given name and writes the invoice

    }

}

class DatabasePersistence: InvoicePersistence {

    let invoice: Invoice


    public init(invoice: Invoice) {

        self.invoice = invoice

    }


    public func save(filename: String) {

        // save invoice in Database

    }

DART Code

class InvoicePersistence {

  final Invoice invoice;


  InvoicePersistence(this.invoice);


  save(String filename) {}

}


class FilePersistence extends InvoicePersistence {

  FilePersistence(super.invoice);


  @override

  save(String filename) {

    // Creates a file with given name and writes the invoice

  }

}


class DatabasePersistence extends InvoicePersistence {

  DatabasePersistence(super.invoice);


  @override

  save(String filename) {

    // save invoice in Database

  }

}


3. Liskov Substitution

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

Example

class Rectangle {

    let width: Int

    let height: Int


    init(width: Int, height: Int) {

        self.width = width

        self.height = height

    }


    public func getArea() -> Int {

        return width * height

    }

}

Now we can substitute this class without knowing it.

Swift Code:

class Square: Rectangle {

    init(size: Int) {

        super.init(width: size, height: size)

    }

}

func getTestArea(shape: Rectangle) {

    print(shape.getArea())

}

//1:

let rect = Rectangle(width: 10, height: 20)

rect.getArea()

getTestArea(shape: rect)

//2:        

let square = Square(size: 15)

square.getArea()

getTestArea(shape: square)

Dart Code:

class Rectangle {

  final int width;

  final int height;


  Rectangle(this.width, this.height);


  int getArea() {

    return width * height;

  }

}


class Square extends Rectangle {

  Square(int size) : super(size, size);

}


getTestArea(Rectangle shape) {

  print(shape.getArea());

}


//1:

  var rectObj = Rectangle(10, 20);

  rectObj.getArea();

  getTestArea(rectObj);


//2:

  var square = Square(15);

  square.getArea();

  getTestArea(square);


4. Interface Segregation

A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.

Problem: 

protocol Worker {

    func eat()

    func work()

}


class Human: Worker {

    func eat() {

        print("eating")

    }


    func work() {

        print("working")

    }

}



class Robot: Worker {

    func eat() {

        // Robots can't eat!

        fatalError("Robots does not eat!")

    }


    func work() {

        print("working")

    }

As we know robots can work only if they can’t eat. So, we Can’t force the Robot class to implement eating. So, we can segregate Worker abstract class with 2 abstract class Feedable and Workable

Solutions: Swift Code

protocol Feedable {

    func eat()

}


protocol Workable {

    func work()

}


class Human: Feedable, Workable {

    func eat() {

        print("eating")

    }


    func work() {

        print("working")

    }

}


class Robot: Workable {

    func work() {

        print("working")

    }

Dart Code:

abstract class Feedable {

  eat();

}


abstract class Workable {

  work();

}


class Human implements Workable, Feedable {

  @override

  eat() {

    print("eating");

  }


  @override

  work() {

    print("working");

  }

}


class Robot implements Workable {

  @override

  work() {

    print("working");

  }

}



5. Dependency Inversion

The Dependency Inversion principle states that our classes should depend upon interfaces or abstract classes instead of concrete classes and functions.

Problem:

class FileSystemManager {

    func save(string: String) {

        // Open a file

        // Save the string in this file

        // Close the file

   }

}

class Handler {

    let fileManager = FilesystemManager()

    func handle(string: String) {

        fileManager.save(string: string)

    }

}


FileSystemManager is a low-level module and it’s easy to reuse in other projects. The problem is the high-level module Handler which is not reusable because it is tightly coupled with FileSystemManager


Solutions: Swift Code

protocol AppStorage {

    func save(string: String)

}

class FileSystemManager: AppStorage {

    func save(string: String) {

        // Open a file in read-mode

        // Save the string in this file

        // Close the file

    }

}

class DatabaseManager: AppStorage {

    func save(string: String) {

        // Connect to the database

        // Execute the query to save the string in a table

        // Close the connection

    }

}

class Handler {

    let storage: AppStorage

    // Storage types

    init(storage: AppStorage) {

        self.storage = storage

    }

    

    func handle(string: String) {

        storage.save(string: string)

    }

}


DART Code

abstract class AppStorage {

  save(String string);

}


class FileSystemManager extends AppStorage {

  @override

  save(String string) {

    //     // Open a file in read-mode

    //     // Save the string in this file

    //     // Close the file

  }


  //  save(string: String) {

  // }

}


class DatabaseManager extends AppStorage {

  @override

  save(String string) {

    // Connect to the database

    // Execute the query to save the string in a table

    // Close the connection

  }

}


class Handler {

  final AppStorage storage;

  // Storage types

  Handler(this.storage);


  handle(String string) {

    storage.save(string);

  }

}



Comments

Popular posts from this blog

Windows Keys

how to send invite and give a access of selected application on App Store

how to display popover in TableView cell in swift