Over the past year, there has been a strong popular push toward what I’ll describe as purely SOLID-based approaches to building Rails apps. While I attempt to adhere to SOLID as a series of guidelines, they are just that: guidelines.
Evidently, once, someone caught me saying “Use Rails until it hurts”. By this, I meant “don’t preemptively implement around perceived Rails weaknesses solely to respect SOLID/your-favorite-OO-principle”. This does not add value to an application.
I recently wrote about #accepts_nested_attributes_for. In that post, I explained that there are clear circumstances where #accepts_nested_attributes_for will save you time and code. However, in many cases, it won’t. And, for those, I began working on an alternative.
Contingencies are good. Once you deviate from Rails’ “golden path”, your work can quickly become challenging. But knowing that a person/tool/technique has a weakness does not mean that you automatically preempt them. It means that you should remain aware of that weakness. You should compensate for weakness when it becomes an issue. To do so sooner smacks of angst.
Let’s consider the discussion here around the Observer pattern versus the benefit of a tightly coupled imperative style. The Observer pattern decouples event producers from event consumers. For that matter, the Observer pattern is similar to Queue-based messaging services such as the CORBA Notification Service, Java Messaging Service(JMS), and all of those that followed save that it is, typically, implemented in a synchronous fashion and within a single process.
As cited in the C2 wiki, the Observer pattern is best used for “dynamic relationships between objects.” There are relatively clear guidelines around when to consider employing it.
So what about when relationships are not dynamic?
Under these circumstances, I posit that tight coupling can be helpful.
What is tight coupling good for? Used selectively, in a word: clarity.
Which is clearer? (Please bear with my non-ActiveRecord ActiveRecord example for the sake of argument)
The Observer (listener)-based example at the top decouples the save event from the pushing the change to the client event. While you can argue that these are two different responsibilties, they are both directly related to the change on the User’s name. I argue that this approach adds unecessary indirection.
To speak more broadly, I argue that our campaign against tight coupling has simply gone too far.
In the latter case, we have less code! This is, often, considered a good thing1. The controller, responsible for performing the change to the User object, also pushes a notification to the client of the change. The tight coupling makes this entirely clear: when I change the user’s name and save the user, I immediately push the change out on a socket to the client.
Yes, you can make a case for extracting the behavior from the UserController#update method into its own method or even class.
Whether to extract the logic out of the controller or not should be a fuzzy decision. Yes, the update method is clearly responsible for more than just routing. This is “bad”, right? Or is it?
If we extract a class to represent the context of the User’s name changing, we’ve just created another file and another class. Also, where does this class go? To me, it’s just a delegate of the controller. It’s certainly not business logic. Pushing a message out on a socket does not represent business logic at all. Instead, it’s just how we interface with our View/Client. Extracting a class, in this case, increases the cognitive burden on ourselves or any developer who comes after us.
If we extract a private method instead of extracting a class, we’ve added a layer of indirection instead of abstraction. Frankly, I’d lean toward something like this.
But, clearly, this will lead to a fat controller! ¡Qué terrible!
Personally, I would instead characterize it as a controller of a healthy weight.
Moderation is key. Noticing its lack is as well.
I hope that, from the above example, you’ve decided to stop factoring everything into another class and, occasionally, give tight coupling a chance. That said, if you’re among those who write “god classes”, “god methods”, and is unfmiliar with “loose coupling”, I strongly suggest reading Working With Legacy Code by Michael Feathers (the man who coined “SOLID”).
Rails isn’t perfect. But it is one of the best solutions for the problem space that most of us work in. Use Rails until it hurts. Don’t preemptively replace Rails features. Use Rails until you find yourself writing more code doing it “The Rails Way” than if you rolled your own solution. Then identify an alternative solution. Use that alternative solution as your application’s fallback convention for when the matching Rails convention fails.
Or, as a peer of mine, wrote:
@elight Refactoring to patterns > starting with patterns IMO.— Brian P. Hogan (@bphogan) November 9, 2012
While Rails provides many solutions out of the box, you should make a concerted effort to keep your own personal toolbox of fallback techniques (such as the Form object) on hand. Share them. Convert them into tools for use by your peers whenever possible.
1 As code itself is a liability!
Posted by evan on Nov 21, 2012