Solid Principle
SOLID represents 5 principles of object-oriented programming:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation
- Dependency Inversion
Ref resource: Link
Single Responsibility Principle:
It states that every module, class, and function should have single responsibility for the assigned work. This principle helps you to keep your classes as clean as possible.
Example
For software development we have multiple people doing different thing like designer do designing, tester doing testing and developer doing development.
from the above example, the Handler class performs multiple responsibilities like making a network call, parsing the response, and saving it into the database.
You can solve this problem by moving the responsibilities down to little classes.
Open/Closed Principle
This principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
In simple terms, you should be able to add new functionality to a class without changing its existing code. This principle is essential because it protects existing code from bugs and ensures that the system remains stable even as new features are added.
Why OCP is Important
The Open/Closed Principle is crucial for several reasons:
- Stability: By not modifying existing code, you reduce the risk of introducing new bugs into a stable system.
- Scalability: OCP allows you to extend the system’s capabilities without altering its core behavior, making it easier to scale and add features.
- Maintainability: Following OCP leads to a codebase that is easier to maintain and understand, as the original logic remains intact and changes are isolated to new extensions.
Without Open/Closed Principle
Payment Processor class without OCP
class PaymentProcessor{
func processCreditCardPayment(double: amount){
Print("Processing credit card payment of amount Rs."+amount);
}
func processDebitCardPayment(double: amount) {
Print("Processing debit card payment of amount Rs."+amount);
}
}
Drawbacks of this approach
Modifying the existing class to accommodate new payment methods can lead to increased complexity, reduced maintainability and challenges in testing as more payment methods are added in future.
With Open/Closed Principle
To overcome these drawbacks, you can apply Open/Closed Principle that allows for extension without modifying the existing code.
PaymentProcessor Class with OCP
protocol PaymentProcessor {
func process(amount: Double)
}
class CreditCardPayment: PaymentProcessor {
func process(amount: Double) {
print("Processing credit card payment for amount Rs. \(amount)")
}
}
class DebitCardPayment: PaymentProcessor {
func process(amount: Double) {
print("Processing debit card payment for amount Rs. \(amount)")
}
}
class Main {
static func main() {
let creditCard: PaymentProcessor = CreditCardPayment()
let debitCard: PaymentProcessor = DebitCardPayment()
creditCard.process(amount: 50.0)
debitCard.process(amount: 100.0)
}
}
Using Open/Closed Principle it becomes very convenient to add new payment processor without hampering the existing payment processors. This promotes a more maintainable and flexible design.
Liskov Substitution Principle: states that objects of a parent class should be replaceable with objects of a child class without affecting the correctness of the program.
var parentObj: Parent = C1() or C2() or C3
If a parent class object is replaced by a subclass object, the program should function as expected.
Breaking LSP Example
Client Code that Fails
import Foundation
let vehicles: [Vehicle] = [
Car(),
Motorcycle(),
Bicycle()
]
for vehicle in vehicles {
print("Number of wheels: \(vehicle.getNumberOfWheels())")
print("Has engine: \(vehicle.hasEngine())") // Crashes when calling Bicycle's method
}
- The
Bicycle
class reduces functionality by overridinghasEngine()
and making it unusable (fatalError
). - This breaks the assumption that all
Vehicle
subclasses can substitute theVehicle
class without introducing errors.
Client Code Failure:
- When the client code calls
hasEngine()
on aBicycle
instance, it causes a runtime crash due to thefatalError
. - This design requires the client code to be aware of specific subclasses (e.g.,
Bicycle
), which violates LSP. Instead, subclasses should seamlessly integrate into the parent class’s functionality.
According to LSP, the following principle should hold:
“If
S
is a subclass ofT
, then objects of typeT
should be replaceable with objects of typeS
without altering the correctness of the program."
In this example:
- The
Bicycle
subclass does not respect the behavior expected byVehicle
(i.e., allVehicle
instances should return a valid value forhasEngine()
). - Introducing
Bicycle
forces changes in the client code to handle its unique behavior, breaking the substitutability required by LSP.
Client Code
Scenario 1: Array of Vehicle
Scenario 2: Array of EngineVehicle
Explanation
- Base Class (
Vehicle
):
- Contains shared functionality common to all vehicles, such as
getNumberOfWheels()
.
2. Specialized Subclass (EngineVehicle
):
- Adds behavior specific to engine-powered vehicles, like
hasEngine()
.
3. Concrete Implementations (Motorcycle
, Car
, and Bicycle
):
- Override methods from the parent classes to provide specific implementations.
4. Type Safety:
- You cannot mix
Bicycle
(aVehicle
) into a list ofEngineVehicle
due to type constraints. This ensures that incompatible subclasses cannot cause runtime errors.
Interface Segregation
The Interface Segregation Principle (ISP) suggests that a class should not be forced to implement methods it doesn’t need. In other words, a class should have small, focused interfaces rather than large, monolithic ones. This helps to avoid unnecessary dependencies and ensures that classes only implement the methods they actually need.
Below is a simple example that does not adhere to the ISP.
We have an interface for a washing machine. This interface has two methods : washClothes() and dryClothes()
In our combo washing machine this interface works great but if we introduce a washing machine with less features we run into problems.
Here’s the equivalent code in Swift:
protocol WashingMachine {
func washClothes()
func dryClothes()
}
class ComboWashingMachine: WashingMachine {
func washClothes() {
print("Clothes washed in a combo washing machine")
}
func dryClothes() {
print("Clothes dried in a combo washing machine")
}
}
class SimpleWashingMachine: WashingMachine {
func washClothes() {
print("Clothes washed in a simple washing machine")
}
func dryClothes() {
print("This method doesn't apply to a simple washing machine")
}
}
// Example usage
let comboMachine: WashingMachine = ComboWashingMachine()
comboMachine.washClothes()
comboMachine.dryClothes()
let simpleMachine: WashingMachine = SimpleWashingMachine()
simpleMachine.washClothes()
simpleMachine.dryClothes()
Using the interface on a simple washing machine means that the method dryClothes() will be unusable and unnecessary. This is a violation of the ISP and we should refactor our interface so we don’t have any empty method implementations or return any dummy values.
A simple solution to our simple program would be to separate the two methods in the single interface into two separate interfaces.
( Keep in mind, this does not mean that an interface should always have only one method. That will lead to problems of it’s own. For this small example it is a sufficient solution. )
protocol Washer {
func washClothes()
}
protocol Dryer {
func dryClothes()
}
class ComboWashingMachine: Washer, Dryer {
func washClothes() {
print("Clothes washed in a combo washing machine")
}
func dryClothes() {
print("Clothes dried in a combo washing machine")
}
}
class SimpleWashingMachine: Washer {
func washClothes() {
print("Clothes washed in a simple washing machine")
}
}
// Example usage
let comboMachine: Washer & Dryer = ComboWashingMachine()
comboMachine.washClothes()
comboMachine.dryClothes()
let simpleMachine: Washer = SimpleWashingMachine()
simpleMachine.washClothes()
The main difference being SRP focuses on classes and ISP focuses on interfaces.
Dependency Inversion
High-level modules should not depend on low-level modules. Both should depend on protocol(abstraction).
we know that the high-level module that we are referring to is the Calculator class while the low-level modules are the different classes (AddCalculatorOperation, SubtractCalculatorOperation, MultiplyCalculatorOperation, DivideCalculatorOperation) that implement the ICalculatorOperation which is our abstraction.
uses.
The
Ref Link:
2. https://medium.com/swlh/dependency-inversion-principle-5187ea8b3332
3. https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5