Search code examples
ruby-on-railsoopmodelsmixins

Better practice to modularize code in mixins? Or just put all the code directly into the model?


The title kind of says it all. I prefer to modularize code into mixins and include them into the model. Other people like to put the code directly into the model, potentially growing the models into MOUS's, models of unusual size.

I'm wondering what you guys do/what's better practice out there in dev land.


Solution

  • Modularizing code as a means to encapsulate independent concerns or behaviors is a good practice, but be careful of correlating short class definitions with “small” objects. The “size” of an object is better measured by the surface area exposed to other objects; in the case of Ruby, methods that are not marked protected or private are a great way of measuring this.

    You must also be aware of and account for objects that are tightly coupled to other objects. Modules can help this when used as mixins, but that is not always the case. ActiveRecord breaks behavior into mixins for easy composition (e.g. Validations, Callbacks, and Dirty Tracking), but the Base object combines these in a way that makes ActiveRecord models tightly coupled to the underlying codebase.

    This is not always a bad thing. David Heinemeier Hansson (@dhh) makes a compelling case for why this sort of “violation” can sometimes create very useful, beneficial interfaces and objects.

    In practice, I avoid delegating much additional behavior to ActiveRecord models. When additional behavior is necessary, more specific objects can be wrapped around the model, enhancing them while separating the behavior and code. For example:

    class UserDecorator
      attr_reader :user
      delegate :first_name, :last_name, to: :user
    
      def initialize(user)
        @user = user
      end
    
      def name
        "#{first_name} #{last_name}"
      end
    end
    

    name is a pretty common method to be on a User object, but the behavior doesn't relate to anything that is persisted. By adding user#name to the public interface of an ActiveRecord object, there is an implicit expectation that user#name= also exists, and that these map to a datastore. By moving even simple methods like this to a more appropriate object, the interface is clearer, more extensible, and more readily tested.

    Note: If you have zero other methods defined on User, and you're not already using Decorators, it would probably make more sense to start by defining the name method on User and extracting it later as complexity is added. Reaching for the most complex solution is a surefire way to build a needlessly complicated application. ;)