Implementing the Open/Closed Principle in C#



25 March 2019 ·  15 min read

In this article we’ll explore one way how you can write branching and/or additive rules-based logic, in an object-oriented manner that satisfies the Open/Closed Principle

You can find the complete source code for this article on my Github page

A brief refresher of the Open/Closed Principle

Let’s start this article by introducing (or reminding ourselves about) the Open/Closed Principle. This principle represents the “O” in “SOLID” - the popular object-oriented software design principles.

I’ve found that of the five SOLID principles, this is the one that seems to cause the most head-scratching amongst developers encountering these principles for the first time.

The Open/Closed Principle states that “Software entities … should be open for extension, but closed for modification.”

So, in other words, the principle is saying that “if you need to change your code because you are introducing additional functionality, then you should be writing your code in such a way that helps you to isolate things - and therefore you shouldn’t then need to touch that older code and consequently risk screwing it up”.

To clarify (and to address a common source of confusion), the principle isn’t saying “you should never edit older code, at all”. If you need to fix bugs, or perform activities such as “tweeking to improve performance”, you can still do these things!.

New programmers, or perhaps those who only have experience with working upon relatively small code-bases, probably won’t appreciate why this design principle is all that important.

I think that “to get it”, you need to put yourself in the mindset of someone who is a small cog in a huge machine … perhaps you’re working alongside dozens of other developers, on a project that has been many years in the making. With large solutions such as this, it’s unlikely that anyone on the team will fully understand all the uses, interconnections, subtleties and dependencies that a piece of code may truly represent.

As a developer, how would you know that something you’re about to change wouldn’t have an unforeseen impact elsewhere in the code? The answer, of course, is that you can’t know - so we need to write software in a way that attempts to mitigate these potential problems.

So how does that work? Let’s step through a simple scenario…



The example problem

In this article, we’re going to talk about a branching/conditional logic scenario (i.e. “if something is true, do the following…”).

There are also other uses that are similar, such as Cumulative rules (i.e. “do this, then this, and this….”), which are easy to adapt from the examples in this article.

Let’s get started…



image showing star trek excelsior

Branching/conditional logic - Starfleet registry numbers

For our coding example, we’re going to write a small console app that performs the important task of decoding the registry number of a Star Trek starship and printing the ship’s name and class to the console.



The venerable switch statement

Amongst the C# tools that could be used to address this problem, the switch statement is likely to be amongst the first to come to mind.

A simple example could look like this:-

class Program
{
   static void Main(string[] args)
   {
       var starshipLogic = new StarshipLogic();
       Console.WriteLine("Please enter starship registry code:");
       string input = Console.ReadLine();
       string output = starshipLogic.GetStarshipNameAndClass(input);
       Console.WriteLine(output );
   }
}

public class StarshipLogic
{
   public string GetStarshipNameAndClass(string registryCode)
   {
       switch (registryCode)
       {
           case "ncc1701":
               return "USS Enterprise (Constitution class)";
           case "ncc2000":
               return "USS Excelsior (Excelsior class)";
           default:
               return "Registry not recognised";
       }
   }
}

In the above demo code, I’ve preemptively made a point of extracting some of the business logic away into the separate class StarshipLogic, so as to address the Single Responsibility Principle.

A switch statement is useful for simple conditional code, such as our straightforward example above. However, it can quickly become unmanageable if, for example, you need to include a great many options.

Things can get increasingly unmanageable if there is more than one or two lines of code against each condition. In that scenario, there is already a strong argument to refactor-out that logic into a separate class/method.

For the purpose of making things a bit clearer in this article, we’ll refer to each coding condition as a “Starship Rule”.

If you look at a switch statement critically, we could say that it combines several different activities into a single statement:-

  • we instantiate the object
  • we populate it with a list of Starship Rule criteria (“if this boolean criteria is met…”)
  • we populate each Starship Rule with an outcome (“… then do this.”)

Sometimes when reading articles such as this, it can be really easy to lose sight of the actual intent being communicated. Our examples have to be lightweight for brevity and clarity, with simple one-line conditional responses, such as “return a line of text”. If you look at this code literally, you may simply think to yourself “why go to all this extra complexity, just to rewrite such simple code?” - you need to imagine that the real-life code is going to be far more complicated and something that we want to avoid changing. Similarly, even if your code is relatively simple, you may have a suite of unit tests written against it, which you would have to spend time updating.



Introducing change

Now, let’s say that we want to add another Starship Rule by adding another case to our switch. Our code could now look like this:-

...
switch (registryCode)
{
   case "ncc1701":
       return "USS Enterprise (Constitution class)";
   case "ncc2000":
       return "USS Excelsior (Excelsior class)";
   case "ncc1864":
       return "USS Reliant (Miranda class)";
   default:
       return "Registry not recognised";
}
...

… but the moment we add more code, we’ve then modified our original class, which then does not observe the Open/Closed Principle.

The very nature of a switch makes it impossible to add to it without modifying the whole thing.

Often on the internet, you’ll see people say that something “violates” a design principle. I personally prefer not to use this word in the context of ‘principles’, as it implies that you must always conform. These aren’t laws.

Software design principles and patterns are there to help you navigate common problems - they were never intended to be the actual source of over-complication. If you do something dogmatically, it may not be the best choice for your project.



image showing a collection of movies

Write extensible code based upon collections.

What we need here is a way for us to break the code into smaller pieces, meaning that if we need to make a change, we won’t have to modify the entire thing.

We can achieve this by using a collection.

In C# we have the IEnumerable interface that provides us with a number of different types of collection, including the versatile List<T>.

If you’re not familiar with Generics you may not appreciate the significance of the T in the line above, but very briefly, the T is a way to tell us that we can use any type of object in our List. This gives us a tonne of flexibility, including the option to specify an Interface rather than a concrete type - we’ll come back to this a bit later.

Reminding ourselves about the points raised earlier in this article, when we use a collection we can benefit from these improvements; we can now separate:-

  • the code that initialises the collection object itself (the bit that creates a New List() ),
  • the code that adds items (Starship Rules) to the collection (the bit that “registers” the various items),
  • the code that actually invokes the logic (the bit that iterates over the list when needed)

In following this process, we can also now separate each piece of logic into two nicely encapsulated parts:-

  • the code that contains the boolean condition (the bit that determines if a rule has been met)
  • the code that contains the outcome (the bit that defines what to do when a condition is met)

The pivotal change that a collection-based system provides us, is that once we have created the collection and written the code that iterates the items in the collection, we shouldn’t ever need to go back and edit that particular part of the code again.

As we introduce new code, we can now do so in a self-contained manner. The only real change to the existing code being the need to add our new item to the collection.

Iterating a collection is a flexible mechanism that lets us perform various activities, for example:-

  • we could iterate each Starship Rule, invoking a method that performs some sort of evaluation that returns a boolean response. This essentially behaves like a switch statement.
  • alternatively, we could iterate each rule additively combining the effect of each. This approach could be well placed for code that filters a common set of data (e.g. “apply this rule, now apply this one and then this one”). This behaviour is a bit like method-chaining which we covered in another article.



Upgrade the Starship Rules to return a model

For this demonstration, we don’t need our various Starship Rules to do a great deal - we just need them to accept a “starship registry code” as an argument and to then return the name and class of the ship.

Before we progress into the main part of this article, let’s make the response from our code a little more granular. Instead of just returning a plain string, let’s modify things so that we return the following model type.

public class StarshipRuleResponse
{
    public bool isMatchedShip { get; set; }
    public string StarshipName { get; set; }
    public string StarshipClass { get; set; }
}



image showing contract

Enforce structure through interfaces

Usually when working with Lists, you will find yourself working with an array of the same type of object.

However, this is no good here, because we need a way to include a selection of different Starship Rules. Therefore, we need a way to be able to continue working with lists, but to be able to interchange many different versions of the objects that represent our Starship Rules.

For each different Starship Rule to share a common set of features (such as requiring each class to contain a method that has a specific name and response type), we need to define a common “footprint”, in code, so that they can all coexist together in the same collection.

In C# we can use something called an interface for this purpose.

If you’re not familiar with interfaces, very briefly, you can think of them as being a kind of “contract”. They specify features (such as a method name and return-type) that must be included in any class that implements that contract. An interface itself doesn’t contain any logic.

By using an interface, we can then create multiple different versions of a class - or implementations - that all share a common design.

Let’s define an interface for our project. It will look like this:-

public interface IStarshipRule
{
    StarshipRuleResponse GetShipDetails(string registryCode);
}

With an interface added to our code, we need to create a List that contains a collection of different Starship Rules that implement our new interface. We do this by adding a List<IStarshipRule> as a private member of the StarshipLogic class, like this:-

public class StarshipLogic
{
    private List<IStarshipRule> _listStarshipRules;
…



Create the various business rules

We can now produce many separate objects, each of which fulfills the requirements of the above interface, whilst implementing a different set of business-logic rules. In our demonstration we have three such example, but for brevity I’ll just show you one fully complete version below:-

public class EnterpriseRule : IStarshipRule
{
    public StarshipRuleResponse GetShipDetails(string registryCode)
    {
        if (registryCode == "ncc1701")
            return new StarshipRuleResponse
            {
                isMatchedShip = true,
                StarshipName = "USS Enterprise",
                StarshipClass = "Constitution"
            };

        return new StarshipRuleResponse();
    }
}


public class ExcelsiorRule: IStarshipRule
...

public class ReliantRule: IStarshipRule
...

As you can see, once we have written an implementation like this, we could then keep adding additional rules without having to touch the older ones.

If a Starship registry is not matched, we return an uninitialised StarshipRuleResponse model where the isMatchedShip boolean value defaults to false.



Create a new collection and register the new methods

Now that we have created new items, we need to introduce them into the code. There are a number of ways we could do this, but for simplicity I will add them into the constructor of the StarshipLogic class, like this:-


public class StarshipLogic
{
    private List<IStarshipRule> _listStarshipRules;

    public StarshipLogic()
    {
        _listStarshipRules = new List<IStarshipRule>
        {
            new EnterpriseRule(),
            new ExcelsiorRule(),
            new ReliantRule()
        };
    }
…



Rework the GetStarshipNameAndClass method

The final piece of work that we need to do now, is to rework the original GetStarshipNameAndClass method, to replace the switch statement with something that iterates over our new collection.

As part of the code improvements, we will now be using the newly introduced isMatchedShip field. Without this, we would have been reliant on testing for the presence of a populated, or null, string.

The new code looks like this:-

public string GetStarshipNameAndClass(string registryCode)
{
    foreach (var rule in _listStarshipRules)
    {
        StarshipRuleResponse response = rule.GetShipDetails(registryCode);

        if (response.isMatchedShip)
        {
            return $"{response.StarshipName} ({response.StarshipClass} class)";
        }
        else
        {
            continue;
        }
    }

    return "Registry not recognised";
}

The above is another example of code that, in principle, shouldn’t need to be modified further.

It is worth noting that we have used C# string interpolation to recombine the separated ship data into a simple string, which is then returned to the calling code.

Also of note in the above code, I have included an else statement which contains a continue. This is only to make the code’s logic more apparent for the benefit of reading … you could completely remove the else code, because if you don’t hit the isMatched criteria and return a value, then there is already an implied continue.



Conclusion

As a reminder, you can find the entire source code here on GitHub

In this article, we’ve demonstrated one way we could reimplement code that accommodates the Open/Closed Principle.

As we’ve seen, the changes we have made have the side-effect of introducing rather a lot of additional code (we more or less doubled the amount of code). In a larger project, applying this style of programming everywhere could result in a huge sprawl of code.

In your opinion, do you think that this an improvement or not? I personally think it’s only possible to answer using that notorious programming cliche of “it depends”.

I would hope that if nothing else, this example reinforces the message that design principles should be applied thoughtfully and not dogmatically.

A few years ago I encountered a developer saying/joke that goes: “refactor once, fool on you, refactor twice fool on me” (which was a play on the classic proverb “fool me once, shame on you; fool me twice, shame on me”).

Although seemingly a very sensible idea, this notion suggests that you should aim to write all your code thoroughly up-front every time. The instinct of most developers will be “of course you should do this!”. This is fine in an environment where you have lots of time and resources, but may not always be appropriate.

This discussion opens up the question that is “so - when is the right time to apply open/closed principle?”.

Consider these two scenarios:-

  • On one hand, we could say that if you are in the business of producing relatively small or uncomplicated systems (typically to a very tight budget) - or perhaps you know with high levels of certainty, that the scope for change is unlikely - then there is an argument for not using the ideas in this article. This could represent the type of work typical to small digital agencies, where speed of delivery is important.

  • Conversely, with a large enterprise system, you will most likely be able to easily justify expanding additional time/effort up-front, to write code in a way that mitigates against regression or simply lends itself better to expansion.

There is a balance between over-engineering your code, versus technical debt (the concept of taking “the easy option” initially, only to inappropriately have to spend resources correcting something later). However, as professional developers, we also need to remind ourselves that we live in a world where time is money - so whilst many of us will feel inclined to make best-efforts to future proof our code, a commercial reality is that this could be needless.

Therefore, despite having just written an article showing you how to write this type of code, my recommendation would be to generally avoid implementing this, unless you have clear business insight that informs you that your code will need extending. Instead, simply wait until change/extension is actually required. Only then, rewrite your code to be more extensible.

What this means in practice, is that you are then only applying this type of work where it is actually needed, rather than where it is speculatively needed.

Furthermore, if you have a scenario where your code has needed to be extended at least once, there is a much greater probability that you’ll have to repeat this activity yet again in the future.



Further reading

  • This Stack Exchange Article has a useful discussion, with examples, that may help make it clearer what the Open/Closed principle is about.



Appendix

Before you go, I wanted to relate back to an earlier article I wrote regarding C# Method Chaining. In that earlier article, we stepped through an example which filtered a list of strings. At the end of that article, I suggested that C# Method Chaining had its uses, but there may be better ways to address the problem.

Below is the entire listing of another example program, that takes the topic of that earlier article and rewrites it using the ideas in this article:-

using System;
using System.Collections.Generic;
using System.Linq;

namespace MethodChainingRefactor
{
    class Program
    {
        static void Main(string[] args)
        {
            var listWords = new List<string>(new string[] { "this", "is", "a", "sample", "list", "of", "words", "that", "demonstrates", "some", "code" });

            var listFilters = new List<IFilterLogic>
            {
                new FilterOutShortWords(),
                new FilterOnlyWordsContainingC()
            };

            foreach (var filter in listFilters)
            {
                listWords = filter.ApplyFilter(listWords);
            }

            Console.WriteLine(String.Join(", ", listWords.ToArray()));
            Console.ReadKey();
        }
    }


    public interface IFilterLogic
    {
        List<string> ApplyFilter(List<string> source);
    }

    public class FilterOutShortWords : IFilterLogic
    {
        public List<string> ApplyFilter(List<string> source)
        {
            return source.Where(w => w.Length >= 3).ToList();
        }
    }

    public class FilterOnlyWordsContainingC : IFilterLogic
    {
        public List<string> ApplyFilter(List<string> source)
        {
            return source.Where(word => word.ToLower().Contains("c")).ToList();
        }
    }
}



Archives

2019 (22)