Getting to single-responsibility in a legacy codebase

Andrew Duncan
5 min readMay 15, 2017

--

One of the most important design principles in object-oriented programming is the Single Responsibility Principle. Yet, I’ve often found codebases littered with violations of this fundamental concept. This presents a difficult, yet interesting challenge. How do we improve the codebase without taking 3 years to rewrite it and without breaking everything?

Codebases live, breath, and change over time. Developers are constantly making changes. And it is during everyday development that opportunities to refactor are abundant. Martin Fowler coined the term opportunistic refactoring to describe the skill of refactoring a little each time you see code in need. He called it a skill because “[s]killful opportunistic refactoring requires good judgement, where you decide when to call it a day.”

“Skillful opportunistic refactoring requires good judgement, where you decide when to call it a day.” — Martin Fowler

Taking advantage of small opportunities to refactor code as you go is the most effective way to achieve a healthy codebase, and the smallest and easiest opportunity to refactor is to simplify your functions.

Simplify your functions

In order to begin grooming a codebase, we must start with the smallest building block. The easiest place to start thinking about single-responsibility is at the function level. In the below code sample, we can see that the UpdateOrder function handles multiple responsibilities.

Violation of SRP at the function level

Even though this is a simplistic example, we can see that the UpdateOrder method is handling at least these responsibilities:

  1. Validating parameters
  2. Determining whether or not to create a new shipping address
  3. Determining whether to create or update each order line
  4. Sending an order confirmation email
  5. Defining what it means to update an order

If we are applying opportunistic refactoring, we might start by simplifying this function so that it is only handling one of the 5 responsibilities listed above. To do this, we simply need to move the logic of each responsibility into its own method. Luckily, modern IDEs provide tools to make this and other types of refactoring easy.

Now our UpdateOrder method is much easier to understand, and only handles one of the 5 responsibilities. It is now solely responsible for defining what it means to update an order.

When taking the opportunity to refactor during a real agile sprint, we might stop here and call it a day. But we’re not done yet. This conveniently and inevitably brings us to the next level building block of our codebase.

Simplify your classes

Whether it be during the current sprint or a future sprint, with the next opportunity we get to refactor this class, we can focus on SRP at the class level. As a recap, a responsibility is simply a reason to change.

A responsibility is simply a reason to change.

Our UpdateOrder method now only has one reason to change. It only needs to change if the definition of what it means to update an order changes.

With that said, it is now evident that our OrderService violates that very definition. It would need to change if any one of the aforementioned 5 responsibilities need to change.

Our next set of refactors should be geared toward limiting the class to just one of the 5 responsibilities. As a recap, here are the responsibilities again.

  1. Validating parameters
  2. Determining whether or not to create a new shipping address
  3. Determining whether to create or update each order line
  4. Sending an order confirmation email
  5. Defining what it means to update an order

In order to simplify this class, we need to create “helper” classes to help it carry out its responsibilities. These will be new services that we can inject into the OrderService.

Here is what our OrderService would look like after removing all responsibilities except to define what it means to update an order:

Here we have extracted the responsibilities out of the OrderService and into a respective service for each responsibility. Notice that we are now injecting the new services as dependencies into the OrderService.

And here are our newly created helper services:

Each of these services now only have a single responsibility. As you can see, these classes are very small. But a closer look at the methods indicates that they may still be violating SRP. I say that to say, these refactorings can be performed recursively.

Simplify your new functions

The thing about opportunistic refactoring is that it is very easy to go down a rabbit hole and get distracted from the real task of implementing features. That’s because the next step to cleaning up the code and applying SRP is to refactor your new functions.

In a corporate environment with a large codebase or codebases, chances are you’ll refactor functions into smaller functions a lot. And from those refactorings will come classes.

Let’s take a look at what the OrderEmailService could look like if we refactored the SendOrderConfirmationEmail method.

Some rules of thumb

There are a couple of high-level code smells that I look for when going about my daily typing and clicking.

  • The size of the class should be less than 150 lines.
  • Keep the number of constructor parameters at 4 or less.

The first is the size of the class. I generally like to see classes no larger than 200 lines. This is by no means a one size fits all solution. There are valid scenarios for classes to be larger, but they are few and far between. I believe it should be possible to get almost all classes down to this size, even with comments and using directives.

The second is the number of constructor parameters being injected into the class.

  • 5 or more constructor parameters is generally a good indicator that the class is violating the SRP and should be split.
  • 4 constructor parameters can indicate that the class could potentially be split, but may not need to be.
  • 3 or fewer constructor parameters is a good indicator that the class is small and concentrates on a single responsibility

Start small, but start

Finally, the key to improving a codebase is to do so over time during normal development activity. You can start by finding one method each day to break apart into a smaller method. Once you’ve done that for a week, try finding a method (or use the new methods you’ve created by refactoring) that you can break out into another class. Slowly over time, this becomes second nature and is a part of your normal development activities. Eventually, you will see your code becoming healthier.

--

--