Comparing software development to constructing a building says that software is solid and difficult to change. Instead, we should compare software development to gardening as a garden changes all the time. Software Gardening embraces practices and tools that help you create the best possible garden for your software, allowing it to grow and change with less effort. Learn more in What is Software Gardening.
Today we visit the letter O, the second principle of SOLID. The O is for the Open-Closed Principle. The Open-Closed Principle (OCP) states “A software Entity should be open for extension but closed to modification”. Credit for creating term Open-Closed generally goes to Bertrand Meyer in his 1998 book, “Object Oriented Software Construction”.
This article is published from the DNC Magazine for .NET Developers and Architects. Download this magazine from here [Zip PDF] or Subscribe to this magazine for FREE and download all previous and current editions
When you first hear about Open-Closed, you have to wonder, “How can something be open and closed at the same time?” I have heard this jokingly called Shröedinger’s OOP Principle. However, Shröedinger was talking about cats, not Object Oriented Programming.
Robert C. “Uncle Bob” Martin further expanded on the definition of OCP in his 2003 book, “Agile Software Development: Principles, Patterns, and Practices”:
“Open for extension.” This means that the behavior of the module can be extended. As the requirements of the application change, we are able to extend the module with new behaviors that satisfy those changes. In other words, we are able to change what the module does.
“Closed for modification.” Extending the behavior of a module does not result in changes to the source or binary code of the module. The binary executable version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched.
Ugh. Sounds like Uncle Bob wants us to do the impossible. Not only can we not change existing code, but we can’t change the exe or dll files either. Hang on. I’ll explain how this can be done.
Closed for Modification
Let’s drill into this principle, first by looking at closed for modification as this one is easier to discuss. In a nutshell, being closed for modification means you shouldn’t change the behavior of existing code. This ties in nicely with Single Responsibility that I explained in my column in the previous issue. If a class or method does one thing and one thing only, the odds of having to change it are very slim.
There are however, three ways you could change the behavior of existing code.
The first is to fix a bug. After all, the code is not functioning properly and should be fixed. You need to be careful here as clients of the class may know about this bug and have taken steps to get around this. As an example, HP had a bug in some of their printer drivers for many years. This bug caused printing errors under Windows, and HP refused to change it. Microsoft made changes in Word and other applications to get around this bug.
The second reason to modify existing code is to refactor the code so that it follows the other SOLID principles. For example, the code may be working perfectly but does too much, so you wish to refactor it to follow Single Responsibility.
Finally, a third way, which is somewhat controversial, is you are allowed to change the code if it doesn’t change the need for clients to change. You have to be careful here so that you don’t introduce bugs that affect the client. Good unit testing is critical.
Open for Extension
Having closed to modification out of the way, we turn to open for extension. Originally, Meyers discussed this in terms of implementation inheritance. Using this solution, you inherit a class and all its behavior then override methods where you want to change. This avoids changing the original code and allows the class to do something different.
Anyone that has tried to do much implementation inheritance knows it has many pitfalls. Because of this many people have adopted the practice interface inheritance. Using this methodology, only method signatures are defined and the code for each method must be created each time the interface is implemented. That’s the down side. However, the upside is greater and allows you to easily substitute one behavior for another. The common example is a logging class based on an ILogger interface. You then implement the class to log to the Windows Event Log or a text file. The client doesn’t know which implementation it’s using nor does it care.
Another way to have a method open for extension is through abstract methods. When inherited, an abstract method must be overridden. In other words, you are required to provide some type of functionality.
Closely related to an abstract method is a virtual method. The difference is where you must override the abstract method; you are not required to override a virtual method. This allows a bit more flexibility to you as a developer.
There is one other, and often overlooked, way to provide additional functionality. That is the extension method. While it doesn’t change the behavior of a method, it does allow you extend the functionality of a class without changing the original class code.
Now, let’s return to Uncle Bob’s expansion of the definition of closed for modification. We need to extend the functionality of the class without changing the original code or the binary (exe or dll) file. The good news is every method of extending behavior we’ve looked at here complies with Uncle Bob’s definition. Nothing in .NET says that everything about a class has to be in the same source file or even in the same assembly. So, by putting the extended code in a different assembly, you can comply to OCP as defined by Uncle Bob.
An Open-Closed Principle example
Explanations are good, but let’s look at an example. First up, code that violates OCP.
public class ErrorLogger
{
private readonly string _whereToLog;
public ErrorLogger(string whereToLog)
{
this._whereToLog = whereToLog.ToUpper();
}
public void LogError(string message)
{
switch (_whereToLog)
{
case "TEXTFILE":
WriteTextFile(message);
break;
case "EVENTLOG":
WriteEventLog(message);
break;
default:
throw new Exception("Unable to log error");
}
}
private void WriteTextFile(string message)
{
System.IO.File.WriteAllText(@"C:\Users\Public\LogFolder\Errors.txt", message);
}
private void WriteEventLog(string message)
{
string source = "DNC Magazine";
string log = "Application";
if (!EventLog.SourceExists(source))
{
EventLog.CreateEventSource(source, log);
}
EventLog.WriteEntry(source, message, EventLogEntryType.Error, 1);
}
}
What happens if you need to add a new logging location, say a database or a web service? You need to modify this code in several places. You also need to add new code to write the message to the new location. Finally, you have to modify the unit tests to account for new functionality. All of these types of change have the possibility of introducing bugs.
You may have missed another problem with this code. It violates the Single Responsibility Principle.
Here’s the better way. While not fully feature complete, this is a starting point.
public interface IErrorLogger
{
void LogError(string message);
}
public class TextFileErrorLogger : IErrorLogger
{
public void LogError(string message)
{
System.IO.File.WriteAllText(@"C:\Users\Public\LogFolder\Errors.txt", message);
}
}
public class EventLogErrorLogger : IErrorLogger
{
public void LogError(string message)
{
string source = "DNC Magazine";
string log = "Application";
if (!EventLog.SourceExists(source))
{
EventLog.CreateEventSource(source, log);
}
EventLog.WriteEntry(source, message, EventLogEntryType.Error, 1);
}
}
When you need to implement other types of loggers, it’s simple…just add new classes rather than modify existing ones.
public class DatabaseErrorLogger : IErrorLogger
{
public void LogError(string message)
{
// Code to write error message to a database
}
}
public class WebServiceErrorLogger : IErrorLogger
{
public void LogError(string message)
{
// Code to write error message to a web service
}
}
Ahhh…a much better way to architect a logging feature.
You now have another seed for your software garden. By following the Open-Closed Principle your code will be better and less prone to bugs. You’re on way to making your software lush, green, and vibrant.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Craig Berntson is a software architect specializing in breaking up the monolith and improving developer teams and processes. He is the author of two books on software development and has been a speaker at conferences across North America and Europe. He received the Microsoft MVP award twenty-two straight years. He lives in Salt Lake City, Utah.