CHAPTER 3
We’ve seen the building blocks that make up an object-oriented language, but how can we write our classes so that they make sense? When to inherit, what to put inside a class, how many interfaces do we need? These are questions that can be answered in part by the so-called SOLID principles. SOLID is an acronym and each letter stands for a principle.
In this chapter we’re going to look at all of them. Knowing these principles can help you write modular code that is easy to maintain and expand. The principles are described by Robert C. Martin in his 2006 book Agile Principles, Patterns, and Practices in C#[1].
The Single Responsibility Principle, or SRP, states that a class should be responsible for a single functionality. A functionality can be considered a reason to change. For example, a class that logs errors to a database could change because the database communication should change, or because the format of the log output needs to change. So more generally, a class should have only one reason to change. That’s a little tricky; after all, each line of code could be changed at one time or another. And indeed, I have met programmers who believed a class should contain only a single method. That’s really not what SRP is about though!
Let’s look at a concrete example. Let’s say you’re building a database module. Now you’ve got a class that can establish a connection to the database, get and send data, and finally close the connection. If we defined an interface it could look as follows:
Code Listing 23: A Possible Violation of SRP
public interface IDatabase { void Connect(string connectionString); void Close(); object GetData(); void SendData(object data); } |
So this could really be the interface to your class, right? Unfortunately, this class is doing two things. The class deals with opening and closing connections and with data communication. Here comes the tricky part, and this may or may not be a problem. The question you should ask yourself is this: will the application change in a way that either connections or data communications will change, but not both? This is important, because when the time comes and you realize you need to change one of the two functionalities, the functionality that doesn’t need to change will be in the way. The more functionality that’s in the way, the more chances you have of breaking those parts of the application.
When applying SRP, your classes will be a lot smaller and more maintainable, but you’ll have more classes to deal with. Which would you rather change, a class with 200 lines of code that does multiple things, or a class with 100 lines of code that does only one thing?
As you see, there is no wrong or right in SRP. Unfortunately, it’s a bit of a gut feeling. If you feel a class is to bloated, try identifying different responsibilities and give each their own class.
Without a doubt, one of the hardest principles to grasp and get right is the Open Closed Principle, or OCP. “Open Closed” means that a class should be open for extension, but closed for modification. That means consumers of your class should be able to customize its usage, but without actually changing its code. And unless your coworkers are working in the same solution consumers of your class, even they probably can’t change the source code of your class.
Let’s say you’ve built that database class we just talked about. In a sense it’s already extensible, since everyone can inherit from it and add methods and properties. Doing so will almost certainly result in breaking SRP though, since the core functionality of the class is already defined in the base class and we can’t change that. You could make every method in the base class virtual, and I’ve seen that happen, but that’s not really good practice as consumers may now break your class. For example, if they override Connect and just leave it empty, no connection will be made and each subsequent method call to your class will fail. Besides, even if the method is overridden correctly the base class will now just be sitting there doing nothing, which doesn’t feel right.
I propose a different solution, one of many. Let’s break up the previously proposed interface into two separate interfaces. You’ll probably need some more methods now, but let’s stick to simplicity for now.
Code Listing 24: SRP for OCP
public interface IConnectionManager { void Connect(string connectionString); void Close(); } public interface IDataManager { object GetData(IConnectionManager connManager); void SendData(IConnectionManager connManager, object data); } |
Now suppose I’d like to change the way I connect to the database. I can implement my own IConnectionManager and pass it to my IDataManager. Without touching the original code, I am now able to extend the functionality of the classes without actually breaking the already existing class! Likewise, if I’d like to change the way I’m getting or sending data I can simply implement my own IDataManager and use it with the already existing implementation of IConnectionManager.
It gets better, though. Every class that depends on IConnectionManager and/or IDataManager is now extendable in that it can use any database you can ever imagine! Well, theoretically.
Later we’ll look at Design Patterns, which can help you in designing classes that conform to OCP.
The Liskov Substitution Principle, LSP for short, is about inheritance. It states that subclasses and base classes must be substitutable. That means that any method that consumes an object of any base type must not know if it’s using a base class or any subclass. Here’s an example of what you wouldn’t do (although I must admit I’ve written code just like this in the past):
Code Listing 25: Violation of LSP
bool TestConnection(IConnectionManager connMngr) { if (connMngr is SqlServerConnectionManager) { // Do something... } else if (connMngr is OracleConnectionManager) { // Do something else... } else { // ... } } |
TestConnection is a function that receives an object of type IConnectionManager, which could be any connection manager. In this case, the method checks for the type of connMngr and acts accordingly. That means that for every new implementation of IConnectionManager, you need to change this method. Furthermore, you’ve just violated OCP as this function is now unable to test new connection types. There’s plenty of ways to fix this, like using a Strategy Pattern, but we’ll get to that in a later chapter. In the above method it should always be good enough to invoke the Connect() or Close() method, no matter what IConnectionManager you get.
A more subtle violation of LSP is when subclasses behave differently than their base classes. The classic example is that of a Square class that inherits from a Rectangle class. A square is a rectangle (with equal width and height), so this inheritance looks plausible. Unfortunately, in some scenarios a square may behave differently than a rectangle and may break users of the Rectangle class and get a Square instead.
Code Listing 26: Breaks with Square
void TestSquareMeters(Rectangle rect) { rect.Height = 2; rect.Width = 3; Assert(rect.Height * rect.Width == 6); } |
The Interface Segregation Principle, or ISP, is kind of like the Single Responsibility Principle for Interfaces. Strictly speaking, we’re not talking about the abstract-class-like Interfaces, but your class’ public methods. In C# practice, though, we’re doing ISP mostly through Interfaces. Let me clear that up. Let’s consider our database example for a second. We had the following interface:
Code Listing 27: An IDatabase Interface
public interface IDatabase { void Connect(string connectionString); void Close(); object GetData(); void SendData(object data); } |
As I mentioned in the bit about SRP, this may or may not be SRP-proof, depending on your requirements. Let’s say it is okay for our example, but let’s take another approach, that of inheritance. More specifically, I’m going to create a base class for connection management and inherit that for our data management.
Code Listing 28: DatabaseManager Using Inheritance
public class ConnectionManager { public void Connect(string connectionString) { // ... } public void Close() { // ... } } public class DataManager : ConnectionManager { public virtual object GetData() { // ... } public virtual void SendData(object data) { // ... } } public class DatabaseManager : DataManager { // Needs ConnectionManager... } |
So our DatabaseManager is inheriting ConnectionManager, through DataManager, to make use of, of course, its base implementation, but also to make use of multipurpose functions such as TestConnection defined earlier. Can we be sure that all our data managers need connection managers though? Sure, the DatabaseManager needs it, but what about our FileDataManager?
Code Listing 29: FileDataManager
public class FileDataManager : DataManager { // Doesn't need ConnectionManager... } |
Now our FileDataManager has a fat interface. It depends on classes it doesn’t need! Notice that this would not have happened if we just created two Interfaces and implemented those instead. We could even use composition to re-use the ConnectionManager in other Data(base)Managers. The downside to this is that FileDataManager now depends upon ConnectionManager, even though it doesn’t need it. Any change to ConnectionManager may now break FileDataManager though! Such couplings between classes should be avoided where possible.
The Dependency Inversion Principle, DIP for short, simply states that you should depend on abstractions instead of on concrete types. This is one principle you’re going to see a lot but which is pretty hard to put into practice (correctly). Let’s consider our TestConnection method again. We could create one for our SqlDatabaseManager, as in the following example:
Code Listing 30: A Violation of DIP
class Program { //... bool TestConnection(SqlConnectionManager connMngr) { // ... } } public interface IConnectionManager { void Close(); void Connect(string connectionString); } public class SqlConnectionManager : IConnectionManager { // ... } |
It should be obvious that TestConnection can now only be used for SqlConnectionManager. We have created two problems. First, TestConnection, and Program with it, now depend on SqlConnectionManager. Second, we’ll have to write a TestConnection for each ConnectionManager we’re going to write. We’re saying that SqlConnectionManager is a concrete type of the abstract type IConnectionManager. So let’s fix that so that we’re relying on abstract types compliant to DIP.
Code Listing 31: Fixed for DIP
bool TestConnection(IConnectionManager connMngr) { // ... } |
You may think that’s a pretty lame solution, and you may even say this will break your existing code because TestConnection relies on methods that aren’t part of the IConnectionManager Interface. If that's the case, you should ask yourself three things. Is TestConnection implemented correctly, and does it really only test connections? Is my SqlConnectionManager really an IConnectionManager? Finally, is IConnectionManager defined correctly?
Now why is this principle so important? It has everything to do with expanding and changing existing systems. If you depend on abstractions, it’s easier to switch implementations. In theory you could switch your database (or at least the connection manager) and TestConnection would still behave as expected. We’ve seen loggers earlier, depend on ILogger and you can switch from file logging to database logging without problems!
As I said you’re going to see DIP a lot. Perhaps you’ve heard of Dependency Injection (DI) before? There’s plenty of “DI Frameworks” that help you put DIP into practice. The previous example was as simple as changing our method signature to using an Interface instead of a concrete Type. But any method still gets a concrete type at runtime, so where does that come from? We’ll have to break this “all abstract type” principle somewhere! And that’s where DI comes in.
For the next example I’m going to use Unity, a DI library developed by Microsoft and now maintained by other people. You can get it using the NuGet Package Manager in Visual Studio. Just go to Tools > NuGet Package Manager > Manage NuGet Packages for Solution and search for “Unity.” Install it in your (saved) project and you’re good to go.
A “DI Container,” which Unity provides, is a sort of repository where abstract types are mapped to concrete types. Any code can then ask that repository for an instance of an abstract type and the repository will return whatever concrete type you’ve mapped to it. Your entire code base, save for the part(s) where you map your types, can now depend upon abstractions. Sounds pretty neat, so let’s have a look at some code!
I’ve slightly modified the IConnectionManager example from earlier:
Code Listing 32: IConnectionManagers
public interface IConnectionManager { void Close(); void Connect(); } public class SqlConnectionManager : IConnectionManager { public void Close() { Console.WriteLine("Closed SQL Server connection..."); } public void Connect() { Console.WriteLine("Connected to SQL Server!"); } } public class OracleConnectionManager : IConnectionManager { public void Close() { Console.WriteLine("Closed Oracle connection..."); } public void Connect() { Console.WriteLine("Connected to Oracle!"); } } |
Here’s the definition of TestConnection:
Code Listing 33: Definition for TestConnection
static bool TestConnection(IConnectionManager connMngr) { connMngr.Connect(); connMngr.Close(); return true; } |
Using Unity, we can register either SqlConnectionManager or OracleConnectionManager with IConnectionManager:
Code Listing 34: Registering a Type with Unity
class Program { static void Main(string[] args) { // Create a new DI Container... IUnityContainer container = new UnityContainer(); // ...And register a type! container.RegisterType<IConnectionManager, SqlConnectionManager>(); } } |
And elsewhere in the code we can now request an IConnectionManager and call TestConnection:
Code Listing 35: Resolving a Type with Unity
IConnectionManager connMngr = container.Resolve<IConnectionManager>(); bool success = TestConnection(connMngr); // Do something with the result... |
You should now see “Connected to SQL Server!” and “Closed SQL Server connection…” in your console window. Now change the call to RegisterType so it registers as OracleConnectionManager. The TestConnection will now print Oracle instead of SQL Server. Imagine: by changing this one line of code you could change an entire application to use Oracle instead of SQL Server!
Now suppose you have some dynamic user interface and the controls that are shown on screen depend on some settings and/or configuration. Let’s say we have some HTML form defined in the database, and we wish to generate the HTML server side. I’m going to let you figure this one out on yourself, but I’m pretty sure you’ll get it.
Code Listing 36: HTML Generator
class Program { static void Main(string[] args) { IUnityContainer container = new UnityContainer(); container.RegisterType<IHtmlCreator, HtmlInputTextCreator>("text"); container.RegisterType<IHtmlCreator, HtmlInputCheckboxCreator>("checkbox"); var someDbFields = new[] { new { Text = "Please enter your name:", Type = "text" }, new { Text = "Do you wish to receive offers?", Type = "checkbox" } }; // Generate the HTML... StringBuilder builder = new StringBuilder(); foreach (var dbField in someDbFields) { builder.AppendLine($"<p>{dbField.Text}</p>"); IHtmlCreator html = container.Resolve<IHtmlCreator>(dbField.Type); builder.AppendLine(html.CreateHtml()); } Console.WriteLine(builder.ToString()); Console.ReadKey(); } } public interface IHtmlCreator { string CreateHtml(); } public class HtmlInputTextCreator : IHtmlCreator { public string CreateHtml() { return "<input type=\"text\" />"; } } public class HtmlInputCheckboxCreator : IHtmlCreator { public string CreateHtml() { return "<input type=\"checkbox\" />"; } } |
See how that just saved you an unwieldy switch/if statement? And the best part is that you can add new IHtmlCreators and types without changing the code that generates the HTML! So you’ve just created OCP and DIP compliant code (except for the <p> tags, but you can figure that out)!
This isn’t a tutorial on using Unity, but it does show the basic principle of DIP and DI containers. The sample in this section demonstrated what is called Interface Injection, but it is also possible to create objects using a specified constructor, called Constructor Injection, and automatically set objects on properties, called Property, or Setter, Injection. Other DI Frameworks exist, such as Ninject and Spring.NET. In this example we’ve written code to setup our container, but other frameworks also work with (XML) config files. All of them work with the same principle though, and depend upon abstractions.
Often mentioned together with DI is Inversion of Control, or IoC. You may even see the terms being used interchangeably. The basic principle of IoC is that “higher” code depends upon abstractions. We have already seen this in action with TestConnection. Here’s another fun fact: DI is actually a form of IoC (not necessarily the other way around though)!
Generally speaking, the SOLID Principles can lead to good, maintainable, extensible, and testable software design at the expense of additional complexity. However, they do not come naturally to a lot of developers. Let’s be honest, it’s hard to think ahead, so if you need a SqlConnectionManager now, why bother with some IConnectionManager that may need to be modified later anyway? It’s a lot easier to simply create a SqlConnectionManager and use it throughout your code. However, you cannot predict the future and, even though you think you’ll never switch to something other than SQL Server now, the future may prove otherwise. When you’ll have to switch to something else, you will wish you had practiced the Dependency Inversion Principle. Likewise, when your API that had always been internal suddenly has to go public, you will wish you had practiced the Open Closed Principle. And that stuff does happen. I’ve heard “that will never happen” a few times too many, so really, think ahead and practice SOLID!