DEV Community

loading...

SOLID Principles in Swift: Open/Closed Principle

Ibrahima Ciss
Avid learner, passionate about . Writing apps for iOS/macOS. Software Engineer.
・4 min read

Last week we revised the Single Responsibility Principle or SRP, today let's have a look at the Open/Closed Principle or OCP, which states: "Entities should be open for extension, but closed for modification." Let's break this definition down.

  • Entities, think of it as classes, structs or methods should be simple to change.
  • Closed for modification, we want to change that entity's behavior without modifying its source code. This can be really problematic. How the heck can we change the behavior of the entity without touching its source code? And that's when we come back to the term extension. We should think about extensibility while writing our entities.

Committing the sin

Without any further ado, let see an example of this principle being applied hoping it’ll click for you.
For that, let’s imagine we have a Square class with its properties like so:

class Square {

  private(set) var width: Double
  private(set) var height: Double

  init(width: Double, height: Double) {
    self.width = width
    self.height = height
  }

}
Enter fullscreen mode Exit fullscreen mode

Now, we say that we want to calculate the total area of squares. If we follow the Single Responsibility Principle, we might create a separated class that is dedicating to calculate that area:

class AreaCalculator {

  func calculate(squares: Square...) -> Double {
    return squares.reduce(0) { $0 + $1.width * $1.height }
  }

}
Enter fullscreen mode Exit fullscreen mode

If we run this, it works flawlessly:

let square1 = Square(width: 10, height: 10) // 100
let square2 = Square(width: 20, height: 20) // 400
let square3 = Square(width: 30, height: 30) // 900

let calculator = AreaCalculator()
calculator.calculate(squares: square1, square2, square3) // 1400
Enter fullscreen mode Exit fullscreen mode

Now let's say we want to calculate the circle's areas as well. Our first thought might be to create a Circle class.

class Circle {

  private(set) var radius: Double

  init(radius: Double) {
    self.radius = radius
  }

}
Enter fullscreen mode Exit fullscreen mode

In the code above, we notice that we call everything square (in the AreaCalculator class) because we didn't think about the possibility that we'll need to calculate the area for anything other than a square. So we'll have to edit the original AreaCalculator class, meaning we broke the Open/Closed Principle.
Let's see how to change the behavior of the calculate method before refactoring it.

class AreaCalculator {

  func calculate(shapes: AnyObject...) -> Double {
    var totalArea: Double = 0
    for shape in shapes {
      if shape is Square {
        let square = shape as! Square
        totalArea += square.width * square.height
      }
      if shape is Circle {
        let circle = shape as! Circle
        totalArea += circle.radius * circle.radius * Double.pi
      }
    }
    return totalArea
  }

}
Enter fullscreen mode Exit fullscreen mode

Swift being a static language, the calculate method has to accept an array of AnyObject, then we manually check the type of the object.
And if we run now the code, we have something like this:

let calculator = AreaCalculator()

let square1 = Square(width: 10, height: 10) // 100
let square2 = Square(width: 20, height: 20) // 400
let square3 = Square(width: 30, height: 30) // 900
let cirlcle1 = Circle(radius: 10) // 314.15926
let cirlcle2 = Circle(radius: 20) // 1256.6370

calculator.calculate(shapes: square1, square2, square3, cirlcle1, cirlcle2) // 2970.79632
Enter fullscreen mode Exit fullscreen mode

Like we said above, we clearly break the Open/Closed Principle by doing this. The AreaCalculator is opened for modification when it should be closed. However, you might be thinking, oh no, it's not a big deal, add a few if statements, and you're done, don't worry about this principle. Ok, fair enough, but let say we also need to calculate the area of a triangle, kite, hexagon, pentagon, and some other …gon shapes (screwed right 😅). Do you think we need to add endless if statements in the calculate method every single time we make a change? Of course no. But now, what can be the solution to make the AreaCalculator closed for modification 🤔?

Coding to an interface (or protocol): the cure

In order to extend the behavior of the AreaCalculator while keeping it closed for modification, we need to separate that behavior behind an interface (or protocol in Swift) and then flip the dependencies around, as Uncle Bob said. It can be a little tricky at first to get this, but let's break it down step by step.

Separating the external behavior behind an interface

To do that, we’ll create a Shape protocol like so:

protocol Shape {
  var area: Double { get }
}   
Enter fullscreen mode Exit fullscreen mode

The nice thing about Swift is we can have properties in Protocols or interfaces and this particularly reads well: a Shape has an area 😁
Now we need to implement or to conform to that interface in each concrete class.

class Square: Shape {

  private(set) var width: Double
  private(set) var height: Double

  var area: Double {
    return width * height
  }

  init(width: Double, height: Double) {
    self.width = width
    self.height = height
  }

}


class Circle: Shape {

  private(set) var radius: Double

  var area: Double {
    return radius * radius * Double.pi
  }

  init(radius: Double) {
    self.radius = radius
  }

}
Enter fullscreen mode Exit fullscreen mode

Step 1 is done, let’s focus on step 2 now.

Flip the dependencies around

To do so, rather than accepting an AnyObject array in our calculate method, we’ll accept an array of Shape.

class AreaCalculator {

  func calculate(shapes: Shape...) -> Double {
    return shapes.reduce(0) { $0 + $1.area }
  }

}
Enter fullscreen mode Exit fullscreen mode

Because we code to an interface and implement it to the Square and Cercle classes, we're 100% sure that they'll have an area property. We simply use it to calculate the sum of all shapes' area.
This is really powerful because if we need to calculate a triangle's area, for instance, we'll just have to create the Triangle class, make sure that it conforms to the Shape protocol, then set the area property along with the formula for calculating the area of a triangle, and we pass that into the calculate method of AreaCalculator.
Notice that we never ever touch the AreaCalculator class again because it already knows how to calculate shape areas.

Conclusion

As a developer, we should strive to write collaborative code. What I mean by that is we should help our fellow colleague or friend to be able to extend the functionality of a system without needing to modify the source code of any existing classes. Yes, it can be difficult at first glance, but by applying these S.O.L.I.D. principles, we can achieve this goal easier. I hope this example makes something click for you for your understanding of the Open/Closed Principle. Next, we'll have a more in-depth look at the Interface Segregation Principle; until then, have a nice week, and may the force be with you 👊.

Discussion (0)

Forem Open with the Forem app