SOLID Principles (Series) [PART 2]

SOLID Principles (Series) [PART 2]

SOLID - make Object-Oriented designs more understandable, flexible and maintainable.

If get to know something new by reading my articles, don't forget to endorse me on LinkedIn

OCP : Open–Closed Principle.

In this article, we'll talk about Open/Closed Principle (OCP) as one of the SOLID principles of object-oriented programming.

OCP Principle states that Software Entities should be open for extension, but closed for modification.

As a result, when any requirements change then the entity can be extended, but not modified.

Example of OCP:

Let's consider, we're building a view action component that might have several actions, such as single click and double click.

interface ViewAction {}

Let's define two classes SingleClick and DoubleClick and both of the implement ViewAction.

class SingleClick : ViewAction {
    fun onSingleClickAction() {
        println("Single Click Action Executed")
    }
}
class DoubleClick : ViewAction {
    fun onDoubleClickAction() {
        println("Double Click Action Executed")
    }
}

Let's create another Object called ViewActionExecutor, which expose an API called perform() which take ViewAction as a parameter. Inside the perform() method, method calls the Implementation of APIs respectfully based on the Instance checking.

object ViewActionExecutor {
    fun perform(viewAction : ViewAction) {
        if (viewAction is SingleClick) {
            viewAction.onSingleClickAction()
        }
        if (viewAction is DoubleClick) {
            viewAction.onDoubleClickAction()
        }
        else { //ignore }
    }
}

Till so far, everything is okay and working fine without any issue. But, wait a minute...

What if there are some additional requirements for ViewAction?. Let's say we have to implement more four classes called SwipeLeft, SwipeRight, SwipeTop, SwipeBottom and all of them implement ViewAction.

class SwipeLeft : ViewAction {
    fun onSwipeLeftAction() {
        println("SwipeLeft Action Executed")
    }
}
class SwipeRight : ViewAction {
    fun onSwipeRightAction() {
        println("SwipeRight Action Executed")
    }
}
class SwipeTop : ViewAction {
    fun onSwipeTopAction() {
        println("SwipeTop Action Executed")
    }
}
class SwipeBottom : ViewAction {
    fun onSwipeBottomAction() {
        println("SwipeBottom Action Executed")
    }
}

So, our final Executor class would be

object ViewActionExecutor {
    fun perform(viewAction : ViewAction) {
        if (viewAction is SingleClick) {
            viewAction.onSingleClickAction()
        }
        if (viewAction is DoubleClick) {
            viewAction.onDoubleClickAction()
        }
        if (viewAction is SwipeLeft) {
            viewAction.onSwipeLeftAction()
        }
        if (viewAction is SwipeRight) {
            viewAction.onSwipeRightAction()
        }
        if (viewAction is SwipeTop) {
            viewAction.onSwipeTopAction()
        }
        if (viewAction is SwipeBottom) {
            viewAction.onSwipeBottomAction()
        }
        else { //ignore }
    }
}

Now, as we can see, the more ViewAction is added to the system, the more If Else condition added to the Executor by checking Instance first.

This is where the main problem occurs, becuase each implementation of ViewAction has different API or Method or Function name (ex, SwipeLeft contain onSwipeLeftAction() API and SwipeRight contain onSwipeRightAction()).

As a result, we need to added more If condition to the executor class, which is a bad practice and will lead to serious problem in future as code base will grow more and more. It will be so much hard to maintain and refactor or fixing bugs.

We can solve this problem by following OCP : Open-Closed Principle

As we've seen our component is not yet OCP compliant. The code in the respective method will change with every incoming new operation support request. So, we need to extract this code and put it in an abstraction layer.

One solution is to delegate each operation into their respective class:

interface ViewAction { 
    fun perform() 
}

So, all of the implmentations (ViewAction) classes would be:

class SingleClick : ViewAction {
    @override fun perform() {
        println("Single Click Action Executed")
    }
}
class DoubleClick : ViewAction {
    @override fun perform() {
        println("Double Click Action Executed")
    }
}
class SwipeLeft : ViewAction {
    @override fun perform() {
        println("SwipeLeft Action Executed")
    }
}
class SwipeRight : ViewAction {
    @override fun perform() {
        println("SwipeRight Action Executed")
    }
}
class SwipeTop : ViewAction {
    @override fun perform() {
        println("SwipeTop Action Executed")
    }
}
class SwipeBottom : ViewAction {
    @override fun perform() {
        println("SwipeBottom Action Executed")
    }
}

And, finally our Executor class would be :

object ViewActionExecutor {
    fun perform(viewAction : ViewAction) {
        viewAction.perform()
    }
}

Executor is now super clean and understandable properly. On the other hand, each of implementation is moved to perform() API.

Recap

  • ViewAction has its own API called perform() and childs who implement ViewAction must override the perform() API.

  • Business logic is different for each child but the identification still remains the same.

  • There is no more If Else in the Executor object, no need to check object Instance and call different APIs to perform respective operations.

  • Childs implement ViewAction with respective business logic and Executor just perform the Execution.

Now, the class/interface/base is closed for modification but open for an extension and we acheived Open-Closed Principle successfully.

In the next article we will talk about Liskov Substitution Principle.

That's it for today. Happy Coding...