Solid Principle

--

SOLID represents 5 principles of object-oriented programming:

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation
  5. 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 overriding hasEngine() and making it unusable (fatalError).
  • This breaks the assumption that all Vehicle subclasses can substitute the Vehicle class without introducing errors.

Client Code Failure:

  • When the client code calls hasEngine() on a Bicycle instance, it causes a runtime crash due to the fatalError.
  • 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 of T, then objects of type T should be replaceable with objects of type S without altering the correctness of the program."

In this example:

  • The Bicycle subclass does not respect the behavior expected by Vehicle (i.e., all Vehicle instances should return a valid value for hasEngine()).
  • 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

  1. 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 (a Vehicle) into a list of EngineVehicle 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:

  1. https://medium.com/@kedren.villena/simplifying-dependency-inversion-principle-dip-59228122649a

2. https://medium.com/swlh/dependency-inversion-principle-5187ea8b3332
3. https://medium.com/movile-tech/dependency-inversion-principle-in-swift-18ef482284f5

--

--

No responses yet