Lev

Rails is fantastic and obviously super successful. Lev is an attempt to improve Rails by:

  1. Providing a better, more structured, and more organized way to implement code features
  2. De-emphasizing the "model is king" mindset when appropriate

Rails' MVC-view of the world is very compelling and provides a sturdy scaffold with which to create applications quickly. However, sometimes it can lead to business logic code getting a little spread out. When trying to figure out where to best put business logic, you often hear folks recommending "fat models" and "skinny controllers". They are saying that the business logic of your app should live in the model classes and not in the controllers. I agree that the logic shouldn't live in the controllers, but I also argue that it shouldn't always live in the models either, especially when that logic touches multiple models.

When all of the business logic lives in the models, some bad things can happen:

  1. your models can become bloated with code that only applies to certain features
  2. your models end up knowing way too much about other models, sometimes multiple hops away
  3. your business logic gets spread all over the place. The execution of one "feature" can jump between bits of code in multiple models, their various ActiveRecord life cycle callbacks (before_create, etc), and their associated observers.

Lev introduces "routines" which you can think of as pieces of code that have all the responsibility for making one thing (use case) happen, e.g. "add an email to a user", "register a student to a class", etc).

Routines...

  1. Can call other routines
  2. Have a common error reporting framework
  3. Run within a single transaction with a controllable isolation level

Handlers are specialized routines that take user input (e.g. form data) and then take an action based on that input.

Handlers...

  1. Help you verify that the calling user is authorized to run the handler
  2. Provide ways to validate incoming parameters in a very ActiveModel-like way (even when the parameters are not associated with a model)
  3. Integrate will with basic routines
  4. Map one-to-one with controller actions; by keeping the logic in each controller action encapsulated in a Handler, the code becomes independently-testable and also prevents the controller from being "fat" with 7 different actions all containing disparate logic touching different models.

In a Lev-oriented Rails app, controllers are just responsible for connecting routes to Handlers. In fact, controller methods just end up being calls to handle_with(MyHandler), handle_with being a helper method provided by Lev.

Lev also provides a lev_form_for form builder to replace form_for. This builder integrates well with the error reporting infrastructure in routines and handlers, and in general is a nice way to get away from forms that are very single-model-centric.

When using Lev, model classes have the following responsibilities:

  1. Hook into the ORM (i.e., inherit from ActiveRecord::Base)
  2. Establish relationships to other models (e.g. belongs_to, has_many, including dependent: :destroy)
  3. Validate internal state
    1. Do not validate state in related models
    2. Can validate the presence of relationship (can check that a foreign key is present)
  4. Can perform queries when those queries only use internal model state and are aware of internal model state (e.g. arguments to queries should be in the language of the model state)
  5. Can create records when those creations only need values internal to this model and take arguments in the language of the internal model state.

The result of the principles above and below, model classes end up being very small. This is good because a lot of code depends on the models and having the be small normally means they are also stable.

Naming Conventions

As mentioned above, a handler is intended to replace the logic in one controller action. As such, one convention that works well is to name a handler based on the controller name and the action name, e.g. for the ProductsController#show action, we would have a handler named ProductsShow.

Routines on the other hand are more or less glorified functions that work with multiple models to get something done, so we typically start their names with verbs, e.g. CreateUser, SetPassword, ConfirmEmail, etc.

Differences between Lev and Rails' Concerns

Both Lev and Concerns remove lines of code from models, but the major difference between the two is that with Concerns, the code still lives logically in the models whereas code in Lev is completely outside of and separate from the models.

Lev's routines (and handlers) know about models, but the models don't know anything about nor are they dependent on the code in routines. This makes the models simpler and more stable (a Good Thing).

Since a Concern's code is essentially embedded in model code, if that Concern breaks it can potentially break other unrelated features, something that can't happen with routines.

Installation

Add this line to your application's Gemfile:

gem 'lev'

And then execute:

$ bundle

Or install it yourself as:

$ gem install lev

Usage

For the moment, details on how to use lev live in big sections of comments at the top of:

TBD: talk about delegate_to_routine.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request