Opt-in to your business logic

Written by on

  Thanks to Bruno for the section on inheritance.

In a legendary post by Bryan Helmkamp, he talks about seven ways you can refactor fat models. All seven relate to extracting code into separate plain old Ruby objects and using them when needed. And this past week, our team had a discussion about inheritance and how it's frowned upon by some of the experts in the Ruby and Rails fields. After really paying attention to all of this and trying to get to the essence of it, here's what I've learned.

The main idea is this: opt-in to your business logic

Opt-in is synonymous with a white-list. Instead of allowing everything and deciding what to exclude (black-listing), you white-list instead. You make a conscious decision as to what should be allowed. You use a white list when you use attr_accessible. You are allowing certain attributes to be mass assigned.

In the case of your business logic, you have to make a conscious decision as to when business logic should execute. 

If you want to save a model from the console, it shouldn't trigger 5 callbacks.

If you have a bunch of callbacks in your model and all of them include some sort of conditional, that's bad. If you want to do some backend work or a migration, you'll have to trace through your code to figure out if the callbacks will run. You have to ask the question, do I even want these callbacks to run? 

Updating a simple field like the name of something should be an easy task.

The exception to this rule is persistence. Callbacks can be managed nicely if you stick to only using them for persistence and avoid using conditionals. If you have a bunch of conditionals on your callbacks, it means they don't execute every time. If your callbacks don't execute every time, they aren't part of the core domain logic and shouldn't be in the model anyway. 

Here's an example of callbacks with conditionals. I'm sure you've seen some code like this before. To figure out which ones will run, you'd have to also look for and understand what the conditionals will return.

Your object should do one thing and do it right every time.

Granularity matters. If you want to opt-in to your business logic, you should be able to opt-in to any combination. 

For example, if you want to post to social networks after a model is created, you should be able to pick and choose which ones you want. If I want to post to Facebook, it also shouldn't post to Twitter.

Take a look at the example below. If you don't want to post to Twitter, you can simply comment out the TwitterPoster and you're done. It's a much better option to keep these separate rather than use a more generic SocialNetworkPoster..

Inheritance is very rigid, usually there is a better option.

If you're writing some code that will be used by a couple of classes in your app, use inheritance only if you absolutely must.

It is common knowledge among folks who practice object oriented design (not just Rubyists) that 'composition should be preferred over inheritance'. In other words, if you were thinking about making a superclass and subclass objects, don't - most likely you can achieve the same thing by making a Ruby module.

Here's a simple example that nicely shows the limitations of inheritance.

If you use these methods and other to opt-in to your business logic, you'll find yourself enjoying your code more. You'll be able to refactor things more easily. You won't find yourself in callback hell and you'll be able to write classes composed of many smaller units. 

Once a unit works, it should work forever. That's the idea. You'll find that you won't have much churn in your units which leads to better code.