Have you ever wondered if a film about an aging Manhattanite being roped into joining his two friends on a cattle drive in the southwest could help you write better software?
Nested deep within the complex social commentary of the movie "City Slickers" there is a scene in which Curly, a software engineer turned cowboy, gives Mitch Robbins, the titular city slicker, advice on how to write great software.
Curly says that the secret of life (AKA writing great software) is one thing, and that it is up to us to figure out what that one thing is. So how can Curly’s advice help you write better software? Good question.
Single Responsibility Principle (SRP)
In his book, “Agile Software Development Principles, Patterns, and Practices” Robert Martin describes the SRP by saying, “A class should have only one reason to change”. When a class has more than one responsibility those responsibilities often introduce coupling. This coupling can make it difficult to modify the behavior of one responsibility without modifying the behaviors of the others. By separating functionalities into distinct classes we can guarantee that when a change is needed it only affects one class.
Putting this in Curly’s terms, the secret to writing good software is making sure that each software module does just one thing.
You should read “Agile Software Development Principles, Patterns, and Practices” by Robert Martin; buy it, borrow it, or beg the universe to pop one into existance for you (don't steal it, you're classier than that).
A class with a single responsibility will have only one reason to change. But what does responsibility mean in this context? Responsibility maps directly to the needs of the users of the software. The key to obeying the SRP is identifying the actors who will be using the software and understanding the functionality that each actor needs. Think back to my blog post on software values. The primary value of software is its ability to change to meet the future needs of future customers. Keeping the SRP in mind while designing software for your current customer will help you to deliver software with a high primary value.
We can view responsibilities as sources of change and our software's responsibilities are dictated by its users. When we say that the responsibilities are dictated by users we mean that responsibilities are dictated by roles that users play; let's call these entities “actors”. The first step in identifying the sources of potential change in our software should be identifying the actors who will interact with our software.
Thinking back to Curly again, as designers of software it is up to us to figure out what that one thing is with regard to each software module. We know that the primary sources of change for our software are our customers' changing requirements. It makes sense that if we map an individual actors' needs on the customer side to software modules that fulfill those needs we can isolate potential areas for change.
As an example we will create an event logger class and apply the SRP to it. Let’s start by writing a few requirements to define the functionality that our event logger class should have.
- Log events as the application executes
- Create new event log each new day
- Export the log of events to disk
- Import a log of events from disk
- Event log should be human readable on disk
Based on these requirments it looks like we could generate a single class that looks like this:
And here is the private class data for the Event Logger.lvclass:
Looking at the Event Logger.lvclass we can see that this one class has more than one responsibility. For example; exporting and importing event data to/from disk, and formatting the event log are both unique responsibilities that our class has. Each of these responsibilities is a potential agent of change.
The Event Logger.lvclass is an Actor (Actor Framework). A detailed explanation of this implementation lies outside the scope of this blog post. By wrapping our Event Logger functionality up inside of an Actor we have created an Event Logger service that can be launched and interacted with via Actor Framework messaging. The methods that you see in the "Messages" virtual folder act as an API for our Event Logging service, where the API is made up of Actor Framework message classes. The methods in the "Overrides" virtual folder are overrides of Actor Framework methods that our service can use to extend the built in Actor Framework functionality.
We can better identify these responsibilities by rewriting our user stories in a way that illustrates which actor needs each ability.
- As a software developer I want to create an event log so that I can use it in my application
- As a software developer I want to destroy the event log when my application finishes executing
- As a test engineer I want events to be logged as they occur on the test system so that I can reference them when something goes wrong
- As a test engineer I want event logs to be formatted in a human-readable fashion so that I can visually analyze them
- As a test engineer I want a new event log to be created each day so that log files do not grow unbounded
- As a database administrator I want to be able to store exported event logs to a database so that they can persist globally
- As a database administrator I want to be able to retrieve events from the database so that they can be imported by the software
User stories are short descriptions of features told from the perspective of the person requesting the ability. They typically follow the following template:
As <type of user> I want <goal> so that <reason>
User stories are typically used in Agile projects but can be a valuable tool regardless of your project management methodology.
The updated user stories point towards three different actors; the developer, the test engineer, and the database administrator. This gives us three potential vectors for change. For example, the formatting for exporting data could be coupled to the table structure of the database, if the database administrator needs to change the way that data is written to the database it could require a change in the way that data is exported from the Event Logger.
Now that we understand that we have three potential sources of change for our Event Logger module we can refactor.
And here is the Event Logger.lvclass private class data after the refactor:
We have taken our original Event Logger.lvclass and separated its functionality out into three separate classes (not counting our Event Logger Service Actor) with the goal being that each class should only have one reason to change. Our understanding of what could cause a class to change is driven by our understanding of the actors who will interact with our software, which we derived from our user stories. The Event Logger.lvclass now has only one responsibility, to provide an API that a Developer can use to interact with our Event Logger Service. The Business Rules class holds all responsibility related to formatting of the event log; how to name the file, how often a new file is created, as well as how to format both the header and event text. The Persistence class holds all logic related to handling the event log file. Lastly we added an Event class, which addresses the possibility that the event logger will need to handle more than one type of event. This was not explicitly stated through a user story but seems like a reasonable vector of change given the event loggers purpose.
One (Last) Thing
I would also like to point out the decision not to break the Business Rules class up into smaller classes. Looking at the Business Rules class we can see that in reality it does have more than one reason to change; it could change because the formatting of the event and header text changes, or it could change because the rules that dictate when a new file is created could change. However, as Robert Martin says, "An axis of change is an axis of change only if the changes actually occur." While our primary goal is for each class to have one responsibility and therefore only one reason to change, our secondary goal is to deliver software.
As software developers we strive to write software with both high primary and high secondary value. Writing software with high primary value requires that the software be able to meet the changing needs of the changing customer over the lifetime of the application. Keeping the SRP in mind as we design our software is one way to keep our software flexible. The SRP states that every class should have one reason to change, and because it is the people who interact with our software that drive this change (remember the changing needs of the changing customer) we can identify potential vectors of change by identifying the actors who will interact with our software. One tool in our toolbox for identifying these actors is user stories.
The SRP is conceptually simple and can be difficult to implement in practice but it is the foundation of good object-oriented design. The idea that each software module should have a single job will be a recurring theme as we discuss the other S.O.L.I.D. principles. So the next time you find yourself designing classes think back to Curly; the secret to writing great software is one thing, just one thing, but figuring out that one thing is up to you.