Welcome to Part 2 of the ever growing, popular web series, How to Write Code that Doesn't Suck! Today I want to go over the Open/Closed Principle of OOP. By discussing this principle, we're also going to cover Liskov Substitution and Interface Segregation (the L and I of the SOLID principle).
In simplest terms, the Open/Closed Principle means code is open for extending but closed for modification. I know, it sounds techy, but all it means is once code is released to your client, it should have been written in a way we can extend the behavior without modifying the original source with a heavy hand. Why wouldn't we just modify the original source to match our new requirements? Because the original code has gone through the software lifecycle. It probably had research, design and code reviews, unit testing, marketing, etc... associated with it. By you changing that original source, anything touching it might need revisited, best case retested. So if we can write code properly, the idea is we can extend behavior without being invasive to the existing production code.
Well then, how do we do that? You have a choice. Either use abstract base classes or interfaces. What's the difference? You should choose an abstract base class when there is definite behavior the base class can provide subclasses. If there is no consistent behavior, then use interfaces to create a code contract. So how does this help us? Let's peek at some example code.
The easiest example I can think of is logging. We have so many log destinations these days. We can write to a physical file, a database table, table storage in Microsoft Azure, other cloud storage, the Windows desktop event viewer, etc...
Let's pretend we have an app in production that writes to a physical file that is circular. Circular file means once the tail reaches a certain length of bytes in the file, the file wraps back around to the beginning. It is beneficial for tracing, but for situations of auditing to maintain a large history of actions, it stinks. Well one of our clients is mad because an employee changed a record they shouldn't have and nobody knows what the old value was because the log overwrote itself removing the audit entry showing the change. They're screaming at the CEO this is unacceptable and we must address it in the next release or they're moving to a competing product. They're demanding we create a database table to store auditing rather than the physical file. How can we write code allowing us to make this change without modifying the code surrounding this logging?
To start, let's look at bad code. More than likely, we've all experienced the following:
public class Widget
{
private readonly FileLogger _log;
public int Value {get; private set;}
public Widget(int value)
{
_log = new FileLogger();
Value = value;
}
public void ChangeTheWidgetValue(int newValue)
{
_log.WriteEntry(string.Format("Changing the Widget Value from {0} to {1}", Value, newValue));
Value = newValue;
}
}
This works. It tracks the old and new values in the log. It probably met the requirements established. I can't argue that. However, while functionally correct, there is a major problem with this code. This issue is called coupling. We have coupled our FileLogger class to our Widget class. The Widget class could care less where the log is written, it's just telling the log to track the change. To meet our client's demands, I now have to change the Widget class to not create me this logger but one that writes to the database. What if I have 100 classes performing complex work and audit that work using the FileLogger? I have to change those as well. That's a minimum of 100 changes, and all should be retested. This is a major headache.
But wait...What if this horrible code didn't exist. What if we went back in time, thought things through and wrote decoupled code making the product codebase maintainable and extensible? What would the Widget class look like then?
To start, we need an interface called ILogger. I'm using an interface because there is nothing we can share between a database logger and a file logger through an abstract base class other than method names.
public interface ILogger
{
void WriteEntry(string entry);
}
Now, we need our FileLogger class, which will implement the ILogger interface:
public class FileLogger : ILogger
{
public void WriteEntry(string entry)
{
...
}
}
Now I can inject this logger dependency into the Widget class by making the Widget ask for an ILogger in its constructor:
public class Widget
{
private readonly ILogger _log;
public int Value {get; private set;}
public Widget(ILogger log, int value)
{
_log = log;
Value = value;
}
public void ChangeTheWidgetValue(int newValue)
{
_log.WriteEntry(string.Format("Changing the Widget Value from {0} to {1}", Value, newValue));
Value = newValue;
}
}
See what's happening here? In the first example which is very badly designed code, we're coupling the Widget class with a FileLogger. Now to change to the Database Logger, I have to break that coupling everywhere it exists. With the second set of code, which is decoupled, I just have to do the following:
Create the DatabaseLogger and implement the ILogger interface:
public class DatabaseLogger : ILogger
{
public void WriteEntry(string entry)
{
...
}
}
Next, wherever I'm creating the Widget, I just pass a DatabaseLogger object instead of a FileLogger. Then I just rinse and repeat wherever we need to audit to the database. The Widget class never has to change because it is not coupled to a specific class. Rather, it is coupled to a code contract by use of an interface (Liskov Substitution). As long as I pass something implementing the code contract through the ILogger interface, I only need to change where the Widget is instantiated. This now allows your QA department to use the app as normal and just ensure audit entries are written to the database. Even better is maybe there are clients that like the circular audit file because it saves storage space. We can add a config option to use File or Database logging, and based-on that value, inject the appropriate logger (Interface Segregation). If you follow these principles, you will surprise yourself with your own code, especially if you have to revisit it in the future.
In the next post, I'm going to cover Dependency Injection and the D in the SOLID principles, Dependency Inversion. It may take a few days to write, both are large subjects to cover, but if you understand them, your code will make you happy!
Since I write this stuff on my own time, I usually blaze through it. If you want more detail or if you find any technical errors, feel free to post a comment. If you want to correct my grammar or point-out typos, go walk the plank, I'm a software engineer not a literary mastermind. After years of programming, sometimes you lose grasp of writing with proper English.
No comments:
Post a Comment