ywen.in.coding

Everythng I do

My Coding Standard for Rails Projects (Part 2)

| Comments

Builders

In the part 1, the code snippet about the controller has a line

1
user = Builders::User.new(params).build

This implies that a Builder Pattern is used for creating a model, more precisely, creating an “Aggregate” in the Domain Driven Design terminology. The return value of a builder should always be the root object of the aggregate. Below is an example of a build method.

User Builder (builder.rb) download
1
2
3
4
5
6
7
8
9
10
11
module Builders
  class User < Builder
    def build
      BusinessModel::User.build(params).tap do |business_object|
        business_object.email_addresses = [EmailAddress.new(params).build]
        business_object.phones = [Phone.new(params).build]
        business_object.address = Address.new(params).build
      end
    end
  end
end

A DRY version of a typical builder in the form of an internal DSL could be:

1
2
3
4
5
6
7
module Builders
  class User
    build_for :user,
      :has_many_associations => [:email_addresses, :phones],
      :has_one_associations => [:address]
  end
end

A lof of Rails projects I have seen have building email_addresses and phones logic in the UsersController, which then becomes hard to unit test and impossible to reuse.

Another replacement to the builders is using hooks, such as after_create. This makes your code highly depends on a specific ORM implementation, making the one line user.create! becomes hard to understand, and slow the unit tests.

The User BusinessModel should be responsible building a user and only it. Below is a possible implementation:

BusinessModel Builder (business_model_builder.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module BusinessModel
  class User
    class << self
      def build(params)
        self.new(valid_params(params)).tap do |object|
          raise ObjectInvalidError.new(object.errors) unless object.valid?
        end
      end

      private
      def valid_params(params)
        [:id, :first_name, :last_name].inject({}) do |attributes, key|
          attributes.merge!(params[key])
        end
      end
    end
  end
end

This, as usual, can be abstracted into a DSL:

1
2
3
4
5
6
7
8
9
10
11
12
module BusinessModel
  class User
    class << self
      private
      def valid_params
        ...
      end
    end

    have_builder_method
  end
end

Business Models

A business model, in Domain Driven Design term, is a Entity. A business model should be persisted, but the model itself knows nothing about how itself being persisted, it delegates such a task to its persistence class.

A business model, contains attributes and business-related calculations based on these atrributes.

A business model also contains the validations that can be called by user.valid?

Persistence Classes

In the controller code in the part 1:

1
Persistence::User.new(user).persist

A Persistence class takes the business model object as its only parameter in its constructor and the persists the object. The Persistence object knows to how to map a model object’s attributes into database columns, for example. An example of such a method could be like this:

Uer Persistence Class (persistence_create.rb) download
1
2
3
4
5
6
7
8
9
module Persistence
  class User
    def persist
      ActiveRecordStore::User.new(business_object).save
      #save part of attributes to redis
      RedisStore::User.new(business_object).save
    end
  end
end

Buidling a business model from the persistence layer might look like this:

Uer Persistence Class (persistence_load.rb) download
1
2
3
4
5
6
7
8
module Persistence
  class User
    def load
      user = ActiveRecordStore::User.new(business_object).load
      RedisStore::User.new(user).load
    end
  end
end

The ActiveRecordStore::User in the code could be smart enough to load a user based on what attributes in the business_object. For example:

ActiveRecordStore example (active_record_store_load.rb) download
1
2
3
4
5
6
7
8
module ActiveRecordStore
  class User < Store
    def load
      non_empty_attributes = HashHelper.new(physical_attributes).non_blanks
      Physical::User.all(non_empty_attributes)
    end
  end
end

The code bears some explainations: physical_attributes is a method to transform the business model object attributes into the table columns. This method is used by both save and load. The Physical::User is a child of ActiveRecord::Base that talks to the database directly.

Some custom methods might be needed, such as find_by_email_address. These kind of finders, though, can be easily standardized.

1
generate_has_many_association_finders :user, :association_name => :email_addresses

DSLs

All the above examples can often be DRYed up by some internal DSLs. But I feel that I must say that these DSLs should not be overused.

A sympton of overuse of DSL (and thus over abstract the logic) is that the DSL allows too much options. When you find out you have to handle more than 2 slightly different situations in one DSL implementation method, it is time to seperate the situations into 2 or more slightly different DSL macros. And of course these DSL macros implementations can share a large portation of logic by abstract the common code out.

Comments