Single Responsibility Principle
Single Responsibility Principle in C# – SOLID Design Principles – Part 1
Overview
In our introduction to the SOLID Design Principles, we mentioned the Single Responsibility Principle as one of the five principles specified. In this post we are going to dive into this design principle with a very simple example in C#.
The Single Responsibility Principle states that a class should have only one reason for change. The greater the number of responsibilities, the more reasons a class will have for change. Seems pretty simple, right? When you consider it for what it is, it is pretty simple.
I am most definitely NOT a mechanic, and I do not claim to know a great deal about combustion engines, but I think that a subset of an engine’s functionality is a great way to illustrate the SOLID design principles. The engine in your automobile is a marvel of modern engineering and has been designed to function optimally with each component having minimal dependencies on other components. An engine is maintainable because the various parts/components are easily removed and replaced. This is how our applications should be written.
So let’s consider an automobile engine from the standpoint of the starter mechanism. In case you don’t know, your engine has a component called a starter that is attached to the engine, has electrical connections to allow it to draw power from the battery, and when you engage your ignition switch via a key or pushbutton, the starter is energized. When it is energized, it forces the engine to turn over and the combustion process begins. If you would like to learn more about how a starter works, here is a great article π Haha. So let’s move on, shall we?
IMPORTANT: For the sake of simplicity, we are going to assume that for this example, the one hard rule that will not ever change is the fact that there will always be a Starter object and a Battery object associated with an Engine. If we don’t make this assumption and declare it as a “rule”, the scope of our design changes could make the illustration of the concept overly-complicated and I really want to keep it simple here and discuss the principle in the simplest terms possible.
Furthermore we are going to use our design in this post to continue to apply SOLID design principles one by one.
Design #1 – Not so Good
Let’s suppose that we wanted to represent an Engine’s ignition/starter functionality in a few C# classes. If we didn’t understand the Single Responsibility Principle, we might build our classes similarly to this:
Based on the design shown above, let’s consider this code:
public class Engine { public IgnitionResult Start(Starter starter, Battery battery) { //we would put code here to handle the logic for checking //whether or not the batter is charged //then we check the result of our logic if (battery.IsCharged) { //we could put logic here to handle //the actual ignition process return IgnitionResult.Success; } else { //uh oh! the battery is not charged //Failure! return IgnitionResult.Failure; } } } public class Starter { public string Brand { get; set; } public string Model { get; set; } } public class Battery { public string Brand { get; set; } public string Model { get; set; } public bool IsCharged { get; set; } } public enum IgnitionResult { Success, Failure }
Let’s think about this code as it is written. It makes sense that we would have an Engine class, a Starter class, a Battery class, and an IgnitionResult enum. So far so good. But when we look in the Engine class and read the Start() method, we can see that there may be more than one reason why the Engine class would have to be changed. It is responsible for too many things. Any logic associated with how to start the engine is contained within the Start() method as is the “validation” of determining whether or not the battery is charged.
Consider the following questions:
- What if we installed a different type of Battery and the logic associated with verifying its charge state changed?
- What if we installed a different type of Starter and the logic associated with how it actually works internally changed?
If either of these things changed we would have to modify our Engine class to accommodate the change(s). The key point here is that the Engine class has more than one responsibility and per the Single Responsibility Principle this is not good.
Design #2 – Better!
Now let’s reconsider our design, remembering that each class should have not more than one reason for change.
First, the logic for actually handling the Starter’s ignition process should be moved to the Starter class and the Starter itself should contain a Start() method which will be invoked by the Engine’s Start() method.
Next, the battery charge validation logic should be moved to the Battery class since the battery itself knows better than anything how to validate its own state. Sounds sensible, right?
Let’s take a look at the improved design:
So what did we do? First, we removed anything to do with the “internal workings” of the Starter and the Battery out of the Engine class and into each respective class. Now, based on the assumption we made above that stated in this scenario an Engine will always have exactly one Starter and exactly one Battery, the Engine class has only one reason for change as do the Starter and Battery classes.
Keep in mind that as we get into the other SOLID Design Principles we are going to begin abstracting things so that we will have a truly “pluggable” design but for now we are working directly with concrete Starter and Battery objects. That will change as we move through the other principles and we will begin to see continuous improvement!
We left the Brand and Model properties in the Starter and Battery classes. Obviously we can see that this is not an ideal design, but remember – we are focusing on the Single Responsibility Principle for now! These properties are inconsequential now.
Based on the design shown above, our new code looks like this:
public class Engine { public IgnitionResult Start(Starter starter, Battery battery) { return starter.Start(battery); } } public class Starter { public string Brand { get; set; } public string Model { get; set; } public IgnitionResult Start(Battery battery) { //since the Battery class now contains that actual charge validation //logic, the Starter merely checks the value of that property //and the Battery takes care of the rest if (battery.IsCharged) { //we can put the ignition logic here return IgnitionResult.Success; } else { return IgnitionResult.Failure; } } } public class Battery { public string Brand { get; set; } public string Model { get; set; } public bool IsCharged { get { //we can put logic here for the battery //to validate its charge and return //a result return true; } } } public enum IgnitionResult { Success, Failure }
So we have a better design from the standpoint of the Single Responsibility Principle. The goal is to modify all of our classes so that each class has one and only one reason for change. Since the example is very simple, accomplishing this is pretty easy. As designs become more complex, the amount of time to create the correct design can grow tremendously but the time required is very well worth it long-term because it will yield a much more maintainable and effective design overall.
In the next post, we will dive into the Open-Closed Principle. See the links below for all posts related to the SOLID Design Principles.
The SOLID Design Principles
The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)
SOLID Design Principles
In software development, principles differ from patterns in the sense that where patterns represent complete, identifiable, repeatable solutions to common problems, principles are objective, factual statements that can be made about code and the manner in which it is constructed and the overall design of an implementation. In other words, patterns refer to code scenarios while principles refer to qualities of code and these qualities are useful in identifying the value of the code.
In this post, we are going to be introduced to Bob Martin’sΒ SOLID design principles. These principles have been around for a long time, and it is immeasurably important for every object-oriented developer to understand them and use them in making day-to-day design decisions. It is not that uncommon to see developers dive into development tasks by writing code first and considering architecture and design second. A good developer though inverts this scenario and considers a sound design before writing the first line of code! This prevents undue code maintenance pain later and allows for the development of better applications through and through.
Consideration of design principles is extremely important throughout a development effort, and failure to make the proper considerations can have a devastating effect on the development of the application and the application’s usefulness, performance, and maintainability.
What are the SOLID Design Principles?
In this section, we are going to outline and briefly discuss each of the five design principles. In subsequent posts we will dive into each principle in more detail. The five SOLID design principles are listed below:
- The Single Responsibility Principle (SRP) – this principle states that there should never be more than one reason to change a class. This means also that a given class should exist for one and only one purpose.
- The Open-Closed Principle – this principle states that modules should be open for extension but closed for modification. This seems a bit unclear at first until you realize that you can change an object’s behavior by either using abstractions, implementing common interfaces, and from inheritance from common base classes or extending abstract base classes.
- The Liskov Substitution Principle – this principle, introduced by Barbara Liskov, simply-stated means that derivative classes have to be substitutable for their base classes. If we think about this for a minute, we can see that this also means that any class that implements a specific interface can be replaced by any other class that implements that interface. In other words, it can be substituted for the original class. Furthermore, a derived class must honor the ‘contract’ set by its super class.
- The Interface Segregation Principle (ISP) – this principle states that objects should not be forced to implement interfaces that they do not use. Though this may sound obvious based on the wording, in practice it really means that interfaces should be finely-grained and not specific to the classes for which their implementation is intended.
- The Dependency Inversion Principle (DIP) – this principle states that higher-level classes should not depend upon lower-level classes but that both should depend on abstractions. Furthermore, these abstractions should not depend on concrete details but rather on abstractions as well.
Though each principle can be discussed individually, we should understand that no single principle should exist or be applied by itself. They should all be considered as part of the design process. In the following posts, we will dive into each principle in detail and take a look at some simplified code examples that illustrate each one.
The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)