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:
Single Responsibility
Open/Closed
Liskov Substitution
Interface Segregation
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
Post a Comment