SOLID Principles (Series) [PART 2]
SOLID - make Object-Oriented designs more understandable, flexible and maintainable.
Table of contents
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 differentAPI
orMethod
orFunction
name (ex,SwipeLeft
containonSwipeLeftAction()
API andSwipeRight
containonSwipeRightAction()
).
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 ownAPI
calledperform()
and childs who implementViewAction
mustoverride
theperform()
API.Business logic is different for each child but the identification still remains the same.
There is no more
If Else
in theExecutor
object, no need to check objectInstance
and call differentAPIs
to perform respective operations.Childs
implementViewAction
with respective business logic andExecutor
just perform theExecution
.
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...