Rado's blog

Dealing with form objects

Let's say - we have the following nice clean model for our new application:

class Label < ActiveRecord::Base  
  belogns_to :user, required: true

  validates :name, presence: true, uniquness: true
end  

Several months later, we have the requirement to add quote and description to Label and those attributes should be mandatory. So we update Label:

class Label < ActiveRecord::Base  
  belogns_to :user, required: true

  validates :name, presence: true, uniquness: true

  validates :quote, :description, presence: true
 end

Now we have a big problem. All of our previous Label records are invalid. Because those new attributes didn't exist an hour ago. Also quote and description are not the kind of fields we add good default values.

So we revert this change and start thinking about a better way to handle the situation.

When a little more thought is put into this - we find that we need this validation to be applied in two places - my/labels/create and my/labels/update pages.

One pattern I have seen used for dealing with this is the following:

class Label < ActiveRecord::Base  
  belogns_to :user, required: true

  validates :name, presence: true, uniquness: true

  validates :quote, presence: true, if: :validating_as_designer?
  validates :description, presence: true, if: :validating_as_designer?

  def update_as_designer(attributes = {})
    @validating_as_designer = true

    update_attributes(attributes).tap do
      @validating_as_designer = false
    end
  end

  private

  def validating_as_designer?
    @validating_as_designer
  end
end  

In a newish model, this approach doesn't look so bad.

But this becomes unmaintainable easy. The main issue here is that we are adding form specific logic into a domain model. Now this model knows about certain pages. In my experience, such things tend to happen for the core modals of the application, which are bigger by nature.

Also, I have seen situations where a new developer enters the project, see that update_as_designer is used and decided that "this is the way they do stuff here" and adds update_as_admin.

So what is a better way to deal with the issue? I tend to favor the extraction of form objects:

class DesignerForm  
  include ActiveModel::Model

  attr_accessor :label, :user, :name, :quote, :description

  validates :user, presence: true
  validates :name, presence: true
  validates :quote, presence: true
  validates :description, presence: true

  def initialize(label = Label.new)
    @label = label
  end

  def persisted?
    @label.persisted?
  end

  def update(attributes)
    attributes.each do |name, value|
      public_send "#{name}=", value
    end

    if valid?
      @label.update attributes
    else
      false
    end
  end
end  

That sure looks too complicated! I can understand why people decide to use the "nice short method" instead of this big bulky object. But this is just our starting point.

Let's apply some minor modules; I use in almost every project I'm working on.

class DesignerForm  
  include ActiveModel::Model
  include ActiveModel::Attributes

  attr_reader :label

  delegate :persisted?, to: label

  # ActiveModel::Attributes method:
  # share attributes between form object and label
  attributes :user, :name, :quote, :description, delegate: :label

  # ValidValidator:
  # runs label validations and copies the errors to the form
  validates :label, valid: true

  # this is form specific validations we need
  validates :quote, presence: true
  validates :description, presence: true

  def initialize(label = Label.new)
    @label = label
  end

  def update(attributes)
    # ActiveModel::Attributes method
    self.attributes = attributes

    if valid?
      @label.save!
      true
    else
      false
    end
  end
end  

That is a lot better. Still not perfect. In my experience after having 2-3 form objects in the application - more and more common functionally is extracted. In the end, most of the form objects become something like following:

class DesignerForm  
  include BaseForm

  model :label, attributes: %i(user name quote description) 

  validates :quote, presence: true
  validates :description, presence: true
end  

I don't start with BaseForm because every project has slightly different requirements for handling forms.

p.s. As expected - there a several gems which provide nice apis for form objects. I don't use any of them.

SearchObject 1.1

Today I release version 1.1 of my Search Object. It has two major new features.

Using instance method for straight dispatch

Suggested and developed by Genadi Samokovarov:

class ProductSearch  
  include SearchObject.module

  scope { Product.all }

  option :date, with: :parse_dates

  private

  def parse_dates(scope, value)
    # some "magic" method to parse dates
  end
end  
Classes mixed with SearchObject can be inherited

I had this setting on a branch for a long time. In one of the projects I worked on it would have been usefull to have a search base object:

class BaseSearch  
  include SearchObject.module

  # ... options and configuration
end

# and used as 
class ProductSearch < BaseSearch  
  scope { Product }
end  

Its implementation actually might push me into an interesting refactoring.

Other minor features
  • I added Rubocop for verifying the project
  • I started testing against Ruby 2.2
  • I stopped testing against Ruby 1.9

Preserving named arguments on inheritance

I don't use inheritance very often, but sometimes it is the best solution. Ruby is not much carrying when you overwrite a method. The method is just replaced by the new method. It doesn't care about arguments at all.

Since 2.0, Ruby support named arguments. I'm starting to use them more and more. But they are a bit tricky when overwriting.

Let's say we have a class which creates user objects named UserBuilder:

class UserBuilder  
  def create(name: )
    puts "name: #{name}"
  end
end  

When I have to create a CustomerBuilder, which creates customers (user with address), I can write it like:

class CustomerBuilder < UserBuilder  
  def create(address:, name:)
    super name: name
    puts "address: #{address}"
  end
end  

This works but creates a coupling between UserBuilder and CustomerBuilder. This coupling is a bit tricky to catch. Imagine the following scenarios:

  • name become optional in UserBuilder
  • name is renamed to full_name in UserBuilder
  • name is split into first_name and last_name in UserBuilder

In all of those cases change in UserBuilder triggers changes in CustomerBuilder.

Such dependencies are toxic. Here is a better solution:

class CustomerBuilder < UserBuilder  
  def create(address:, **args)
    super **args
    puts "address: #{address}"
  end
end  

Now we can add, change, remove named arguments to UserBuilder#create and CustomerBuilder#create is not affected. Named arguments are just delegated down the change.

Not Just Code Monkeys

I really liked Martin Fowler's talk at OOP 2014:

That was part II of his keynote. Part I was Workflows of Refactoring.