ActiveRecord anti-patterns which you should avoid. Part 1: Conditional validations

Subscribe to my newsletter and never miss my upcoming articles

Imagine that you develop an e-commerce platform. Your basic model is Order.

You would have some constraints which don't have to be valid before order is placed:

validates :order_items, length: {minimum: 1}, unless: :cart?
validates_presence_of :delivery_address, :delivery_type, :payment_type, unless: :cart?

You would alse need some validations which have to be checked on cart submition:

validates_inclusion_of :delivery_type, in: :available_delivery_types, on: :being_placed_via_cart
validates_inclusion_of :payment_type, in: :available_payment_types, on: :being_placed_via_cart
validates_acceptance_of :shop_rules_acceptance, :terms_of_anti_spam_policy_acceptance, on: :being_placed_via_cart

... but not always:

validates :electronic_order_rules_acceptance, acceptance: true, if: :electronic_order?, on: :being_placed_via_cart

Others should be checked only if the order is passed through an external system.

validates_presence_of :external_id, on: :being_placed_via_api

All the above leads to situations in which you get confused which validations would be triggered on certain save.

So... What to do instead?

I suggest using form/command objects.

class PlaceOrderViaExternalApi < PlaceOrder
  validates_presence_of :external_id
end

class PlaceOrder < Command
  #...
end

class Command
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations
end

PlaceOrderViaExternalApi.new(order_params).valid?

You can initialize and validate them just like ActiveRecord objects but your model's code keeps cleaner and more maintainable.

No Comments Yet