This afternoon at Windy City Rails, Yan Pritzker‘s talk, “Domain Driven Rails—Use Case Driven Simplicity,” went over how the Reverb application avoids this:
cartoon courtesy of bonkersworld.net
That is, how we go about avoiding finishing our app…and then having to refactor everything.
“How many of you are happy with the size and complexity of your models and controllers?” Yan asked the crowd. A few people raised their hands.
“There are a few more questions that I’d argue might be more useful.”
They were:
Am I happy with the scalability of my team—can I add developers to the project with ease?
Am I happy with the adaptability of my business—can I add and change the functionality on my app?
The practical questions that Yan suggests do make sense, and they are, perhaps, extensions of the model/controller complexity question. These are the reasons that we care whether the models and controllers are complex. So, theoretically, if we could answer those questions in the affirmative even with some complexity…maybe that would be okay.
Not that Yan suggests a complex model or controller. What he does suggest, though, implies the use of more files. But before you gag, keep in mind what we just established about the purpose of having simple application code. It’ll be important for considering Yan’s architecture suggestions.
First, we should note that what Yan talked about here is not a SOA network: it is a monolith, with 115 models and over 1000 classes. Hold your prejudices till the end, developers. Monoliths have their advantages (for refactoring and, again, team scalability), and though they may not be your go-to choice, what we’re discussing here is a practical consideration:
How do we build better monoliths?
First of all, what does it mean to have a “better” monolith?
We all have our ideas about what it means to have “good” code, but the practical truth is that commonly used classes are hard to refactor because you can’t just make the whole team go hands–off while you make those changes. So instead, let’s figure out how to write things once, ensure that they work, and then leave them alone—rather than modifying them 30-50 times a year.
HOW?
Yan’s suggestions:
1. “Don’t put different rates of change together” -Kent Beck
That is, create separate modules and classes for code that does not change and code that changes all the time. This way, we’re not muddying up long-standing functionalities every time we want to maintain a feature or detail that changes all the time.
2. Build domain layers—and put the business logic there. ActiveRecord is not the place for it because the models get too fatso that way. Domain layers consist of pure Ruby code outside of rails. What will that look like in practice? Well, like this:
That’s Yan’s presentation, with the Rails MVC architecture represented on the left and the Reverb architecture represented on the right.
While you’re doing this, you’ll want to namespace to avoid collisions with Gems. You’ll also want to diligently assure that each method has a unique name, which will make it easy to search in the application for testing and debugging purposes.
So, example: You have some logic in your controller, and you want to move it out. What do you do?
Well, you create a class, stick in the logic from the controller. require the class in the controller, and then invoke it where needed. The end. Not so scary.
3. Assign roles: add methods to objects on demand in the context of a Use Case.
Don’t change the original objects to add functionality in your app. Instead, add new stuff by namespacing and adding the few new methods you need. Bonus: now the logic is separated by use case, so the form of your app supports and reinforces the function.
4. Use policy objects: separate objects to represent events that happen often but are likely to change. Then you can change these often while using them in conjunction with objects that do not change as often.
A tool Yan suggests to help with this whole process: Wisper.
Wisper allows for subscribe classes that listen to events. When you add another listener to modify behavior, the core code does not change—thus separating behaviors (what software does) from models (what the software is).
You can also use it to set global listeners for cross-cutting concerns without littering code. For example, by creating an AnalyticsListener that you can call in multiple places, you can avoid your analytics functionality giving rise to a god model of some kind.
Some questions worth considering for embarking on a reverb-style architecture:
- How should we manage sprawl in this application?
- How do we onboard new developers and tour them through code built like this?
- How do we name things in a clear way?
- How do we write documentation for this?
But those are the types of questions that drive the discussion forward and expand our understanding of how to design software.
If you want to learn more about software architecture, Yan recommends:
Many things (slides to come), but at the very least, following the work of Kent Beck and Uncle Bob.
Fantastic summary! Btw, the gem is called Wisper with no ‘h’: https://github.com/krisleech/wisper
Slides here https://speakerdeck.com/skwp/domain-driven-rails