Note: This is the first of five posts I’m writing on the SOLID principles of object-oriented programming, and the fourteenth of 30 posts that I’m writing in September.
If you write object-oriented code, but have never heard the phrase “SOLID principles”, we should talk.
SOLID is a mnemonic for these five things that you should pay attention to when doing object design:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Come back over the course of the week to see the last four, but today I’m going to ramble a little about the Single Responsibility Principle – or as I sometimes call it, the Small, Dumb Object Principle.
In code calling web services, there’s a pattern I commonly see: Someone will write a class that takes some query parameters, calls the network endpoint, pulls out the JSON (or an error) from the response, interprets the JSON into a model object, and returns it to the user (usually asynchronously, via a block).
I think that’s a fine interface to show to calling code – request params in, model objects out – but I don’t think much of it as an object design. There’s too much going on in that one class. I would break it up like so:
- There should be an Endpoint class that does nothing but turn the parameters into an HTTP request, and blindly report the result (successful JSON response or an error state).
- There should be a Marshaller class that does nothing but consume JSON and spit out model objects.
- There should be a Service class that coordinates the other classes, and perhaps does some manipulation/saving of the models or interpretation of the error state before reporting them to the user.
To some people, that seems like overkill – three classes when you could have one. I’d say they’re not thinking deeply enough about software requirements and how they tend to change.
The Single Responsibility Principle states that a class should have only one responsibility. Put another way, there should only be one category of system requirement that forces each class to change.
What happens when your web service gets versioned? What if you have to handle multiple versions of the API in your code? If you’ve coupled your JSON marshalling code tightly to the output of your network call, it’s going to be a pain to tease that out. It’s wiser to separate your concerns, keep the network code dumb about how its output will be used, and be able to swap in the right Endpoint object as needed.
Keeping the Marshaller separate has benefits, too. What if you’re working on a dating app that has identical user objects come down as part of login, search results, messages, and a local newsfeed? You’ll want that marshalling code packaged reusably, decoupled from any one API call, but accessible to them all.
Another problematic implementation I see is packaging the marshalling code inside the model class – why shouldn’t the model object create itself? You can have one less object, and it doesn’t break the reusability case I described above.
What if you’re working on a retail app that has different product representations come down from different API calls? The product detail call is going to have a lot more information than the search results, and will also look different from products in a shopping cart (which will at least have a quantity added), or the products in order history (which will have a quantity, an order price separate from the current price reported by the detail call, possibly a link to the order…). If you try to handle all these cases in the model object, three quarters of your model code is going to be devoted to translating these different JSON representations into model instances. Does that make sense? Does that match with the “data and business logic” description of the M in MVC? On the other hand, a small class hierarchy of Marshallers can handle this cleanly while keeping the model class ignorant of the details of its own serialized representation.
Because the Endpoint and Marshaller objects are ignorant of each other, they’ll need help to get anything done. So I write a Service object that passes parameters to an Endpoint, passes its output to the Marshaller, and returns the results to the user.
Is the Endpoint pulling from an HTTP web service? A SQL data store? An interface to a fax line? The Service doesn’t care; it just knows that the Endpoint takes parameters and asynchronously spits out data and errors. Is that data in JSON? XML? Packed binary format? Cuneiform? The Service doesn’t care; that’s the Marshaller’s problem. It just knows to feed the Marshaller data and collect the model objects that fall out – at most, it has to choose the right Marshaller based on a data type or other out-of-band signal from the Endpoint.
Because each class is small and limited in scope, such classes will tend to be very testable in isolation – which is great if you’re doing TDD or any other automated testing practice. Because each class is small and limited in scope, it will be very easy to look at the code and reason about what the object is doing – and that makes your life so much easier when you’re debugging.
The most common complaint I’ve heard about this way of doing things is that it leads to a proliferation of small classes and class files, and I have admit, that leaves me a little baffled about the logic behind the objection. Reducing the number of files in a project is not a reason to do anything. Is business value somehow inversely proportional to the number of files in a project? Does such an objection refute any of the practical points about reusability, complexity, or coupling that the Single Responsibility Principle addresses? As Edward Tufte has famously said: “There is no such thing as information overload. Only bad design.” I’d extend that to say that there is no such thing as too many class files in a project – there are only poorly organized projects.
Small, dumb classes with standard interfaces are the building blocks of stable systems. This is one of the key ways we manage and mitigate complexity in object-oriented programming.
Tomorrow, I’ll share some thoughts about the Open/Closed Principle, which is one of the ways we manage change in OOP.