SOLID principles in .NET revisited part 4: the Liskov Substitution Principle

Introduction

In the previous post we looked at the Open-Closed Principle, i.e. ‘O’ in the SOLID acronym. We saw how the OCP facilitated the flexibility and extensibility of a class. It also places a constraint on a class by making it “append-only”. In other words you can extend a class but you cannot change the implementation of existing parts of a class.

In this post we’ll take a look at letter ‘L’ which stands for the Liskov Substitution Principle LSP.

Definition of LSP

LSP stands for the interchangeability of different implementations of an abstraction. An abstraction in C#, and probably most other popular object-oriented programming languages, can either be an abstract base class or an interface. According to LSP it shouldn’t make any difference what implementation of an abstraction you call from a client. Any concrete implementation of the abstraction should behave in a way that doesn’t break the client and doesn’t produce any unexpected and/or incorrect results. The client shouldn’t ever be concerned with the implementation details of an abstraction. It should be able to consume any concrete implementation “without batting an eye”.

Changing the requirements

For a demo we’ll extend the requirements of OnDemandAgentService. Currently it is designed to start Amazon cloud-based servers. However, there are many actors in the cloud business nowadays: Azure, Google, Rackspace etc. The consumers of OnDemandAgentService now want to be able to start Rackspace and Azure agents as well.

Let’s see the first iteration of a solution. We’ll collect the possible cloud-based providers in an enumeration:

public enum CloudProvider
{
	Amazon
	, Azure
	, Rackspace
}

We then modify the private StartNewAmazonServer to accommodate the vendor-specific server start-up logic:

private OnDemandAgent StartNewCloudServer(CloudProvider cloudProvider)
{
	//Call Amazon API and start a new EC2 instance, implementation omitted
	string ip = string.Empty;
	string host = string.Empty;
	string imageId = string.Empty;
	switch (cloudProvider)
	{
		case CloudProvider.Amazon:
			ip = "43.65.23.654";
			host = string.Concat("aws-", ip.Replace(".", "-"), ".mycompany.local");
			imageId = "ami-784930";
			break;
		case CloudProvider.Azure:
			ip = "75.234.5.34";
			host = string.Concat("azr-", ip.Replace(".", "-"), ".mycompany.local");
			imageId = "azr-dfgd-786";
			break;
		case CloudProvider.Rackspace:
			ip = "97.34.2.4";
			host = string.Concat("rsp-", ip.Replace(".", "-"), ".mycompany.local");
			imageId = "rsp-fghwef-sdfwe";
			break;
	}
	OnDemandAgent amazonAgent = new OnDemandAgent();
	amazonAgent.Host = host;
	amazonAgent.Ip = ip;
	amazonAgent.ImageId = imageId;
	return amazonAgent;
}

We now supply the StartNewAmazonServer with a parameter of type CloudProvider and build a switch-block to create the server accordingly. Note that the ip, host and imageId fields are specific to each provider.

The change breaks the implementation of OnDemandAgentService.StartNewOnDemandMachine so we need to change that method so that we have a good test case. I’ll only copy the changes here:

public OnDemandAgent StartNewOnDemandMachine(CloudProvider cloudProvider)
{
.
.
.
      OnDemandAgent agent = StartNewCloudServer(cloudProvider);
.
.
.
}

The code works and everyone is happy. That’s until someone wants to add yet more cloud providers, such as Google. You’ll need to extend the CloudProvider enumeration and the switch-based logic within the StartNewCloudServer method. You’ll need to add yet more provider-specific code to an ever-growing method. It will quickly lead to a monolithic method implementation that tries to take care of all thinkable cloud provider specific API logic. On the one hand we could argue that it’s fine. After all we identified a single reason to change for the OnDemandAgentService, right? And that was the cloud server creation logic. However, I think we’ve gone too far. The OnDemandAgentService is now responsible for 3 different ways of starting up a cloud based server and that list can grow more in the future.

Instead, we’ll break out the different cloud provider logic into their own classes like we did with e.g. LoggingService.

Refactoring iteration 1

An on demand agent has 3 properties: IP, host and image ID. Each new server will have a different IP but the host name can be derived from that depending on the IP. The image ID, which is the ID of the image that the server instance is based on will be constant. So at first we’ll let the cloud providers return the IP addresses only:

public class AzureCloudProvider
{
	public string StartServer()
	{
		return "75.234.5.34";
	}
}

public class AmazonCloudProvider
{
	public string StartServer()
	{
		return "43.65.23.654";
	}
}

public class RackspaceCloudProvider
{
	public string StartServer()
	{
		return "97.34.2.4";
	}
}

The switch-block can delegate the machine creation to the concrete classes:

case CloudProvider.Amazon:
	ip = new AmazonCloudProvider().StartServer();
	host = string.Concat("aws-", ip.Replace(".", "-"), ".mycompany.local");
	imageId = "ami-784930";
	break;
case CloudProvider.Azure:
	ip = new AzureCloudProvider().StartServer();
	host = string.Concat("azr-", ip.Replace(".", "-"), ".mycompany.local");
	imageId = "azr-dfgd-786";
	break;
case CloudProvider.Rackspace:
	ip = new RackspaceCloudProvider().StartServer();
	host = string.Concat("rsp-", ip.Replace(".", "-"), ".mycompany.local");
	imageId = "rsp-fghwef-sdfwe";
	break;

Have we gained anything? At first sight not really, but we’re on the right track. We have at least started delegating the tasks to specialised classes.

Refactoring iteration 2

We then realise that all the cloud providers can be generalised into either an abstract base class or an interface. They all have a single method with the same method signature. This sounds like a perfect case for an abstraction according to good OOP design. Let’s go for the following interface:

public interface ICloudProvider
{
	string StartServer();
}

All 3 cloud provider implementations can implement the interface:

public class AzureCloudProvider : ICloudProvider
public class AmazonCloudProvider : ICloudProvider
public class RackspaceCloudProvider : ICloudProvider

We’ll add a public property in OnDemandAgentService so that the clients can specify which cloud provider they would like to use:

private ICloudProvider _cloudProvider;

public ICloudProvider CloudProvider
{
	get
	{
		if (_cloudProvider == null)
		{
			_cloudProvider = new AmazonCloudProvider();
		}
		return _cloudProvider;
	}
	set
	{
		if (value != null)
		{
			_cloudProvider = value;
		}
	}
}

This is the same technique we saw in the post on OCP in the case of the LoggingService property. I want to remind you that this is sub-optimal code and we’ll come to an improved version later on in the series.

We can now get rid of the enumeration:

public OnDemandAgent StartNewOnDemandMachine()
{
...
     OnDemandAgent agent = StartNewCloudServer();
}

We can now forget the switch-block within the StartNewCloudServer method. However, as the host and imageId properties depend on the cloud provider type we’ll need to query for the actual implementation of ICloudProvider as follows:

private OnDemandAgent StartNewCloudServer()
{
	//Call Amazon API and start a new EC2 instance, implementation omitted
	string ip = CloudProvider.StartServer();
	string host = string.Empty;
	string imageId = string.Empty;
	if (CloudProvider is AmazonCloudProvider)
	{
		host = string.Concat("aws-", ip.Replace(".", "-"), ".mycompany.local");
		imageId = "ami-784930";
	}
	else if (CloudProvider is AzureCloudProvider)
	{
		host = string.Concat("azr-", ip.Replace(".", "-"), ".mycompany.local");
		imageId = "azr-dfgd-786";
	}
	else if (CloudProvider is RackspaceCloudProvider)
	{
		host = string.Concat("rsp-", ip.Replace(".", "-"), ".mycompany.local");
		imageId = "rsp-fghwef-sdfwe";
	}
			
	OnDemandAgent amazonAgent = new OnDemandAgent();
	amazonAgent.Host = host;
	amazonAgent.Ip = ip;
	amazonAgent.ImageId = imageId;
	return amazonAgent;
}

Have we gained anything? A little bit at least, yes. The callers can define the server creation implementation through a public property. However, the type checking fulfills the same function as the switch-block above. OnDemandAgentService still needs to concern itself with implementation details of each server creation type. We still violate LSP as OnDemandAgentService cannot just call upon an ICloudProvider to perform its main task. It only works partly in that we can extract the IP from the StartServer method.

Refactoring iteration 3

The solution is to move all provider-specific logic under the concrete implementation. We return an OnDemandAgent object from the StartNewCloudServer method. We’ll need to adjust the ICloudProvider interface accordingly:

public interface ICloudProvider
{
	OnDemandAgent StartServer();
}

Here come the updated implementations:

public class RackspaceCloudProvider : ICloudProvider
{
	public OnDemandAgent StartServer()
	{
		string ip = "97.34.2.4";
		string host = string.Concat("rsp-", ip.Replace(".", "-"), ".mycompany.local");
		string imageId = "rsp-fghwef-sdfwe";
		return new OnDemandAgent() { Host = host, ImageId = imageId, Ip = ip };
	}
}

public class AzureCloudProvider : ICloudProvider
{
	public OnDemandAgent StartServer()
	{
		string ip = "75.234.5.34";
		string host = string.Concat("azr-", ip.Replace(".", "-"), ".mycompany.local");
		string imageId = "azr-dfgd-786";
		return new OnDemandAgent() { Host = host, ImageId = imageId, Ip = ip };
	}
}

public class AmazonCloudProvider : ICloudProvider
{
	public OnDemandAgent StartServer()
	{
		string ip = "43.65.23.654";
		string host = string.Concat("aws-", ip.Replace(".", "-"), ".mycompany.local");
		string imageId = "ami-784930";
		return new OnDemandAgent() { Host = host, ImageId = imageId, Ip = ip };
	}
}

We can eliminate the private StartNewCloudServer method from OnDemandAgentService altogether. We can call upon ICloudProvider.StartNewServer directly from the public StartNewOnDemandMachine method:

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 = CloudProvider.StartServer();
			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;
	}
}

OnDemandAgentService can now fully forget the concrete implementation of the ICloudProvider interface. It can safely just call ICloudProvider.StartNewServer without the need of funny switch-blocks and type checking.

Switch-blocks and type checking if-else statements are often good indicators of LSP violations in your code. Switch-blocks can be replaced with other if-else statements that check for magic strings, like…

if (provider == "amazon")
{
}
else if (provider == "azure")
{
}

That’s not very fortunate in a service class like OnDemandAgentService either.

In the next post we’ll discuss another case for 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.

One Response to SOLID principles in .NET revisited part 4: the Liskov Substitution Principle

  1. 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: