We evolved to our approach through a lot of TDD and refactoring and plain old trial and error, but the final touches came when we watched Gary Bernhardt's Destroy All Software screencasts and saw that he was using pretty much the same techniques, but with some nice naming patterns.
I don't know if there is a widely accepted name for this pattern, so I'm just going to call it the Service Layer Pattern. It's biggest strengths are it's simplicity and clarity. In a nutshell I'd describe it by saying that for every operation of your application, you create a "service" class. You provide the necessary context to these services, in our case as Active Record objects (NOTE: service does NOT mean remote service (ie, REST), it just means a class that performs some function for us).
So far so basic, the real goodness comes when you add the layering. I find there are a couple ways to look at this. The more prescriptive is similar to DDD's (Domain Driven Design) "Layered Architecture" which recommends 4 layers: User Interface, Application, Domain, and Infrastructure. From DDD:
The value of layers is that each specializes in a particular aspect of a computer program. This specialization allows more cohesive designs of each aspect, and it makes these designs much easier to interpret. Of course, it is vital to choose layers that isolate the most important cohesive design aspects.In my Demonstrating the Costs of DI code examples the classes and layers looked like this:
SpeakerController (app) > PresentationApi (app) > OrdersSpeakers (domain) > PresentationSpeakers (domain) > Active Record (infrastructure) > Speaker (domain) > Active Record (infrastructure)This concept of layering is very useful, but it's important not to think that a given operation will only have one service in each layer. Another perspective on this that is less prescriptive but also more vague is the Single Responsibility Principle. The layers emerge because you repeatedly refactor similar concepts into separate objects for each operation your code performs. It's still useful to label these layers, because it adds some consistency to the code.
Each of these services is an object, but that doesn't make this an object-oriented design. Quite the opposite, this is just well organized SRP procedural code. Is this Service Layer approach inferior to the OOP design hinted at by Uncle Bob? Or are these actually compatible approaches?
The OOP approach wants to leverage polymorphism to act on different types in the same way. Does that mean that if I have a service, like OrdersParties, that I should move it onto the Party object? What about the PartyApi class, should I find some way of replacing that with an object on which I could introduce new types?
There is a subtle but important distinction here. Some algorithms are specific to a given type: User.Inactivate(). What it means to inactivate a user is specific to User. Contrast that with User.HashPassword(). Hashing a password really has nothing to do with a user, except that a user needs a hashed password. That is, the algorithm for hashing a password is not specific to the User type. It could apply to any type, indeed to any string! Defining it on User couples it to User, preventing it from being used on any string in some other context.
Further, some algorithms are bigger than a single type. Ordering the speakers on a presentation doesn't just affect one speaker, it affects them all. Think how awkward it would be for this algorithm to reside on the Speaker object. Arguably, these methods could be placed on Presentation, but then presentation would have a lot of code that's not directly related to a presentation, but instead to how speakers are listed. So it doesn't make sense on Speaker, or on Presentation.
Some algorithms are best represented as services, standing on their own, distinctly representing their concepts. But these services could easily operate on Objects, as opposed to Data Structures. Allowing them to apply to multiple types without needing to know anything about those specific types. So I think the Service Layers approach is compatible with the OOP approach.
In the next post I'll take a look at how interfaces fit into this picture.