Read time: 9 minutes
1. Introduction
When it comes to software development, there is a lot of debate about the best practices of doing things and if you’ve played in this environment for a while, you’ve probably heard about SOLID. In case you don’t know, it is a mnemonic acronym that states five principles, which have generally been accepted as some of the best practices in the industry. Without further ado, we’re going to jump right into it and try to shed some light on each of these SOLID principles and find out how to use them to develop .NET applications.
2. SOLID Principles
2.1 Single Responsibility
The Single Responsibility Principle states that software entities (classes or methods) should only have one responsibility and one reason to change.
In my opinion, this is the trickiest of the five principles because sometimes it can be challenging to draw the line between responsibilities. The following may not be the best example to highlight this, but hopefully it will at least give you an idea. I will also provide you with some further reading that takes a more in-depth approach.
Let’s consider a simple example where we have an e-commerce application and we want to manage customers, products, orders and so on. Let’s say we want to implement the action of a customer placing an order. In order to achieve this, we would first need a Customer class that manages customers, right? We’ll call this the CustomerService class. Suppose we have this class and now we want to save a customer’s orders in the database.
How do we do this? Let’s consider the following situation:
class CustomerService { public void SaveCustomer() { var customer = new Customer(); customer.Name = "Harleen Quinzel"; customer.Address = "Gotham City"; repository.Save(customer); } public void SaveOrder() { var order = new Order(); order.Customer = new Customer("Bruce Wayne"); order.DateOfPurchase = DateTime.Now; repository.Save(order); } }
We can see here that we have the CustomerService class with two methods: one that saves a customer and one that saves an order into the database. As previously mentioned, a class should only have one responsibility and one reason to change. In this example, one reason to change could be if we decide to add more fields to the Customer like DateOfBirth, Gender, PhoneNumber and so on.
Another reason to change could be if we want to add more to the Order entity (e.g.MethodOfPayment). So we could have two reasons to change the class, which is a violation of the Single Responsibility Principle. A solution could be to encapsulate the SaveOrder() method in a separate OrderService class.
Admittedly, this is a very trivial example and not nearly enough to develop an e-commerce application, but hopefully it sheds some light on what the principle is about. If you want to look at a more complex example, there’s one in this article.
2.2 Open/Closed
The Open/Closed Principle states that software entities should be open for extension but closed for modification. It’s very useful to master this principle because it will greatly reduce the risks of breaking an application’s existing functionality. We achieve this by extending the functionality with new classes instead of modifying existing ones.
Let’s imagine we want to develop a .NET application for a bookstore and estimate the value of the bookstore’s inventory. To do this, we basically need to provide some data regarding average book price and number of units as input and the output would be an estimate of the total worth.
First, we create the model:
class Inventory { public string Genre { get; set; } public double AveragePrice { get; set; } public int Units { get; set; } }
Now we provide some input, process it and return the output:
class BookstoreBad { public void Main() { var mystery = new Inventory { Genre = "Mystery", AveragePrice = 10.50, Units = 100 }; var adventure = new Inventory { Genre = "Adventure", AveragePrice = 8.8, Units = 8 }; var fantasy = new Inventory { Genre = "Fantasy", AveragePrice = 7, Units = 21 }; var inventories = new List<Inventory> { mystery, adventure, fantasy }; var worth = new SalesEstimates().EstimateTotalSales(inventories); Console.WriteLine("Estimated worth is: {0}", worth); } } class SalesEstimates { public double EstimateTotalSales(IEnumerable<Inventory> inventories) { var total = 0D; foreach (var inventory in inventories) { total += inventory.AveragePrice * inventory.Units; } return total; } }
This is the entire application and it works just fine, but imagine if we want to change a few things. Let’s say mystery books are on sale at 25% off and fantasy books are 30% off. In order to implement these changes, we could modify the SalesEstimates class as shown below. We could also modify the Inventory.AveragePrice directly, but this would break the Single Responsibility Priciple.
class SalesEstimates { public double EstimateTotalSales(IEnumerable<Inventory> inventories) { var total = 0D; foreach (var inventory in inventories) { if (inventory.Genre == "Mystery") { total += inventory.AveragePrice * inventory.Units * 0.75; } else if (inventory.Genre == "Fantasy") { total += inventory.AveragePrice * inventory.Units * 0.70; } else { total += inventory.AveragePrice * inventory.Units; } } return total; } }
This again works just fine, but it is not pretty at all. We have modified the SalesEstimates class and we have hard-coded if statements. This is a clear violation of the Open/Closed Principle and it just cries for a better solution. So how do we improve this? Well, the goal here is to not modify the SalesEstimates class every time there is a change (e.g. price changes, adding new books, etc.). Instead, we should make it eligible for extensions.
We can achieve this by making the SalesEstimates class a base abstract class like so:
class BookstoreGood { public void Main() { var mystery = new Inventory { Genre = "Mystery", AveragePrice = 10.50, Units = 100 }; var adventure = new Inventory { Genre = "Adventure", AveragePrice = 8.8, Units = 8 }; var fantasy = new Inventory { Genre = "Fantasy", AveragePrice = 7, Units = 21 }; var mysterySales = new MysterySalesEstimates(mystery); var adventureSales = new AdventureSalesEstimates(adventure); var fantasySales = new FantasySalesEstimates(fantasy); var estimates = new List<BaseSalesEstimates> { mysterySales, adventureSales, fantasySales }; var worth = 0D; foreach (var estimate in estimates) { worth += estimate.EstimateSales(); } Console.WriteLine("Estimated worth is: {0}", worth); } } abstract class BaseSalesEstimates { protected Inventory Inventory { get; private set; } public BaseSalesEstimates(Inventory inventory) { Inventory = inventory; } public abstract double EstimateSales(); } class MysterySalesEstimates : BaseSalesEstimates { public MysterySalesEstimates(Inventory inventory) : base(inventory) { } public override double EstimateSales() { return Inventory.AveragePrice * Inventory.Units * 0.75; } } class AdventureSalesEstimates : BaseSalesEstimates { public AdventureSalesEstimates(Inventory inventory) : base(inventory) { } public override double EstimateSales() { return Inventory.AveragePrice * Inventory.Units; } } class FantasySalesEstimates : BaseSalesEstimates { public FantasySalesEstimates(Inventory inventory) : base(inventory) { } public override double EstimateSales() { return Inventory.AveragePrice * Inventory.Units * 0.70; } }
We can see that this is a much more neat and flexible solution. On a related note, I would like to mention the Decorator Pattern which adheres to the Open/Closed Principle and I encourage you to check out this article, which provides interesting information about the Decorator Pattern and a nice example.
2.3 Liskov Substitution
The Liskov Substitution Principle states that child class objects should be able to replace parent class objects without breaking application integrity. This means that you should pay attention to class hierarchies. I’ll try to illustrate this principle with a simple example.
Let’s assume we have a base Game class and two different games that inherit this base class as shown below:
class Program { static void Main(string[] args) { Game game = new WoW(); game.PaySubscription(); } } class Game { public string Title { get; set; } public DateTime? Release { get; set; } public virtual void PaySubscription() { Console.WriteLine("In order to play this game you have to pay a subscription"); } } class WoW : Game { public override void PaySubscription() { Console.WriteLine("To play WoW you have to pay a subscription"); } } class Solitaire : Game { public override void PaySubscription() { throw new InvalidOperationException("This is not a subscription-based game"); } }
As you can see, we have a very simple example with two lines of code in the Main entry point. The first line creates a Game object of type WoW and the second line calls the PaySubscription() method. Now this works just fine, but the Liskov Principle says that child class objects should be able to replace parent class objects without breaking the application. In this case, the parent class is Game and the child classes are WoW and Solitaire. This means that we should be able to use either WoW or Solitaire without any issues. Unfortunately, if we use Solitaire, it will throw an exception and this violates the principle.
The solution is to add another class for subscription-based games and structure our class hierarchy a bit better as shown below:
class Program { static void Main(string[] args) { Game game = new WoW(); //game.PaySubscription(); game.Title = "WoW"; game.Release = new DateTime(2005, 2, 11); } } class Game { public string Title { get; set; } public DateTime? Release { get; set; } } class SubscriptionGame : Game { public virtual void PaySubscription() { Console.WriteLine("To play WoW you have to pay a subscription"); } } class WoW : SubscriptionGame { public override void PaySubscription() { Console.WriteLine("To play WoW you have to pay a subscription"); } } class Solitaire : Game { }
Now we are able to use either WoW or Solitaire without breaking the application. Notice that we have left out the game.PaySubscription() method because the Game class does not have such a method which is totally fine from the Liskov point of view.
In the context of the Liskov Substitution Principle, you may also hear about covariance and contravariance, which you can read more about here.
2.4 Interface Segregation
The Interface Segregation Principle states that no client should depend on methods it does not use. I think this is the easiest of the five SOLID principles to understand. Essentially it means that you should not have methods in interfaces that are either left empty with no implementation or just throw an exception (when implementing the interface). If this is the case, then we should split the interface into multiple smaller interfaces.
To illustrate this, I am going to use the same example as before but with interfaces.
interface IGame { public string Title { get; set; } public DateTime? Release { get; set; } public void PaySubscription(); } class WoW : IGame { public string Title { get; set; } public DateTime? Release { get; set; } public void PaySubscription() { Console.WriteLine("To play WoW you have to pay a subscription"); } } class Solitaire : IGame { public string Title { get; set; } public DateTime? Release { get; set; } public void PaySubscription() { } }
Notice that in the Solitaire class, the PaySubscription() method is empty. This is because we are forced to implement it, but we don’t really need it. This is a violation of the Interface Segregation Principle and the solution is to define a separate interface for the PaySubscription() method:
interface IGame { public string Title { get; set; } public DateTime? Release { get; set; } } interface ISubscription { public void PaySubscription(); } class WoW : IGame, ISubscription { public string Title { get; set; } public DateTime? Release { get; set; } public void PaySubscription() { Console.WriteLine("To play WoW you have to pay a subscription"); } } class Solitaire : IGame { public string Title { get; set; } public DateTime? Release { get; set; } }
Now we implement ISubscription only where we really need it.
2.5 Dependency Inversion
The Dependency Inversion Principle states the following:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
This means that you should make heavy use of interfaces when applying this principle and I cover this in detail in my other article, which I encourage you to check out.
3. Conclusion
I hope you enjoyed reading this article!
It might seem like there are a lot of different principles, but don’t worry; they are actually related and each one can be understood by looking at relatively simple examples. By mastering the SOLID principles, you will find that your .NET applications become easier to develop and maintain.