The Open-Closed Principle

Today we are going to go through one of the five tools that every Object Oriented Designer, or just anyone not wanting his code to become a loaded gun pointing at her head, should have in her toolset: the Open/Closed Principle.

The Open/Closed Principle is one of the most mentioned, but yet most misunderstood, of the five SOLID principles. I am sure you are fed up with reading the definition all around the interwebs, but here it is anyway:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Say what!?

Open for extension, but closed for modification. Neat. But, what does that mean? Let’s rephrase the Principle a little bit:

We should be able to modify the behaviour of a piece of code without changing the actual code that implements it.

In other words, we should have magic powers, or superpowers, or be able to bend the space-time continuum.

Having superpowers would be neat. Can I have some?

Sure, it would be neat. But it is definitely out of the scope of this post.

Let me rephrase. How can my code be open for extension but closed for modification?

Let’s go through a real life example. Real life, like in “code extracted from an actual codebase my team has to maintain”. Let’s pretend that the fact that I personally wrote the offending piece of code does not matter.

The problem

I mostly work with OVPs (that’s Online Video Platforms). I build those pesky apps that some TV providers deliver to their subscribers, and allow said subscribers to watch TV on their devices.

So, I needed to display a Program Guide. An Electronic Program Guide, actually.

An Electronic Program Guide is a list of what is going to be broadcasted during a particular range of dates/times. Let’s pretend this is what I need to display:

Screen Shot 2015-04-03 at 8.29.42 pm

Bear with me on this one, please.

As you can see, the list needs to display Movies, TV Shows, and Live events. When the user taps a particular Program, the app needs to transition to a “details” view, which is, obviously, different (different content and different layout, and even different business logic) according to the type of Program the user selected.

For historical reasons, and some practical reasons, the model object that was provided with in order to build that listing looks similar to this:

/*
    The backend provides a Program object in the services response. 
Also, it provides a "type" that identifies the type of program.
*/
enum ProgramType {
    case Movie
    case Episode
    case LiveProgram
}

// Simplified Program, for the shake of the example.
class Program {
    private(set) var title: String
    private(set) var type: ProgramType
    
    init(title: String, type: ProgramType) {
        self.title = title
        self.type = type
    }
}

Yes, you can argue than all my troubles will end if I looked at the right abstraction, and if I did not need to rely on that “type” property. And I would agree with you: remember, there are historical reasons behind this. Anyway, all this “look at the right abstraction” thing is probably matter of another post.

But, on the other hand, relaying on the right abstraction does not change the fact that I need to navigate to different views according to the type of content I need to display.

The simplest possible solution.

I am a big fan of keeping things simple. When you keep things simple, code smells, bad designs, and bad solutions just popup in front of you when you look at the code, jumping out of the monitor, waving their arms, screaming “refactor me, refactor me!”

So, the simplest implementation that would get the job done would be:

/*
    Function to be called when the user taps a program. 
This shall launch the a different view controller according 
to the content that needs to be displayed.
*/
func navigateToDetails(program: Program) {
    switch program.type {
    case .Movie:
        println("It is a movie, navigate to Movie Details")
    case .Episode:
        println("It is an Episode, navigate to Episode Details")
    case .LiveProgram:
        println("It is a LiveProgram, navigate to Live Program Details")
    }
}

let sampleMovie = Program(title: "The Quiet Man", type: .Movie)
let sampleEpisode = Program(title: "CSI: S47E03", type: .Episode)

navigateToDetails(sampleMovie)
//navigateToDetails(sampleEpisode)

Riiiiiiiight, a switch.

Switch is bad

No matter what they tell you, no matter what you see, switch clauses are usually a code smell. And in this particular case, ermahgawd, it is just bad. I wrote that code, and I still want to punch myself in the face overtime I see it. But why?

Well, this will not escalate well. First, and most obvious, Program is poorly designed, it is the wrong abstraction, and to workaround that bad design, it exposes a type to help identify what it actually represents. That is clearly meant to change.

But, the worst is about to come. What if I need to deal with a new type of program? I would need to update that switch. There is no way to extend this code that does not imply editing the navigateToDetails function.

This code is not open for extension and closed for modification.

Again, the simplest possible solution clearly exposes two code smells: first, Program is the wrong abstraction, or at least it is poorly designed, and second, the initial design is not easy to extend.

The better solution, open for extension and closed for modification

Let’s assume that Program is not going to change now. It will, but not at this precise moment.

If you look at the first implementation of the navigateToDetails function, you will see how all the business logic that deals with navigation is in there. That function checks the program type, and launches the command needed to navigate to the desired destination. What I want, is find a way to delay those decisions as much as possible. That way, I can deal with the problem in a generic way here.

The first step to fix my problem (remember, I want to be able to navigate to different views according to the type of Program the user selects) starts with declaring a protocol that abstracts the details of how I want to perform that navigation.

// Protocol that abstract the behaviour: 
navigating to the view that contains the Program's extended info.
protocol Router {
    func canHandleProgram(program: Program)->Bool
    func navigateTo(program: Program)
}

Then, I will have different implementations of that protocol. Each of those concrete implementations will deal with the details of how to navigate to the desired destination, and more importantly, they will provide a way to decide if that particular implementation is supposed to handle a Program or not.

// Implementation of the Router protocol that navigates to the
 Movie extended info
final class MovieRouter: Router {
    func canHandleProgram(program: Program)->Bool {
        return program.type == .Movie
    }
    
    func navigateTo(program: Program) {
        println("It is a movie, navigate to Movie Details")
    }
}

// Implementation of the Router protocol that navigates to the 
Episode extended info
final class EpisodeRouter: Router {
    func canHandleProgram(program: Program)->Bool {
        return program.type == .Episode
    }
    
    func navigateTo(program: Program) {
        println("It is an Episode, navigate to Episode Details")
    }
}

// Implementation of the Router protocol that navigates to the 
LiveProgram extended info
final class LiveProgramRouter: Router {
    func canHandleProgram(program: Program)->Bool {
        return program.type == .LiveProgram
    }
    
    func navigateTo(program: Program) {
        println("It is a LiveProgram, navigate to Live Program Details")
    }
}

That last part is the key of the whole refactoring. Notice how I can inject (or in less fancy words, pass as a parameter) a list of all the available implementations of the Router. My navigateToDetails function will loop those, passing them the Program. If it finds any implementation that can deal with the Program passed as parameter, it will ask the implementation found to proceed and navigate to… where? That’s the thing! The navigateToDetails function does not know where, because it doesn’t need to know.

/*
    Function to be called when the user taps a program. 
All logic related to navigation has been removed from this function.
 A naive implementation of this method would loop the routers array, 
and when it finds one that responds true to canHandleProgram, 
it would call navigateTo on it.

    Here, we use the first element returned by filter. 
If we add more routers, we do not need to touch this method. 
If the implementation of Program changes we do not need to touch 
this method either.
*/
func navigateToDetails(program: Program, routers:[Router]) {
    let matchingRouters = routers.filter({$0.canHandleProgram(program)})
    if let router = matchingRouters.first {
        router.navigateTo(program)
    }
}

let routers:[Router] = [MovieRouter(), EpisodeRouter(), LiveProgramRouter()]
let sampleMovie = Program(title: "The Quiet Man", type: .Movie)
let sampleEpisode = Program(title: "CSI: S43E02", type: .Episode)

navigateToDetails(sampleMovie, routers)
//navigateToDetails(sampleEpisode, routers)

 

Delay all decisions!

I recall reading, some time ago, that good Object Oriented Design consisted in finding ways to delay decisions. I only agree with that up to a point (I guess I have been a victim of over-engineered systems more than once), but I think it makes a good rule of thumb. If a particular part of your system knows too much about others, maybe it is time to rethink it.

But I digress. The whole point of this post was implementing a solution to a problem in a way that allows easy maintenance, and simplifies scalability.

Summary.

Object Oriented Design is all about trade-offs. Simple code usually gets the job done, but sometimes simplicity in the code brings along poor maintainability. Sometimes, ten lines of code get the work done, but sometimes you need to substitute those ten lines by a hundred lines, as long as those hundred lines provide a solid, clean and maintainable solution.

And, did I mention that unit testing the second solution is way easier?

Code samples.

You can get a couple of Swift playgrounds containing the source code to this post on Github

Leave a Reply

Your email address will not be published. Required fields are marked *