The O in SOLID -Swift Edition

The O in SOLID -Swift Edition

ยท

4 min read

In this article, we will break down the 'O' in SOLID.

I strongly recommend that you start with the first article in the series in order to cement the concepts in your mind as best as possible.

What are the SOLID principles in software development?

SOLID is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin.

S - Single Responsibility Principle

O - Open-Closed Principle

L - Liskov Substitution Principle

I - Interface Segregation Principle

D - Dependency Inversion Principle

I will be dedicating one article to each of the above principles so that we can get into the nitty-gritty of each of them, and today it is the turn of the Open-Closed Principle.

What is the Open-Closed Principle?

The Open-Closed principle was defined by Robert C Martin in order to create software systems that are robust.

The end-goal of adopting this principle is to design modules that seldom change, or find very significant reasons to change. For instance, the logic of a module should only change if the very core of the logic needs to be updated or modified.

If only the requirements around the module change, one must endeavour to only add new code to support the requirement, rather than modifying any of the older code.

SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.

The key words to look out for in this definition and understand are:

  • "Open for extension"
  • "Closed for modification"

What does it mean when software is "open for extension"?

When a module is "open for extension", it means that the module is basically extendable to represent different behaviours based on differing requirements.

Let us take the example of making coffee.

Choose one coffee recipe and you will notice that the coffee can taste very very different based on the types on ingredients you add to it.

Consider this: You want to enjoy a good ol' cappuccino but you've recently been diagnosed with lactose intolerance. Easy! All you have to do is swap out the cow's milk in the recipe for a plant-based milk of your choice.

The takeaway here is that the recipe for the cappuccino actually remains the same. But the entire outcome of the recipe is a completely different drink specifically catered to a particular requirement.

How is the above example "closed for extension"?

The fact of the matter her is that there is a fixed and pre-determined recipe to make this cappuccino. Modifying the recipe by adding chocolate to it or by changing the proportions of the milk and coffee would fundamentally change the type of coffee we are attempting to make.

This makes the original cappuccino recipe "closed for modification".

Let's take a look at how this is implemented in code:

enum Texture {
    case ground, powdered
}
enum Source {
    case cow, goat, almond, oat, coconut
}

protocol CoffeeBean {
    var texture: Texture { get set }
}
protocol Milk {
    var source: Source { get set }
}
protocol Sweetener {}

class CoffeeMaker {
    let sweetener: Sweetener
    let coffeeBean: CoffeeBean
    let milk: Milk

    init(sweetener: Sweetener, coffeeBean: CoffeeBean, milk: Milk) {
        self.milk = milk
        self.sweetener = sweetener
        self.coffeeBean = coffeeBean
    }

    func makeCoffee() -> Coffee {
        // the logic for the preparation of the coffee would reside here
        return self
    }
}

Here we have a simple coffee maker, that takes three ingredients, a sweetener, coffeebeans and milk.

To make a regular cappuccino, you'd make it like something like this:

struct PowderedSugar: Sweetener {}

struct PowderedCoffeeBeans: CoffeeBean {
    var texture: Texture = .powdered
}

struct CowMilk: Milk {
    var source: Source = .cow
}

let regularCoffee = CoffeeMaker(sweetener: Honey(), coffeeBean: PowderedCoffeeBeans(), milk: CowMilk())

And for the slightly more environmentally conscious, you can make a vegan coffee like this:

struct Honey: Sweetener {}

struct OatMilk: Milk {
    var source: Source = .oat
}

struct GroundCoffeeBeans: CoffeeBean {
    var texture: Texture = .ground
}

let veganCoffee = CoffeeMaker(sweetener: PowderedSugar(), coffeeBean: GroundCoffeeBeans(), milk: OatMilk())

Notice how we did not have to make any changes to the original CoffeeMaker module to make the vegan milk, despite it being a fundamentally different requirement?

The key here is to build modules using abstractions.

Modules should be designed to describe and implement only the intended behaviour of the module.

In this case, we design the CoffeeMaker module to only accept abstractions of ingredients and make the coffee with a particular recipe.

The benefit of this is that we can compose the CoffeeMaker with whichever implementation of the ingredients and still achieve a polymorphic module that is capable of accepting changes based on changing requirements without modifying the logic of the module.

AAAAAAAND we are done!

I hope you have liked and understood the content. All feedback is appreciated :)

Don't forget to Get Swifty ๐Ÿ‘‰๐Ÿป๐Ÿ˜Ž๐Ÿ‘‰๐Ÿป with me and the iOS Community on Twitter and LinkedIn! Tschรผss!

ย