The open closed principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.  For the sake of this post I will use the term “server” to describe a class in an API – a business object for example.  “Client” will be used to describe the consumer of a service – it could be a user interface component for example.  The client has a dependency on a server at some level, the server knows nothing about a client.

When considering the open closed principle I think it is interesting to consider the original definition.  According to Wikipedia, Bertrand Meyer originated the principle in 1988 and I’ll paraphrase his definition as stating that once a class is completed it could only be modified to fix errors.  If you wanted to add any new features or change the behavior in any way you had to create a new class.  Inheritance may be used to reuse code from the original class but the resulting interface of the new class need not be the same as the interface of the original class.  Consider client class A that depends on server class B.  Once A and B are working they would never change.  Now we create server C which is an extension of B (reuses implementation but may have a different interface).  We have extended the API by creating server C but we did not change A or B so A continues to behave correctly.  The focus here is on how we can extend our API but keep clients working by not changing any of the working code.

According to Wikipedia, Robert Martin is credited with popularizing the more modern application of the open closed principle which advocates the use of abstract interfaces.  With this approach, server implementations can be changed, and multiple server implementations can be created and substituted for each other in the client.  Again consider client class A that depends on server class B but this time B is an abstract class.  Now we create server class C which is a derivation of B.  Since client A is dependent on an abstraction, we can extend the client by passing it C without changing A.  The focus here is on opening the client to extension but closing the client to modification.

Meyer’s approach is better in that it shouldn’t be possible to break working client A by adding server derivation C.  The down side, however, is that our answer to all change is to add more code, client A will not be able to make use of server extension C, and refactoring of B is not allowed.  With Martin’s approach we are essentially saying “as long as we respect the interface, never mind what I do to the server”… a pretty bold statement.  The down side here is that it is easy to imagine breaking client A by adding a poorly designed server derivation C or even by carelessly refactoring B and inadvertently adding a bug.  See my blog on the Liskov Substitution Principle for some examples of bad derivations.  The upside is that with this approach, client A may be extended to make use of server C  without changing the client and since we have the freedom to refactor our servers we should have less code, we can make use of new language features where it makes sense, and we can reorganize things as our API evolves.  In short the resulting code should be easier to maintain.  If we do not have good unit tests in place I would suggest that Meyer’s approach is the better way to go but if we do have a good suite of unit tests in place I am going to favor Martin’s approach

We are not going to look at Meyer’s application of the principle as it is straight forward and intuitive but let’s look at some violations of Martin’s application of the principle.  Consider the following code:

namespace Solid.OCP.Violation
{
    public class DbService
    {
        public bool Create()
        {
            throw new NotImplementedException();
        }

        public int Update()
        {
            throw new NotImplementedException();
        }

        public List<string> Select()
        {
            throw new NotImplementedException();
        }

        public int Delete()
        {
            throw new NotImplementedException();
        }
    }
}

Conceptually DbService is a part of an API responsible for persisting something to a database.  The method implementation is not important, only the shape.  It is important to note that DbService does not make use of any sort of abstraction.

Imagine now that after some time we decide we also want to use a NoSql structure to store some of our data, we might create the following server:

namespace Solid.OCP.Violation
{
    public class AzureTableService
    {
        //Assume Azure Table Storage is NOT updatable
        //which is not true

        public bool Create()
        {
            throw new NotImplementedException();
        }

        public List<string> Select()
        {
            throw new NotImplementedException();
        }

        public int Delete()
        {
            throw new NotImplementedException();
        }
    }
}

Again, the method implementation is not important.  The shape of the class is similar to DbService but there is no use of abstraction.

If we need to create a client that works with both services it might look like this:

namespace Solid.OCP.Violation
{
    public class Client
    {

        public void Save(List<object> persitables)
        {

            foreach (var item in persitables)
            {
                if(item is AzureTableService)
                {
                    AzureSave((AzureTableService) item);
                }
                else if(item is DbService)
                {
                    DbSave((DbService)item);
                }
                LogMessage("Somebody updated something");
            }
        }
            
        private void DbSave(DbService container)
        {

            if(container.Select().Count >0 )
            {
                container.Update();
            }
            else
            {
                container.Create();
            }

        }

        private void AzureSave(AzureTableService container)
        {

            if (container.Select().Count > 0)
            {
                container.Delete();
            }

                container.Create();

        }

        private void LogMessage(string msg)
        {
            //Log an audit record
        }
    }
}

Consider the Save method.  Conceptually we want to save a list of “things”, if the object doesn’t exist in the storage device we should create it otherwise we want to update it (this is called an upsert).  Since we didn’t use abstraction, the list of things must be of type “object”.  Next we iterate through the list and inspect each object then we have to run code specific to the type of object to accomplish the save.  There are many problems with this.  First if an object other than AzuerTableService or DbService is in the list we are going to blindly do nothing – the alternative would be to throw an exception but neither behavior is good.  Second, the client must implement a specialized method for each type of server. Finally, if we want to add a new server to the mix we have to crack open the client, modify the if/then or maybe add a switch as the number of different Servers we support grows and we also have to add the specialized method mentioned above for each Server.

Now consider a design where we make use of an abstraction.  First we will define an abstraction called IUpsertable

namespace Solid.OCP.Better
{
    //************ Put this in a seperate assembly ************


    //Create an abstraction for the services
   public  interface IUpsertable
    {
        bool Create();
        List<String> Select();
        int Upsert();

    }
}

We are using an interface, it could also have been an abstract class if there was some shared implementation.

The key now is that each of our servers is going to implement this interface:

namespace Solid.OCP.Better
{
    //************ Put this in a seperate assembly ************

    public class DbService:IUpsertable
    {
        public bool Create()
        {
            throw new NotImplementedException();
        }

        public int Update()
        {
            throw new NotImplementedException();
        }

        public List<string> Select()
        {
            throw new NotImplementedException();
        }

        public int Delete()
        {
            throw new NotImplementedException();
        }


        public int Upsert()
        {
            
            if (this.Select().Count() > 0)
            {
                return this.Update();
            }
            this.Create();
            return 1;
        }
    }
}

 

namespace Solid.OCP.Better
{
    //************ Put this in a seperate assembly ************

    public class AzureTableService : IUpsertable
    {
        public bool Create()
        {
            throw new NotImplementedException();
        }

        public int Upsert()
        {
            int count = 1;
            if (this.Select().Count()>0)
            {
                count = this.Delete();
            }
            this.Create();
            return count;
        }

        public List<string> Select()
        {
            throw new NotImplementedException();
        }

        public int Delete()
        {
            throw new NotImplementedException();
        }

    }
}

And here is how our new client may look:

namespace Solid.OCP.Better
{
    public class Client
    {
        public void Save(List<IUpsertable> persitables)
        {

            foreach (var item in persitables)
            {
                item.Upsert();
                LogMessage("Somebody updated something");
            }


        }
        private void LogMessage(string msg)
        {
            //Log an audit record
        }

    }
}

We have made a couple key design decisions here.  First we decided that the DbService and AzureTableService should be responsible for implementing an Upsert method and thus removed the burden of adding a server specific method in our client.  Second, by having the Save method use a parameter of type List<IUpsertable> there is no danger of getting any type of object into the method that does not implement the IUpsertable interface.  Finally, the Save method no longer needs to know anything about the concrete types that are being passed in, it may simply iterate the list and call Upsert on each object in the list.

By using an abstraction in our Save method we can add any number of servers to the system and have the client use them without ever having to modify the Client.  This discussion has been limited to class design but we should mentally go through this exercise with each method as well.  In the Client above our Save method requires a List<IUpsertable> parameter to be passed.  In our implementation we are doing nothing to alter the contents of the list so an even better design would be to define the save method as follows:

        public void Save(IEnumerable<IUpsertable> persitables)

By defining the parameter as List<T> we limited the possible consumers of this client to consumers that had a List of IUpsertable objects.  Modifying the parameter to IEnumerable<IUpsertable> opens up our potential clients to those that have a List, Array or any other structure that Implements the IEnumerable interface.