SOLID principles in .NET revisited part 2: Single Responsibility Principle
April 27, 2015 3 Comments
Introduction
In the previous post we set the tone for the recycled series on SOLID. We also presented a piece of code to be improved. There are several issues with this code and we’ll try to improve it step by step as we go through the principles behind SOLID.
In this post we’ll start with ‘S’, i.e. the Single Responsibility Principle (SRP). We’ll partly improve the code by applying SRP to it. Note that the code will still have a lot of faults left after this but we’ll improve it gradually.
What is SRP?
The goals of SRP can be expressed in several ways that all boil down to the same bottom line. A method or a class should concentrate on one thing and perform that thing only. It’s easily conceivable that a method will need to carry out multiple actions to perform its goals. However, all tasks not strictly related to the final goal of the method should be delegated to other components which in turn can concentrate on some specific responsibility.
Another way of stating the main goal of SRP is that every object should have only one reason to change. Let’s try to identify places in our demo starting point code that can change in the future:
- Logging: right now we perform file-based logging directly within OnDemandAgentService in private methods. What if we want to log to the database instead? We’ll need to revisit OnDemandAgentService and change the logging logic which actually has nothing to do with the main purpose of the class, i.e. to construct an on-demand cloud-based server
- Authorization: the authorization code is also embedded in a private method. We have the same issue here as above. What if we want to modify the implementation and check the user’s credentials in the DB? Again, we’ll have to revisit the IsAuthorized which doesn’t have much to do with the purpose of the OnDemandAgentService.StartNewOnDemandMachine method
- Emailing: there’s built-in support for emailing in .NET and it’s unlikely that you’ll go for another implementation for sending emails. However, the implementation details, such as the recipient and email host can change which will lead to the same issue as above
- Server creation: server creation implementation details can change quite often in line with updates in the cloud service, the service API endpoints or details like the required arguments such as the image ID. For all those changes we’ll need to revisit the OnDemandAgentService class
So there are 4 major reasons to revisit the OnDemandAgentService class in the future. Of those 4 it’s only the last one which is legitimate, i.e. the server creation logic. OnDemandAgentService is responsible for handling on-demand servers directly. Such a class design is monolithic. It tries to control too many aspects at once. The current state of the OnDemandAgentService is the software equivalent of a control-freak boss who wants to carry out the tasks of his/her subordinates. The OnDemandAgentService class is bloated with areas that it shouldn’t be responsible for, or at least not directly in the manner it is currently implemented.
Refactoring for SRP
Logging, authorization and emailing should be delegated to other classes. Let’s see what a logging class could look like:
public class FileBasedLoggingService { private readonly string _logFile = @"c:\log\log.txt"; public void LogInfo(string info) { File.AppendAllText(_logFile, string.Concat("INFO: ", info)); } public void LogWarning(string warning) { File.AppendAllText(_logFile, string.Concat("WARNING: ", warning)); } public void LogError(string error) { File.AppendAllText(_logFile, string.Concat("ERROR: ", error)); } }
You’ll recognise this code I presume. Let’s also create separate classes for authorization and emailing:
public class EmailService { public void SendEmail(string message, string recipient, string emailHost) { //implementation ignored } }
public class AuthorizationService { public bool IsAuthorized(string username, string password) { return (username == "admin" && password == "passw0rd"); } }
Let’s see who these services can be called from OnDemandAgentService:
public class OnDemandAgentService { public OnDemandAgent StartNewOnDemandMachine() { FileBasedLoggingService loggingService = new FileBasedLoggingService(); 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; } }
So now all logic that’s not strictly part of OnDemandAgentService has been “outsourced” or delegated to other objects that are designed to concentrate on their own fields of responsibility: emailing, authorization and logging. OnDemandAgentService is now directly responsible for the on-demand agent creation.
Keep in mind that at this point there’s still a lot of refactoring left even on this small code base. However, we’re on the correct path. We already have at least one additional benefit out of this exercise. Logging, emailing and authorization logic are not hidden within OnDemandAgentService in private methods invisible to other parts of the application. So what if an unrelated class, such as a CustomerService also wants to do logging and emailing? It will need to reinvent the wheel and perform those actions possibly through duplicated code. Now that we have those dedicated classes other parts of the application can also make use of them. In other words we have created some re-usable code.
SRP is strongly related to what is called Separation of Concerns (SoC). SoC means breaking up a piece of code into distinct features that encapsulate unique behaviour and data that can be used by other classes. Here the term ‘concern’ represents a feature or behaviour of a class. Separating a program into small and discrete ‘ingredients’ significantly increases code reuse, maintenance and testability. We have now separated out the different fields of responsibility into dedicated objects.
In the next post we’ll continue our improvements with the letter ‘O’, i.e. the Open-Closed Principle (OCP).
View the list of posts on Architecture and Patterns here.
Nice Post!
Why are you using throw ex and not just throw? With throw ex you are creating a new stacktrace in your exception.
There’s no reason in particular, that’s not the main focus of the post.
//Andras
Pingback: Architecture and patterns | Michael's Excerpts