Skip to content

Instantly share code, notes, and snippets.

@endash
Last active August 29, 2015 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endash/11233091 to your computer and use it in GitHub Desktop.
Save endash/11233091 to your computer and use it in GitHub Desktop.

Idle Reminiscing (on Rails)

Rails, in 2007, was a tall glass of cold water. Coming from a world of spaghetti-coded PHP scripts (if you were lucky—ColdFusion, Java, or ASP if you weren't) it seemed to be the right framework at the right time: the router took away the jumble of scripts that hard-coded our app endpoints, the "MVC" structure and naming conventions fit all of our code into nice, individually labeled bins, and ActiveRecord single-handedly eliminated 99% of the reams of hand-coded, error-prone, wildly insecure SQL that littered our codebases.

In time, though, it's become more and more clear that Rails was simply a framework at the right time. Rails didn't impose a new object-oriented structure on our previously formless apps... it simply imposed a new Railsy structure, and obscured the difference with a language change. If a block of code went at the top of a PHP file, that same chunk of code, transmogrified, got stuck into the controller, by default, because that was the first point of control the developer had access to. The Rails structure didn't turn our apps into OO programs, they just split our PHP files over three different files and imposed a bit of sanity with the nice naming conventions and automatic file generators. It felt great at the time, though. Maybe you had to be there.

Fat controllers have given away to "skinny controllers, fat models." A User model might be given the responsibility for sending a password reset email after creation. An Order model might be responsible for generating a shipping label. This is still pretty bad. Our "default bucket" for code just switched from whatever point we can get access to earliest—e.g., whatever is most analogous to the "top of a PHP file"—to a conceptual bucket. User related stuff goes here. Order related stuff goes there. Still not object-oriented.

Moving Towards Object-Oriented Design: SOLID Principles

The next big leap in Rails best practices currently underway is jettisoning "fat models" in favor of more object-oriented code. There are big overarching patterns (hexagonal development, DCI, Uncle Bob's "clean architecture") and there are smaller, more immediately realizable extensions (use case, service, and policy objects), but in either case ultimately what the newer practices do is start to unravel the mess that Rails has made of SOLID principles, in particular the "S" (the single responsibility principle) and the "D" (the dependency inversion principle). The "OLI" are important, too, but these are the biggest and easiest to grasp, and the simplest to reap benefits from.

Single Responsibility Principle

SRP is poorly named, because it leads to thinking along the lines of "my User model follows SRP because it's only responsible for Users". OK, but it still does 20 million different things. I prefer to put it this way: a class should fulfill one need in your application. "A user needs to be able to be authenticated against their stored password", "A user needs to be activated once they've confirmed their identity", "A user needs to be sent a confirmation email upon creation" are all valid "needs" that indicate they should not be conflated with the User object itself.

Following the SRP will lead to a proliferation of service, policy, and other helper classes. This is a feature, not a bug. The new classes are all easily tested and isolated from one another. There's no way that mucking up your UserActivation service will break anything for already activated users, and the tests for each component will be much more comprehensible and authoritative. Ideally, you've whittled your User down to being responsible for only a handful of things... hashing a new password, validating an email address format. And, of course: persisting your User to a database. More on that one later, though. (hint: "A user needs to be saved to the database")

Dependency Inversion Principle

DIP says that the implementation of whatever external code your code depends on should be swappable in principle, because you're programming against an abstraction: a formal interface, a standard API, or a set of common method signatures. It also requires that the implementation of your dependency should be swappable in practice, because you maintain a local reference to the implementation, and do not rely on hard-coded globals or inheritance. If you explicitly reference a global (::Redis, or ::FbGraph, etc...) you are violating the DIP because you can not vary the implementation independent of your own code.

If you have to inherit from a base class to get the functionality (via a mythical RedisReader or FbGraphReader), you are soooo violating the DIP because your code is bolted as tightly to the implementation as possible. Can you think of any places in your code base where you inherit from an implementation where you maybe shouldn't be? (hint: "A user needs to be saved to the database")

ActiveRecord is solid, but not SOLID

ActiveRecord has built up a lot of good will over the years, and has, to its credit, withstood the tests of time. I'm not about to tell people that they can't properly test apps that use ActiveRecord because that's obviously not true, we've been doing it for years. Instead, I'm going to try to illustrate the straitjacket that AR has been keeping us in, without our knowledge, via the following example.

Let's pretend we're building a simple order fulfillment application for our Kickstarter supporter rewards (everyone gets one t-shirt). Our back-end is Rails, naturally, but our front-end is a single page application that communicates via our JSON API. For now, we have but one endpoint: /orders.

We end up with three models: Order, Person, and Address. An Order encompasses a Person which encompasses an Address. We'll add the usual validations to make sure we only save completed orders. We wire everything up and post an Order to our endpoint and see that it saves to the database. Perfect.

And then... a Change Request

For various reasons, it turns out that a lot of orders don't get completed in one sitting: power failure, calls drop, inconveniently timed breaks. "They" want us to modify the app to live-sync the data constantly, so a user's last incomplete order is always retrievable. The only guarantee we would have in the form of data is a phone number will be associated with the order automatically.

That blows a rather large hole in our current implementation. Our assumptions about data validity are still correct, a complete order/person/address still needs all those things we specified in our validations. But now it turns out that we still care about data even when it isn't valid, we can't just spit out a list of validation messages and discard the data.

One option is to make the validations stateful... we might modify our state machines on the models to have an indeterminate state during which the validations don't get enforced. However, there's a major problem with that option: database constraints prevent invalid data as well. A more fundamental issue is that we'd be mucking with/complicating our statemachines and throwing data consistency to the wind to work around an architectural problem.

We have coupled ourselves into a corner: an Order is an instance of ActiveRecord::Base, so despite our best efforts elsewhere in the code base we have completely conflated the concept of an Order with the manner in which we persist an Order, and with the manner in which we validate an Order.

What might you want to do in this case, in an ideal world? Maybe you'd want to have two different Validator objects, one for a complete Order and one that just makes sure it has a phone number. If an Order passes the CompleteOrderValidator, it gets persisted to the database. If it doesn't, but does pass the phone number Validator, a JSON representation gets temporarily stashed in Redis. The API code is responsible for making sure these get serialized appropriately for the browser app. (Probably using ActiveModelSerializers)

As it stands, though, we're probably stuck hacking something into the controller (uh oh) to provide an alternate persistence strategy. How might we better conform to the Single Responsibility Principle and the Dependency Injection Principle, and decouple our models from our persistence strategy? Stay tuned for part 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment