Open-Closed Principle

Open-Closed Principle in C# – SOLID Design Principles – Part 2

Posted on Updated on

Overview

In our introduction to the SOLID Design Principles, we mentioned the Open-Closed Principle as one of the five principles specified. In this post we are going to dive into this design principle and work with a very simple example in C#.

The Open-Closed Principle states thatΒ modules should be open for extension but closed for modification. Simply stated, this means that we should not have to change our code every time a requirement or a rule changes. Our code should be extensible enough that we can write it once and not have to change it later just because we have new functionality or a change to a requirement.

If you consider the number of times that code is changed after an application makes its first ‘release’ and you begin to quantify the actual time (and expense) associated with repeated changes to code over the life of the application, the actual development cost is probably not going to surpass the ongoing maintenance costs.

Think about it, when you change existing code, you have to test the changes. We all know that πŸ™‚ But to be complete in your implementation, you also have to perform regression testing to ensure no unforeseen bugs have been introduced either as direct or indirect results of your new changes! In short, we don’t want to introduce breakages as the result of modifications – we want to extend functionality.

But let’s be realistic for a second. To interpret the Open-Closed Principle to say that we can NEVER change our code is a bit over-the-top in my opinion. I can tell you from years of practice that this is simply never going to happen! There will always be code changes as long as there are requirements changes – that’s just the way it is. You can however greatly minimize the actual code changes needed by properly designing applications in the beginning, and the Open-Closed Principal is one of the five SOLID design principles that allow you to do that.

In the last post, we began building a few classes that represent the starter/ignition system of an automobile engine. As is the theme for all posts here, we kept the example scenario very simple and kept the code simple to illustrate the design principle.

We are going to take the code that we wrote in the Single Responsibility Principle post and modify it further to illustrate the Open-Closed Principle. We have removed the Battery class because in the context of this discussion it is really not significant and introduces unnecessary noise.

So let’s get going! For more background on how we arrived at the design below, refer back to the the previous post.

The code looks like this:

public class Engine
{
    public IgnitionResult Start(Starter starter)
    {
        return starter.Start();
    }
}

public class Starter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //logic to initiate start
        return IgnitionResult.Success;
    }
}

public enum IgnitionResult
{
    Success,
    Failure
}

Let’s consider this design, and also consider the two notions described by the Open-Closed Principle. Modules that conform are:

  1. Open for Extension – the behavior can be extended in a variety of ways as the requirements change
  2. Closed for Modification – existing, stable code should not be changed

With these things in mind, let’s discuss what we need to do with our Engine/Starter scenario to allow us to minimize changes going forward.

Open-Closed Principle – C# Example

So to get started, let’s take the code from the last post (shown above) and add a little meat to it! We purposely left the body of the Start() method of the Starter class kind of bare because we weren’t concerned with that when we discussed the Single Responsibility Principle. But now we are very interested in that so let’s add a StarterType enum, a StarterType property to our Starter class, and let’s add some dummy logic within the Start() method to make this all work. Remember, this initial code is NOT going to adhere to the Open-Closed Principle – we are modifying the original code in preparation for this.

Design #1 – Not so Good

First, we will add the StarterType enum:

public enum StarterType
{
    Electric,
    Pneumatic,
    Hydraulic
}

Then we will add a property to the Starter class:

public StarterType StarterType
{
    get;
    set;
}

Finally, we will add some logic to our Starter class’s Start() method that performs some action based on the StarterType specified.

public IgnitionResult Start()
{
    //initiate the starter based on the StarterType
    if (this.StarterType == SOLIDPrincipleSamples.StarterType.Electric)
    {
        //initiate the electric starter
        return IgnitionResult.Success;
    }
    else if (this.StarterType == SOLIDPrincipleSamples.StarterType.Pneumatic)
    {
        //initiate the pneumatic starter
        return IgnitionResult.Success;
    }
    else
    {
        //initiate the hydraulic starter
        return IgnitionResult.Success;
    }
}

Okay, we know that we could have one of three types of starters – electric, pneumatic, or hydraulic. Seems reasonable that we would write an if block that checks the type and performs some type of action, right? Well, not really. What if our requirements change and we have to now accommodate another type of starter that we hadn’t anticipated? Well, we will have to change our Starter class, and we really don’t want to do that!

So what can we do?

Design #2 – Much Better

The solution is actually pretty straightforward and it involves thinking in terms of extending NOT modifying! Here’s how we can do that:

First, let’s rethink our Starter class. Instead of having a single concrete Starter class, we will create a concrete class for each type of starter and consider each as a distinct type within our design. To bring all those together, we will create an interface which we will name IStarter. Each of our three starter types will implement this interface and each will contain its specific logic for initiating the engine start functionality.

Our new design looks like this:

Open Closed Principle

So here are our new classes and our new interface:

public class Engine
{
    public IgnitionResult Start(IStarter starter)
    {
        return starter.Start();
    }
}

public interface IStarter
{
    string Brand { get; set; }
    string Model { get; set; }
    IgnitionResult Start();
}

public class ElectricStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the electric starter
        return IgnitionResult.Success;
    }
}

public class PneumaticStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the pneumatic starter
        return IgnitionResult.Success;
    }
}

public class HydraulicStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the hydraulic starter
        return IgnitionResult.Success;
    }
}

public enum IgnitionResult
{
    Success,
    Failure
}

So now we have three concrete classes, one for each type of starter, a single interface which is implemented by each of the three concrete starter classes, and our StarterType enum is no longer needed! With this design, if we need to add a new type of Starter, we do NOT have to change our existing code but instead we can add a new class for that type of starter and change the consuming code very slightly to expect the new type where needed. We are not going to dive into a Factory pattern in this post, but that is an effective way to handle the Open-Closed Principle.

So to recap, if we write code that is open for extending but closed to modification we are much better off! The Open-Closed Principle provides us guidance for how to accomplish this. Now keep in mind that this was a simple example, but as I say all the time, simple examples are the best ways to illustrate virtually any development topic.

In the next post, we will dive into the Liskov Substitution Principle. See the links below for all posts related to the SOLID Design Principles.

The SOLID Design Principles
The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)

Advertisement

SOLID Design Principles

Posted on Updated on

In software development, principles differ from patterns in the sense that where patterns represent complete, identifiable, repeatable solutions to common problems, principles are objective, factual statements that can be made about code and the manner in which it is constructed and the overall design of an implementation. In other words, patterns refer to code scenarios while principles refer to qualities of code and these qualities are useful in identifying the value of the code.

In this post, we are going to be introduced to Bob Martin’sΒ SOLID design principles. These principles have been around for a long time, and it is immeasurably important for every object-oriented developer to understand them and use them in making day-to-day design decisions. It is not that uncommon to see developers dive into development tasks by writing code first and considering architecture and design second. A good developer though inverts this scenario and considers a sound design before writing the first line of code! This prevents undue code maintenance pain later and allows for the development of better applications through and through.

Consideration of design principles is extremely important throughout a development effort, and failure to make the proper considerations can have a devastating effect on the development of the application and the application’s usefulness, performance, and maintainability.

What are the SOLID Design Principles?

In this section, we are going to outline and briefly discuss each of the five design principles. In subsequent posts we will dive into each principle in more detail. The five SOLID design principles are listed below:

  1. The Single Responsibility Principle (SRP) – this principle states that there should never be more than one reason to change a class. This means also that a given class should exist for one and only one purpose.
  2. The Open-Closed Principle – this principle states that modules should be open for extension but closed for modification. This seems a bit unclear at first until you realize that you can change an object’s behavior by either using abstractions, implementing common interfaces, and from inheritance from common base classes or extending abstract base classes.
  3. The Liskov Substitution Principle – this principle, introduced by Barbara Liskov, simply-stated means that derivative classes have to be substitutable for their base classes. If we think about this for a minute, we can see that this also means that any class that implements a specific interface can be replaced by any other class that implements that interface. In other words, it can be substituted for the original class. Furthermore, a derived class must honor the ‘contract’ set by its super class.
  4. The Interface Segregation Principle (ISP) – this principle states that objects should not be forced to implement interfaces that they do not use. Though this may sound obvious based on the wording, in practice it really means that interfaces should be finely-grained and not specific to the classes for which their implementation is intended.
  5. The Dependency Inversion Principle (DIP) – this principle states that higher-level classes should not depend upon lower-level classes but that both should depend on abstractions. Furthermore, these abstractions should not depend on concrete details but rather on abstractions as well.

Though each principle can be discussed individually, we should understand that no single principle should exist or be applied by itself. They should all be considered as part of the design process. In the following posts, we will dive into each principle in detail and take a look at some simplified code examples that illustrate each one.

The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)