SOLID Design Principles - Open Closed Principle (OCP)
In the second part of the series, We will have a look at one of the most important principles OCP or Open Closed Principle. You can also go thorough the first article on Single Responsibility Principle here.
STOP
Before reading further, I would highly recommend that you have a clear and practical understanding of Polymorphism, Composition and abstraction. Most of the next principles, including OCP, find their roots in these Object Oriented principles. Without a clear understanding of Polymorphism and Composition, OCP and rest of the SOLID principles won't make much sense.
Overview Of OCP
As I had mentioned in the first article about SRP, no article on SOLID principles can be written without crediting Robert C. Martin. However, Open Closed Principle was first defined by Bertrand Meyer in 1988 in his book Object Oriented Software Construction. He explained the Open Closed Principle as
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
What does it mean?
It means that your code should be written in a way that makes it flexible to adapt to changing functionality or new additions to the application (Open), however, without modifying already written and tested code (closed).
Note that whenever we talk about modification in the code while discussing SOLID or other OOAD techniques, we are talking about the code in production that is duly tested and running with Live users.
Implentation vs Polymorphic OCP
But, Meyer's explanation of OCP was based on (implementation) inheritance. He suggested that classes and common code should be distributed as compiled libraries which makes them closed for modification, however, they would still be open for extension, as other developers could inherit from these classes.
However, Inheritance introduces tight coupling as the child class depends upon the implementation details of the parent class. In 1990s, Robert C. Martin started proposing to use Polymorphic Open Closed Principle. Using Polymorphism, we can provide the interface abstraction to the changing behaviors/functionality and avoid the dependency on a strict implementation of that module. Polymorphism introduces loose coupling which helps in plugging new functionality without modification to the existing code behaviors.
LETS GO TO WAR !
Consider that we are writing a computer game where the players have the option to create an army of soldiers with different attack power. A very simple way to write this code is to create a Parent class Soldier containing the base functionality and create different Child classes with specific functionality. Something like this:
Class Soldier
{
public string March()
{
return "March By Foot";
}
public int Attack()
{
return -5;
}
}
Class SwordsMan : Soldier
{
public override int Attack()
{
return -10;
}
}
Class SpearMan : Soldier
{
public override int Attack()
{
return -15;
}
}
The Problem(s)
At first sight, it looks pretty good. We are already using a certain level of polymorphism and by overriding the Attack() function, we can write the specialized code for each type of Soldier.
NOW, Consider that we decided to add "NEW" Soldier Types say Knights and Skirmishers (Stationed Guards), something like this
Class Knight : Soldier
{
public override int Attack()
{
return -25;
}
}
Class Skirmisher : Soldier
{
public override int Attack()
{
return -10;
}
}
WHAT CAN GO WRONG ?
Because of our tight coupling with Soldier class, Our Knight which is supposed to March on a horse is "Marching by Foot" and Our Skirmisher, which is supposed to be "Stationed" and Guard a location, is also free to "March By Foot".
In such cases we mostly go back and try to override these special cases in Sub Classes which defeats the whole purpose of inheritance at the first place and definitely not suggested on a production environment.
Because of Tight coupling, We find ourselves in these situations:
- Adding a "New" functionality to the Base class will, unintentionally, add that functionality to all the existing SubTypes, even if not required.
- Adding "New" SubTypes will inherit all the static functionality of the Base class as we just saw above, and have to be overridden to NOT do anything, or do things DIFFERENTLY.
- Modifying a common functionality in a fixed set of subtypes need conditional code in each of the subtypes that needs to be modified and/or the calling code.
OCP Solution - Think Abstraction
The Polymorphic Open Closed Principle suggests to use Abstract Classes or Interfaces to abstract the differentiating "Behaviors" (or variable algorithms) in your flow/code. This does not just simply mean using Interfaces and Polymorphism but also thinking from a generalization point-of-view where you need to identify Common but Variable behaviors in similar classes and implement those behaviors separately with a common abstraction/interface/contract.
While thinking from a generalization perspective, a useful tip that helped me personally is to change the way we think about naming the behaviors and functionality. While writing Classes and Method names, we should start thinking in terms of common behaviors and Name them accordingly. All the common behaviors, then, form a family and can be separated as a common interface,
For example, if we have an ecosystem of Animals, we might have a class Animal and then sub classes like Dog , Cat, Cow etc. Each of these animals make a sound.
Class Animal
{
//Some Common Code
}
Class Dog : Animal
{
public void Bark()
{
}
}
Class Cat : Animal
{
public void Meow()
{
}
}
Class Cow : Animal
{
public void Moo()
{
}
}
This will end up programmers having to have a long if-else/switch block to choose the right method.
class main()
{
Arraylist<Animal> animals = new Arraylist<Animal>();
animals.Add(new Dog());
animals.Add(new Dog());
animals.Add(new Cat());
animals.Add(new Cow());
animals.Add(new Dog());
foreach(Animal a in animals)
{
switch(a.GetType())
{
case Dog:
a.Bark();
break;
case Cat:
a.Meow();
break;
case Cow:
a.Moo();
break;
}
}
}
which means we have to add the conditional code to this calling code every single time we add a new SubType in the system, which makes our code vulnerable to bugs (in case we forget to do so).
However, if we consider these behaviors as SOUND behaviors and can find a common action like MakeSound, we can abstract the internal call of each class, to a more generalized function.
abstract Class Animal
{
public abstract void MakeSound();
}
Class Dog : Animal
{
public override void MakeSound()
{
this.Bark();
}
private void Bark()
{
}
}
Class Cat : Animal
{
public override void MakeSound()
{
this.Meow();
}
private void Meow()
{
}
}
Class Cow : Animal
{
public override void MakeSound()
{
this.Moo();
}
private void Moo()
{
}
}
class main()
{
Arraylist<Animal> animals = new Arraylist<Animal>();
animals.Add(new Dog());
animals.Add(new Dog());
animals.Add(new Cat());
animals.Add(new Cow());
animals.Add(new Dog());
foreach(Animal a in animals)
{
a.MakeSound();
}
}
Always prefer Composition over implementation
Second Step towards generalization is to choose Composition rather than statically implementing interfaces or abstract classes, as much as we can. Implementation, again, leads to tight coupling of code.
Strategy Pattern
Strategy Pattern is one of the most common ways to implement Open Closed Principle. Strategy Design Pattern works by choosing any one of the algorithms among a family of algorithms at run time (which ofcourse is made possible by using Polymorphism and Composition).
Lets Revisit our Soldier Example once again and see how we can improve.
The first problem we faced was with March method when we tried to add new subtypes, as different sub classes might have different (variable) behavior. So, Lets try to separate this out in an Interface
Inteface MarchingBehavior
{
public abstract void March();
}
and create specific March Behaviors
Class MarchByFoot : MarchingBehavior
{
public override void March()
{
"March by Foot";
}
}
Class MarchByHorse : MarchingBehavior
{
public override void March()
{
"March by Horse";
}
}
Class NoMarch: MarchingBehavior
{
public override void March()
{
"No March";
}
}
Compose this Behavior in the Soldier class instead of implementing different Behaviors in different classes.
Class Soldier
{
MarchingBehavior marchBehavior;
public int Attack()
{
return -5;
}
}
Instantiate the specific Marching behavior in SubTypes while Construction
Class SwordsMan : Soldier
{
public void SwordsMan()
{
marchBehavior = new MarchByFoot();
}
public override int Attack()
{
return -10;
}
}
Class SpearMan : Soldier
{
public void SpearMan()
{
marchBehavior = new MarchByFoot();
}
public override int Attack()
{
return -15;
}
}
Class Knight : Soldier
{
public void Knight()
{
marchBehavior = new MarchByHorse();
}
public override int Attack()
{
return -25;
}
}
Class Skirmisher : Soldier
{
public void Skirmisher()
{
marchBehavior = new NoMarch();
}
public override int Attack()
{
return -10;
}
}
Generalize the calling code
class main()
{
Arraylist<Soldier> army = new Arraylist<Soldier>();
army.Add(new SwordsMan());
army.Add(new Skirmisher());
army.Add(new Knight());
army.Add(new SwordsMan());
army.Add(new SpearMan());
foreach(Soldier s in army)
{
s.marchBehavior.March();
}
}
Benefits
By separating the Marching Behavior in its own family of algorithms, we have been able to separate all the variable code that can be implemented by all the subtypes. By doing so, we have been able to achieve these benefits:
- Calling Code is now fixed (or closed for modification). No Matter how many modifications we make in the March Behavior or how many New Subtypes of Soldier or Marching Behavior we add, calling code will never change, since it relies on the Marching behavior interface and not specific implementation.
- Adding of new SubTypes is easy. While adding New Subtypes, we can now choose to instantiate the subtype with just the right instance of MarchingBehavior and that is it.
- Adding New Marching Behavior also relies on the MarchingBehavior interface. New MarchingBehavior subtyes will not interfere with existing Soldier Subtypes or the calling code. If we ever need to change the Marching Behavior of any existing Soldier subtype, we just need to change a single line where we are instantiating the MarchingBehavior with the appropriate instance and That is it. No changes in any other code file.
We can also think to move the Attack Behavior to a separate Interface and Add Weapons and other such Behavior. However, I will leave the further improvements to the reader to think and try to implement these behavioral families as an action item.
Till next time, Happy Coding.
Software Engineer | .NET | C# | ASP.NET | SQL | Web & Windows Development
1 年Thanks for sharing Your example was explicit
Senior Software Engineer (Manager 3) @ Times Internet(Times Ad Platform - AdTech)
4 年Where are the other ones SOLID principles. Explained Nicely Amritpal Singh !! I want all ?? Can u share please.
Software Engineer| API Development| Backend Development | JavaScript | Java | nodejs | python | Web Development | Spring Boot | Django | AngularJs
5 年Perfect examples? sir?
SDE-2 at Amazon | MX Player | Ex - Airtel | Ex - Fabhotels | Java | Kotlin
6 年Wonderful explanation sir , Thank you for sharing with us.
iOS Developer at BINGE
6 年This is a very nice explanation of the open closed behaviour of SOLID Principles and you have always been the best teacher ever sir. Just a small change in the main method at the very end, after creating marching behaviour interface and creating all marching classes. We created an array of army but added all the soldiers to animals array instead of army May be it was a typo. The explanation is the best.