SOLID principles in .NET revisited part 3: the Open-Closed principle

Introduction

In the previous post we looked at the letter ‘S’ in SOLID, i.e. the single responsibility principle. We identified the reasons for change in the OnDemandAgentService class and broke out 3 of them into separate classes. The OnDemandAgentService can now concentrate on starting a new on-demand agent and delegate other tasks to specialised services such as EmailService. Also, an additional benefit is that other classes in the assembly can re-use those specialised services so that e.g. logging can be centralised.

In this post we’ll look at the second constituent of the SOLID principles, the letter ‘O’ which stands for the Open-Closed Principle OCP.

Open and closed?

So what’s open and what’s closed according to the OCP? OCP states that classes should be closed for modification but open for extension. Closed for modification?! What?? Yes, that’s correct, a class that is put into production and called upon by several consumer classes in your application should not change its implementation with the exception of correcting bugs. That sounds harsh, right? At least that was my first reaction to this rule. The reason for this rule is that classes that rely on e.g. the logging service will be affected by an implementation change of that service. The clients will depend on the behaviour of the logging service and may be susceptible to modifications. Those clients in turn will be called by other clients which also can be affected by those changes.

So what do we do if we really need to change something in the implementation of the service? We can ensure that future clients will have the opportunity to extend its behaviour without breaking the original implementation. There are multiple ways to achieve this. In this post we’ll look at the inheritance-based solution that I actually wouldn’t recommend you to follow in real code. That might sound counterproductive but there are at least two reasons why I decided to do this:

  • OCP has been often linked with inheritance, i.e. that classes should offer extensibility points through overridable methods. You may still encounter such solutions today and it’s good to be familiar with them so that you can propose a different solution.
  • The more ideal solution, which is based on abstractions would lead us towards other parts of SOLID such as the letters ‘L’ and ‘D’. My goal, however, is to consider each component of SOLID in isolation as much as possible even though they are all interconnected.

We’ll come back to these points later on in the series and propose a better solution than the one we’ll see here.

Extensibility points in LoggingService

Logging is implemented in the FileBasedLoggingService class. It is hard coded to write the log messages to a file on the hard drive. However, there are lots of other ways to implement logging. First off, there are multiple libraries that you can use, e.g. log4net or NLog. Alternatively you can roll your own solution and write log messages to the database – which is not too wise in reality but that’s beside the point. The point is that FileBasedLoggingService offers one and only one way to save log messages. Currently if you want to modify the logging logic you’ll need to overwrite the implementation of the methods in FileBasedLoggingService – and most likely modify the name of the class as well to reflect the changes. However, all that will have an effect on the consuming classes of course. They might not be aware of the changes and the code owners will be looking for the log messages in vain when trying to fix a bug.

The way to indicate overridable methods in C# is through the ‘virtual’ keyword. We’ll also rename FileBasedLoggingService to LoggingService. Here’s the modified logging service class with virtual methods:

public class LoggingService
{
	private readonly string _logFile = @"c:\log\log.txt";

	public virtual void LogInfo(string info)
	{
		File.AppendAllText(_logFile, string.Concat("INFO: ", info));
	}

	public virtual void LogWarning(string warning)
	{
		File.AppendAllText(_logFile, string.Concat("WARNING: ", warning));
	}

	public virtual void LogError(string error)
	{
		File.AppendAllText(_logFile, string.Concat("ERROR: ", error));
	}
}

Then if you’d like to modify the behaviour of the class you can derive from it and override the methods as necessary. Here comes a skeleton example:

public class DatabaseLoggingService : LoggingService
{
	public override void LogInfo(string info)
	{
		//implementation omitted
	}

	public override void LogError(string error)
	{
		//implementation omitted
	}

	public override void LogWarning(string warning)
	{
		//implementation omitted
	}
}

OCP in OnDemandAgentService

The next step is to offer a way to modify the logging mechanism in OnDemandAgentService. It is currently hard coded to LoggingService everywhere. Again, we’ll take a sub-optimal solution but we’ll soon improve it. We’ll add a property in LoggingService which lets us define the implementation from the outside. The property will ensure that we can fall back onto the file based LoggingService in case the private _loggingService field is null:

public class OnDemandAgentService
{
	private LoggingService _loggingService;

	public LoggingService LoggingService 
	{
		get
		{
			if (_loggingService == null)
			{
				_loggingService = new LoggingService();
			}
			return _loggingService;
		}
		set
		{
			if (value != null)
			{
				_loggingService = value;
			}
		}
	}

	public OnDemandAgent StartNewOnDemandMachine()
	{
		AuthorizationService authService = new AuthorizationService();
		EmailService emailService = new EmailService();
		LoggingService.LogInfo("Starting on-demand agent startup logic");
		try
		{
			if (authService.IsAuthorized(Username, Password))
			{
				LoggingService.LogInfo(string.Format("User {0} will attempt to start a new on-demand agent.", Username));
				OnDemandAgent agent = StartNewAmazonServer();
				emailService.SendEmail(string.Format("User {0} has successfully started a machine with ip {1}.", Username, agent.Ip), "admin@mycompany.com", "email.mycompany.com");
				return agent;
			}
			else
			{
				LoggingService.LogWarning(string.Format("User {0} attempted to start a new on-demand agent."));
				throw new UnauthorizedAccessException("Unauthorized access to StartNewOnDemandMachine method.");
			}
		}
		catch (Exception ex)
		{
			LoggingService.LogError("Exception in on-demand agent creation logic");
			throw ex;
		}
	}

	public string Username { get; set; }
	public string Password { get; set; }

	private OnDemandAgent StartNewAmazonServer()
	{
		//Call Amazon API and start a new EC2 instance, implementation omitted
		OnDemandAgent amazonAgent = new OnDemandAgent();
		amazonAgent.Host = "usweav-ec2.mycompany.local";
		amazonAgent.Ip = "54.653.234.23";
		amazonAgent.ImageId = "ami-784930";
		return amazonAgent;
	}
}

We have now modified the original FileBasedLoggingService class to offer extensibility points for future modifications. Also, OnDemandAgentService provides a way to employ a logging mechanism different from the default implementation by a public property. You can imagine that the same technique can be applied to other dependencies like the EmailService and AuthorizationService.

A useful language feature in .NET is the “obsolete” keyword as seen in this post. It can happen that you simply have to modify an existing class which has a lot of clients. OCP states that you shouldn’t modify the implementation of that class because it can have unforeseeable effects on the existing code base. Instead of poking in the original class you can create a brand new one and declare the old one obsolete. Clients will then be advised to use the new, improved implementation. Gradually all usage of the old class can disappear and you’ll be able to delete it. However, this process can take months or even years.

In the next post we’ll take a look at the Liskov Substitution Principle LSP.

View the list of posts on Architecture and Patterns here.

Advertisements

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

3 Responses to SOLID principles in .NET revisited part 3: the Open-Closed principle

  1. Nice post. As always. Looking forward to the rest of the series. Keep up the good work!

  2. Pingback: Architecture and patterns | Michael's Excerpts

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

ultimatemindsettoday

A great WordPress.com site

Elliot Balynn's Blog

A directory of wonderful thoughts

Robin Sedlaczek's Blog

Developer on Microsoft Technologies

HarsH ReaLiTy

A Good Blog is Hard to Find

Softwarearchitektur in der Praxis

Wissenswertes zu Webentwicklung, Domain-Driven Design und Microservices

the software architecture

thoughts, ideas, diagrams,enterprise code, design pattern , solution designs

Technology Talks

on Microsoft technologies, Web, Android and others

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

Anything around ASP.NET MVC,WEB API, WCF, Entity Framework & AngularJS

Cyber Matters

Bite-size insight on Cyber Security for the not too technical.

Guru N Guns's

OneSolution To dOTnET.

Johnny Zraiby

Measuring programming progress by lines of code is like measuring aircraft building progress by weight.

%d bloggers like this: