Solid Principles Swift
Solid principles are used to make object oriented programs more flexible, understandable and maintainable.
There are 5 principles :
S — Single-responsibility principle
O — Open-closed principle
L — Liskov substitution principle
I — Interface segregation principle
D — Dependency Inversion Principle
Let’s start with the first item, the single responsibility principle.
1. Single Responsibility
A class (object) or struct or unit should focus only if it is one and not take on multiple responsibilities.
For example, let’s have a class called Car, this class must have a property that determines the car age of the car object, no other responsibility should be assumed.
Inccorrect usage:
class Car {
private var manufacturingDate: Date
init(manufacturingDate: Date) {
self.manufacturingDate = manufacturingDate
}
func calculateAge() -> Int {
let calendar = Calendar.current
let currentDate = Date()
let ageComponents = calendar.dateComponents([.year], from: manufacturingDate, to: currentDate)
return ageComponents.year ?? 0
}
func startEngine() {
}
func stopEngine() {
}
func accelerate() {
}
func brake() {
}
}
There is incorrect usage here because while calculating the age of the car, calculate startEngine, stopEngine, accelerate, brake is mentioned at the same time.
if correct usage :
class Car {
private var manufacturingDate: Date
init(manufacturingDate: Date) {
self.manufacturingDate = manufacturingDate
}
func calculateAge() -> Int {
let calendar = Calendar.current
let currentDate = Date()
let ageComponents = calendar.dateComponents([.year], from: manufacturingDate, to: currentDate)
return ageComponents.year ?? 0
}
}
As can be seen, the Car class only took responsibility for the job calculating the age of the car.
In this example, the Single Responsibility Principle was followed. The car class contains a vehicle’s core features and servers, and only calculating the vehicle’s age takes care of the perceptions.
2. Open-Closed
The classes, structures or units we develop should be open to development and expansion but closed to change.
That is, instead of replacing an existing component, components must be extensible to add new functionality.
As an example, let’s take a Shape class. This class provides a method for calculating the area of various shapes. First, let’s write an application that can only calculate the area of circles:
class Shape {
// Diğer ortak özellikler ve metodlar burada olabilir.
func calculateArea() -> Double {
fatalError("Subclasses must override calculateArea method.")
}
}
class Circle: Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
}
override func calculateArea() -> Double {
return Double.pi * radius * radius
}
}
In this construct, the Shape class is used as an abstract class and is extended by the Circle class. However, suppose we want to add new shapes to the application in the future, for example squares (Square) and rectangles (Rectangle). In accordance with the Open-Closed Principle, we should be able to add new shapes without changing the Shape class:
class Square: Shape {
var sideLength: Double
init(sideLength: Double) {
self.sideLength = sideLength
}
override func calculateArea() -> Double {
return sideLength * sideLength
}
}
3) Liskov Substitution
This principle states that a superclass (base class) can be substituted by any subclass (derived class). That is, when you subclass a class in your code, it means that the program should work as expected. For example, the subclass inheriting from the parent class must be able to use all the methods, otherwise it will violate this method.
incorrect usage example :
protocol Birds {
func fly()
func walk()
}
class Pigeon : Birds {
func walk() {
print("Pigeon is walking")
}
func fly() {
print("Pigeon is flying")
}
}
class Chicken : Birds {
func walk() {
print("Chicken is walking")
}
func fly() {
fatalError("Chicken is not flying")
}
}
Correct usage:
protocol BirdsFlyProtocol {
func fly()
}
protocol BirdsWalkProtocol {
func walk()
}
class Pigeon : BirdsFlyProtocol ,BirdsWalkProtocol {
func walk() {
print("Pigeon is walking")
}
func fly() {
print("Pigeon is flying")
}
}
class Chicken : BirdsWalkProtocol {
func walk() {
print("Chicken is walking")
}
}
4) Interface Segregation Princible
According to this principle, a class should not depend on interfaces it does not need or use. It is important to use small and customized interfaces that focus on needed features. This reduces unnecessary complexity and dependencies and enables a more flexible design.
If an approach that did not conform to the Interface Segregation Principle had been used, it could have been as follows:
protocol MusicPlayer {
func play()
func stop()
func connectBluetooth()
}
class Smartphone: MusicPlayer {
func play() {
// Play music
}
func stop() {
// Stop music
}
func connectBluetooth() {
// Start Bluetooth
}
}
class MP3Player: MusicPlayer {
func play() {
// Play music
}
func stop() {
// Stop music
}
func connectBluetooth() {
fatalError("MP3 dont have bluetooth")
}
}
Correct Usage:
protocol MusicPlayer {
func play()
func stop()
}
protocol BluetoothConnectable {
func connectBluetooth()
}
class Smartphone: MusicPlayer, BluetoothConnectable {
func play() {
// Play
}
func stop() {
// Stop
}
func connectBluetooth() {
// Start Bluetooth
}
}
class MP3Player: MusicPlayer, WiredConnectable {
func play() {
// Play
}
func stop() {
// Stop
}
}
5) Dependency Inversion Princible
According to this principle, higher-level modules should not be directly dependent on lower-level modules. Instead, both levels should depend on abstractions.
This principle aims to create a more flexible, maintainable and testable code base by reversing dependencies.
We can use an example to understand the Dependency Inversion Principle in Swift. Let’s say we are working on a music player application. In the application, we need to be able to play music from different music sources (Spotify, Apple Music, YouTube, etc.). For this, we can create a code structure as follows:
protocol MusicSourceProtocol {
func playMusic()
}
class Spotify: MusicSourceProtocol {
func playMusic() {
}
}
class AppleMusic: MusicSourceProtocol {
func playMusic() {
}
}
class MusicPlayer {
private let musicSource: MusicSourceProtocol
init(musicSource: MusicSourceProtocol) {
self.musicSource = musicSource
}
func play() {
musicSource.playMusic()
}
}
In this example, the MusicSource protocol represents a common abstraction of music sources. The Spotify and AppleMusic classes provide their own way of playing music by implementing this protocol.
The MusicPlayer class, on the other hand, is dependent on a music source, but implements this dependency over an abstract protocol rather than concrete classes. This prevents the MusicPlayer class from being directly dependent on the Spotify or AppleMusic classes. Dependency is reversed and music playback is handled through MusicSource.
In this way, we get a design that complies with the Dependency Inversion Principle. The high-level MusicPlayer class is dependent on the low-level MusicSource protocol and runs over that protocol instead of its concrete implementations. In this way, we can more easily adapt to future changes of music sources, add new music sources and make the code more flexible.