Regardless of the individual criteria for what constitutes outstanding code, one essential quality remains consistent: maintainability. Proper syntax, concise variable names, comprehensive test coverage, and other such details can only take you so far. Code that isn’t maintainable or adaptable to evolving requirements is on a fast-track to obsolescence. Prototypes, proofs of concept, and minimum viable products might not demand top-tier code, but in all other cases, maintaining code should be a priority. Arguably, this is a fundamental characteristic of sound software engineering and design.
In this write-up, the focus is on explaining how the Single Responsibility Principle, along with several techniques revolving around it, can enhance your code’s maintainability. Writing good code is indeed an art form, but leveraging certain principles can provide the necessary direction for creating robust and maintainable software.
The Importance of a Framework Model
Almost every guide to a new MVC (Model-View-Controller), MVP (Model–View–Presenter), MVVM (Model–View–ViewModel), or other similar software architectural pattern is filled with explanations of subpar code. These examples aim to showcase the framework’s capabilities and potential. However, they often inadvertently provide counterproductive advice for beginners. Simplistic conceptual models under descriptions like “let’s use ORM X for our models, templating engine Y for our views and have controllers to manage it all,” typically result in oversized, clunky controllers.
However, it’s crucial to note that these examples are designed to demonstrate the ease at which you can get started with the framework, rather than functioning as lessons in software design. Yet, it’s often only after years of development that coders realize the downsides of having monolithic blocks of code embedded within their projects.
Models are crucial to your application. Keeping models separate from the rest of your application logic significantly simplifies maintainability, irrespective of how complex your application gets. Even for intricate applications, an efficiently implemented model can result in impressively expressive code. To achieve this, ensure your models focus solely on their designated function and remain oblivious to the app built around them. Equally, your models shouldn’t concern themselves with the specificities of the underlying data storage layer, be it an SQL database or simple text files.
As we delve deeper into this article, it’ll become more evident that great code significantly leans on the separation of concerns.
Understanding the Single Responsibility Principle
You’re likely familiar with the SOLID principles of object-oriented design – Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. The initial letter, S, represents the Single Responsibility Principle (SRP), and its significance in software development is significant, to say the least. It is reasonable to argue that SRP is both a necessary and sufficient condition for good code. Indeed, in most weakly written code, you’ll typically find a class that bears more than one responsibility – a thousand lines of code in form1.cs or index.php isn’t an uncommon sight.
Let’s explore a typical example of this in C# – specifically, with the ASP.NET MVC and Entity frameworks. Even if you’re not a C# developer, a basic understanding of Object-Oriented Programming (OOP) should suffice to follow along.
public class OrderController
{...
public ActionResult CreateForm()
{
/*
* View data preparations
*/
return View();
}
[HttpPost]
public ActionResult Create(OrderCreateRequest request)
{
if (!ModelState.IsValid)
{
/*
* View data preparations
*/
return View();
}
using (var context = new DataContext())
{
var order = new Order();
// Create order from request
context.Orders.Add(order);
// Reserve ordered goods
…(Huge logic here)…
context.SaveChanges();
//Send email with order details for customer
}
return RedirectToAction("Index");
}
... (many more methods like Create here)
}
The above is the usual OrderController class, showing its Create method. In such instances, it’s not uncommon to find the Order class itself being used as a parameter in an HTTP POST request. Using specialized request classes, however, provides an additional layer of adherence to the Single Responsibility Principle.
Observe from the above code snippet how the controller knows more about “placing an order” than necessary. This includes but isn’t limited to storing the Order object and e-mail notifications, amongst other responsibilities. Handling all these tasks makes it unwieldy. Every minor change requires adjusting the whole controller’s code. Furthermore, if another Controller needs to adopt order creation functionality, developers typically resort to duplicating the code. In principle, controllers should merely supervise the overall process, rather than house the entire logic behind it.
Today, the aim is to bid adieu to these sprawling controllers.
Consider an initial step in cleaning up such a controller by extracting all business-specific logic from the OrderController and moving it to an OrderService class:
public class OrderService
{
public void Create(OrderCreateRequest request)
{
// all actions for order creating here
}
}
public class OrderController
{
public OrderController()
{
this.service = new OrderService();
}
[HttpPost]
public ActionResult Create(OrderCreateRequest request)
{
if (!ModelState.IsValid)
{
/*
* View data preparations
*/
return View();
}
this.service.Create(request);
return RedirectToAction("Index");
}
}
After the above adjustment, the controller becomes a leaner operative – doing only what it should: controlling the process. Now it only knows about views, the OrderService, and OrderRequest classes – absolutely the minimum information needed to conduct its role efficiently, which is to handle requests and send responses.
Selective reorganization like this dramatically reduces the need for recurrent adjustments to the controller code. Components like views, request objects, and services might still adopt changes since they’re closely linked to business requirements. However, changes to controllers themselves can be minimized.
This example encapsulates the essence of the Single Responsibility Principle. Several techniques are available to write code compliant with this principle. One such technique is ‘Dependency Injection’, which also aids in writing testable code.
Dependency Injection
Envisioning a large project predicated on the Single Responsibility Principle without the implementation of Dependency Injection is tough. For instance, revisiting our aforementioned OrderService class:
public class OrderService
{
public void Create(...)
{
// Creating the order(and let’s forget about reserving here, it’s not important for following examples)
// Sending an email to client with order details
var smtp = new SMTP();
// Setting smtp.Host, UserName, Password and other parameters
smtp.Send();
}
}
While the above code could work, it isn’t optimal. To comprehend how the Create method of OrderService class operates, developers need to dive into the SMTP specifics. What’s more, replicating this use of SMTP across other requirements leaves duplicating code as the only viable option. But a slight refactoring can change that:
public class OrderService
{
private SmtpMailer mailer;
public OrderService()
{
this.mailer = new SmtpMailer();
}
public void Create(...)
{
// Creating the order
// Sending an email to client with order details
this.mailer.Send(...);
}
}
public class SmtpMailer
{
public void Send(string to, string subject, string body)
{
// SMTP stuff will be only here
}
}
The changes definitely show an improvement, but there’s still more work to be done. The OrderService class still knows too much about sending emails. It needs to create an instance of the SmtpMailer class to send an email. While this code works, it could pose an issue in the future. What happens when the functionality changes? How can developers log the contents of sent emails for their development environment, instead of dispatching them? What about unit testing for the OrderService class? To address these queries, let’s continue refactoring by creating an IMailer interface:
public interface IMailer
{
void Send(string to, string subject, string body);
}
SmtpMailer will inherit the properties of this interface. In the interim, the application will use an IoC-container (Inversion of Control Container), and this container will be configured such that an IMailer instance is generated by the SmtpMailer class. This results in the following changes to the OrderService class:
public sealed class OrderService: IOrderService
{
private IOrderRepository repository;
private IMailer mailer;
public OrderService(IOrderRepository repository, IMailer mailer)
{
this.repository = repository;
this.mailer = mailer;
}
public void Create(...)
{
var order = new Order();
// fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.)
this.repository.Save(order);
this.mailer.Send(<orders user email>, <subject>, <body with order details>);
}
}
Finally, we’re making significant strides! This refactoring change facilitated the OrderService to interface with the IOrderRepository to interact with the component that stores all our orders, thereby making the service entirely agnostic about its implementation or the underlying technology powering it. The OrderService class now contains code that is laser-focused on the specificities of order business logic.
In case of a malfunction, such as an issue with email transmission, the developer knows precisely where to look – the SmtpMailer class. If an aspect, such as discounts, don’t work as expected, the developer can refer to the OrderService (or a DiscountService, if you’re a vehement adopter of SRP) class.
Event-Driven Architecture
Despite massive improvements to the code, the OrderService.Create method isn’t perfect yet:
public void Create(...)
{
var order = new Order();
...
this.repository.Save(order);
this.mailer.Send(<orders user email>, <subject>, <body with order details>);
}
Sending an email doesn’t belong to the primary order creation workflow. Even if email dispatch fails, the order creation process remains unaffected. Furthermore, consider a scenario where a user can choose not to receive an email upon successful order placement. Incorporating this into our existing OrderService class requires the introduction of a new dependency, IUserParametersService. Add localization to the scenario, leading to yet another dependency, ITranslator (to compose email messages in the user’s preferred language). All this leads to a surge in the number of dependencies and bloating the constructor beyond the screen’s dimensions. Magento’s codebase furnishes an excellent example of this scenario – a popular eCommerce CMS written in PHP features a class with a whopping 32 dependencies!
Distinguishing separation logic in such scenarios is indeed challenging, and Magento’s class possibly fell into this predicament. This is where an event-driven approach becomes valuable:
namespace <base namespace>.Events
{
[Serializable]
public class OrderCreated
{
private readonly Order order;
public OrderCreated(Order order)
{
this.order = order;
}
public Order GetOrder()
{
return this.order;
}
}
}
Whenever an order is created,
Discover more from TechBooky
Subscribe to get the latest posts sent to your email.