Writing Flexible Code with the Single Responsibility Principle
SOLID Principles and Maintainable Code
If you’ve been around software for a while, then you’ve almost certainly heard of the SOLID principles. In short, these are a set of principles intended to help developers write clean, well-structured, and easily-maintainable code. In software, as in any intellectual and creative endeavor, there is quite a bit of debate about the “right” way to do things. Different practitioners have different ideas about what is “right”, depending on their individual experiences and inclinations. However, the ideas prescribed by SOLID adherents have been widely adopted in the software community, and agree with them or not, they’re a useful set of principles from which to draw best practices. Moreover, SOLID has been thoroughly integrated into a broader set of Agile development practices and understanding them is thus a virtual requirement in the modern software industry.
Developers and bloggers have written seemingly ad infinitum about SOLID in various places across the web. In researching this article, I encountered many such resources, some of which are cited at the bottom of this article for your reference. So, if the SOLID principles are well-covered elsewhere, why write yet another article about them? In short, for my own edification. Writing about complex topics is one of the best ways to learn them yourself. And for that reason, I am planning a series of five articles — one on each of the SOLID principles. What follows is the first such article and focuses on the single responsibility principle. Although I don’t expect my addition to the corpus on this topic will be particularly unique, I hope that it will prove useful to some readers. And with that, let’s dive in.
A Quick Refresher on SOLID
SOLID is an acronym for a set of five software development principles, which if followed, are intended to help developers create flexible and clean code. The five principles are:
- The Single Responsibility Principle — Classes should have a single responsibility and thus only a single reason to change.
- The Open/Closed Principle — Classes and other entities should be open for extension but closed for modification.
- The Liskov Substitution Principle — Objects should be replaceable by their subtypes.
- The Interface Segregation Principle — Interfaces should be client specific rather than general.
- The Dependency Inversion Principle — Depend on abstractions rather than concretions.
The Single Responsibility Principle
The single responsibility principle (SRP) states that every class or module in a program should have responsibility for just a single piece of that program’s functionality. Further, the elements of that responsibility should be encapsulated by the responsible class rather than spread out in unrelated classes. The developer and chief evangelist of the SRP, Robert C. Martin, describes a responsibility as a “reason to change.” So, another way of putting the SRP is to say, as Martin does, that “a class should have only one reason to change.”
Before going any further it’s worth taking a look at the history of the SRP. Martin originally introduced the term in as part of his Principles of Object Oriented Design [1]. According to Martin, the SRP has origins in Tom Demarco’s idea of cohesion, which describes the extent to which elements in a given class/module are related and relevant to one another. Furthermore, it builds on David Parnas’ description of encapsulation, or information hiding, which says that attributes and behavior relevant to a given object should be bundled together and hidden from outside access [2]. Taken together, these ideas lead naturally to the principle that a given piece of software functionality (aka, a responsibility) should be bundled into a single class and hidden from other elements of the program — exposing only those pieces necessary to the functionality of the program as a whole.
On its face, this seems relatively straightforward. Individual pieces of a program’s functionality should be distributed to distinct entities that are capable of handling them without outside assistance. But how do you define an “individual piece” of a program? What, exactly, is a “responsibility” and how do you reason about it from a business perspective? Martin, popularly known as “Uncle Bob,” clarified just this concern in a 2014 blog article where he tied “responsibility” to the idea of interested actors [3]. Martin’s article is well worth a read, but to summarize, he argues that if a piece of software has several different kinds of users (aka, actors), then the disparate interests of each of those users defines a piece of that software’s responsibilities. Martin uses the example of C-Suite executives (COO, CTO, CFO), each of whom uses some piece of business software for different reasons. Moreover, when considering how software should be changed, each of those actors should be able to dictate changes in the software without affecting the interests of the other actors.
The “God Object”
Per usual, probably the best way to learn about the SRP is to see it in action. But to do that, we should perhaps first see what a program looks like when it does not adhere to the SRP. Let’s take a look at a simple program and see if we can break down its responsibilities. What follows is a brief Ruby program that outlines a class that describes the behaviors and attributes of space stations. Read through it and see if you can identify: a) the various responsibilities of objects instantiated by the SpaceStation class; and, b) the types of actors who might have an interest in an a space station’s activities.
Admittedly, our space stations aren’t particularly capable (I guess NASA won’t be calling on me any time soon); however, there is still quite a bit to unpack here. Immediately we can see that the
SpaceStation
class has several disparate responsibilities. Roughly, we might say that space station operations can be broken down into four areas: sensors; supplies; fuel; and, thrusters. Although personnel are not specified in the class, we can easily imagine different actors who might care about these operational areas. Perhaps a scientist who manages the sensors, a logistical officer who handles supplies, an engineer who manages fuel, and a pilot who manages the thrusters. Given this variety of different operational areas and interested actors, might we say that this class is violating the SRP? Absolutely.Currently, our
SpaceStation
class is a classic example of a so-called “God object” — that is, an object that knows about and does everything. This is a major anti-pattern in object-oriented programming and should be avoided. But why? What’s wrong with a “God object”? Well, for starters, such objects are extremely hard to maintain. Our program is very simple right now but imagine what would happen if we added in some new functionality. Maybe our space station will need crew quarters, or a medical area, or a communications bay. As we added in such functionality, theSpaceStation
class would grow to immense size. Worse yet, each piece of functionality would be inextricably tied to all the others. If we want to change how the fuel tank is managed we might inadvertently break thruster operations. If the station scientist requests changes to sensor operations, those changes could have trickle-down effects to the communications bay.Violating the SRP may be convenient in the beginning but the short-term benefits are not worth the long-term maintenance costs. Not only do we have to worry about how changes in one place affect another (due to our failure to separate concerns), but the code itself becomes unwieldy and unpleasant to deal with. Breaking program functionality down into encapsulated pieces is a much better option. Given that, let’s make some changes to our
SpaceStation
class.Breaking Down Responsibilities
Earlier, we identified four rough areas of operation that our
SpaceStation
class was managing. Those seem like a good place to start as we refactor our code to be more in line with the SRP.Phew! That was a lot of changes, but things are already looking a lot better. Now, our
SpaceStation
class is mostly just a container for subservient parts that manage individual operations, namely: a supply hold; a set of sensors; a fuel tank; and, thrusters. Each of these takes the form of an instance variable that is set during space station initialization. For each variable there is a corresponding class:Sensors
;SupplyHold
;FuelTank
; and,Thrusters
.As you look through this version of the code, you will note several important differences with the first version. Not only are particular pieces of functionality encapsulated in their own classes but they are organized in a manner that is both predictable and consistent. The idea is to group like pieces of functionality in an attempt to follow the cohesion principle and to isolate data such that it is only accessible to relevant actors. Now, if we wanted to change how supplies are managed from a hash structure to an array, we could do so very easily in the
SupplyHold
class without affecting anything else in the program. Put another way, if the station logistics officer requests changes to her section’s functionality, then we can do that without affecting work being done by the station science officer. Meanwhile, theSpaceStation
class has no idea how supplies are being stored and nor does it care!Our users (science officer, pilot, etc.) are probably reasonably happy now with how their relevant parts are broken out, and they can request changes as needed; however, there is still more work we can do. Note, for example, the
report_supplies
method in theSupplyHold
class and thereport_fuel
method in theFuelTank
class. What happens if flight control back on Earth asks for a change in the way reports are submitted? Well, we would have to change theSupplyHold
andFuelTank
classes. But what now if flight control decides to change how supplies or fuel are loaded on the station? No problem, we’ll once again change the relevant methods on these classes. Hmm… it would seem then that we have multiple reasons for change on these particular classes. That sounds to me like a violation of the SRP! Let’s see if we can make a few more adjustments.In this final version of our program, we have broken out reporting responsibilities into a
FuelReporter
class and aSupplyReporter
class, both of which inherit from a parentReporter
class. We then add instance variables to ourSpaceStation
class to initialize relevant reporters for it to use. Now, if flight control asks for changes to reporting procedures, we can make the relevant changes in theReporter
subclasses without affecting the classes that they are reporting on.Of course, there is still some coupling taking place between our various classes. A
SupplyReporter
object depends on being handed aSupplyHold
object, as does aFuelReporter
object depend on aFuelTank
object. Necessarily, theThrusters
too require aFuelTank
to draw upon. All of this seems reasonable to me, as some linkage is unavoidable, and we could still alter the operations of one object without drastically affecting the others. However, there is still room to improve on this program and to propose changes that would increase flexibility and maintainability (and indeed, I welcome such proposals in the comments!) What’s important for now is that this version of the code is a fairly significant improvement over our first “God object” version. We have effectively separated responsibilities into individual classes and thus reduced the chances of code changes in one place breaking operations in another. It’s also much nicer to work with when updates are needed.TL;DR
The Single Responsibility Principle (SRP) is one of the five so-called SOLID principles, developed and promoted by Robert C. Martin to help developers produce flexible and maintainable code. In short, the SRP says that a given module or class should have responsible for a single element of a program’s functionality, and thus have just a single reason to change. The benefits of adhering to the SRP include: clearly defined boundaries as to where a piece of functionality is implemented; information-hiding practices that protect the integrity of data; separation of concerns that ensures changes in one location do not affect others; and, ease of code maintenance. In practice, it can be useful to think of your programs in terms of interested users and whether changes they request to one piece of functionality might inadvertently affect others. In the long run, following the SRP will both save you time and result in more effective code.
That’s all for our discussion of the SRP. Stay tuned for articles on the remaining four SOLID principles. Part 2 on the Open-Closed Principle is available here. If you have any comments or questions, leave them below — I would love to hear what you think.
If you would like alerts when a new article is published you can follow me here on Medium, on Twitter, or subscribe on my personal blog where these articles are cross-published. Happy coding!
References
- Paper: SRP: The Single Responsibility Principle (objectmentor)
- Paper: On the Criteria to be Used in Decomposing Systems into Modules (1971; Parnas)
- Article: The Single Responsibility Principle (20140508; Martin, R.)
- Article: The Secret Behind the Single Responsibility Principle (20170830)
- Article: SOLID: Part 1 — The Single Responsibility Principle
- Article: Understanding SOLID Principles: Single Responsibility
- Article: Think You Understand the Single Responsibility Principle?
- Article: Single Responsibility Principle: A Recipe for Great Code
- Wikipedia: Single Responsibility Principle