Thursday, 28 June 2012

Where's Your Business Logic? // Collective Idea

If I sat down with your code base and asked you how such-and-such a feature is implemented, what would you say? Would you lead me through controller filters and actions? What about model methods, includes, callbacks or observers? How about objects in lib/ (oh, and there?s also this other call to a mailer to make sure emails get sent out?), or would you even dive down into the database itself? Or would you send me all of these paths at once? Can you even remember or properly follow the code flow for a given use case? If any of this sounds familiar, and you?re nodding in a ?yeah I know but?? fashion, but feel stuck, I?ve got an answer.

Before we continue, if you haven?t watched Uncle Bob Martin?s keynote talk Architecture: The Lost Years yet, please go do so now. I?ve yet to find a better explanation of this problem nor a better solution than what Uncle Bob presents.

The fundamental problem with almost every Rails project (and I?m sure in other frameworks as well), is that there is no direct codifying of the business rules and use cases of the application. There is no single location you can point to and say ?here, these objects implement our use cases?. I put some of the blame on Rails itself, which has guided developers to use Controllers, Models, or Libraries, and nothing else. However I put most of the blame on us, the developers, for two reasons. First, we rarely spend enough time up front thinking about the problem space and designing a solution, and second, we haven?t been listening and reacting to test pain (you are doing TDD right?). Are your tests slow (>1s to run a single unit test)? Do your tests have large ungainly setup? Are you using factory_girl in your unit tests? Are you mocking implementation instead of interfaces?

If you answered ?yes? to any of these questions, your tests are screaming at you that your design is wrong or nonexistent. If you do TDD right, following the Red, Green Refactor cycle will lead you towards small, simple objects that do one thing and do it well. That said, getting from here to there is a very daunting task, but it?s not impossible.

Enter the Interactor. An Interactor handles a use case. It pulls together the models and libraries it needs to process a single business rule, and then it?s done. These objects are very easy to test and use and in proper OO fashion can be used anywhere the app needs to apply the use case or business rule. If an Interactor?s test ever feels painful, then the Interactor is probably doing too much and you actually have two rules being processed by one object, so refactor! Take control of your tests and your code again, and you?ll wonder why you never did this before (I sure did!).

I?m still working on the general API an Interactor should have, but this is what I currently recommend:

  • Interactor class names are Verbs: LogUserIn, ProcessComment, etc.
  • The constructor takes current-state information, e.g. the currently logged in user
  • It has one or more instance methods to fire off the process, I normally try to have one called #run.
  • These methods take any required parameterized information, like login and password from the user.
  • It can use other Interactors as needed

As an example, here?s my LogUserIn interactor from my personal project raidit that applies all five of these rules. This object takes a login type (:web, :api, :mobile, etc) and the login / password from the user, and applies the rules necessary to log the user in:

 require 'securerandom'  require 'models/user' require 'interactors/find_user' require 'repository'  class LogUserIn    attr_reader :login_type    def initialize(login_type)     @login_type = login_type   end    def run(login, password)     action = FindUser.new     user = action.by_login login     if user && user.password == password       user.set_login_token @login_type, new_login_token       user     else       nil     end   end    protected    def new_login_token     SecureRandom.hex(32)   end end 

The controller action that uses this Interactor is such:

   def create     action = LogUserIn.new :web      if user = action.run(params[:login], params[:password])       reset_session       cookies[:web_session_token] = {         value: user.login_token(:web),         httponly: true       }        redirect_to root_path     else       flash.now[:login_error] = true       render action: "new"     end   end 

As you can see, the controller action takes care of everything Rails should: setting cookies, passing in parameters, clearing out the session, showing messages and redirecting. Everything else not Rails specific is handled in the Interactor. The test for this Interactor can be found here: log_user_in_test.rb and if you look at unit/test_helper.rb you?ll notice that this test doesn?t load Rails at all; it?s not needed! This gives the test an extremely fast start-up and improves the TDD experience dramatically.

So I?ll ask again, Where is your Business Logic? Do you have a nicely Object Oriented application that Rails simply uses or is your code spread everywhere as from a shotgun? If it?s the latter, Interactors are a great way to start cleaning up your code and giving your application some structure and architecture.

patrick witt leprosy tampa bay buccaneers birdman whip it gabby giffords gabby giffords

No comments:

Post a Comment