Duplicated business logic in Ruby and SQL

Viewed 122

I have a PORO (Plain Old Ruby Object) to deal with some business logic. It receives an ActiveRecord object and classify it. For the sake of simplicity, take the following as an example:

class Classificator
    STATES = {
      1 => "Positive",
      2 => "Neutral",
      3 => "Negative"
    }

    def initializer(item)
      @item = item
    end

    def name
      STATES.fetch(state_id)
    end

    private

    def state_id
      return 1 if @item.value > 0
      return 2 if @item.value == 0
      return 3 if @item.value < 0
    end
end

However, I also want to do queries that groups objects based on these state_id "virtual attribute". I'm currently dealing with that by creating this attribute in the SQL queries and using it in GROUP BY statements. See the example:

class Classificator::Query
  SQL_CONDITIONS = {
    1 => "items.value > 0",
    2 => "items.value = 0",
    3 => "items.value < 0"
  }

  def initialize(relation = Item.all)
    @relation = relation
  end

  def count
    @relation.select(group_conditions).group('state_id').count
  end

  private
  def group_conditions
    'CASE ' + SQL_CONDITIONS.map do |k, v|
      'WHEN ' + v.to_s + " THEN " + k.to_s
    end.join(' ') + " END AS state_id"
  end
end

This way, I can get this business logic into SQL and make this kind of query in a very efficient way.

The problem is: I have duplicated business logic. It exists in "ruby" code, to classify a single object and also in "SQL" to classify a collection of objects in database-level.

Is it a bad practice? Is there a way to avoid this? I actually was able to do this, doing the following:

item = Item.find(4)
items.select(group_conditions).where(id: item.id).select('state_id')

But by doing this, I loose the ability to classify objects that are not persisted in database. The other way out would be classifying each object in ruby, using an Iterator, but then I would lose database performance.

It's seem to be unavoidable to keep duplicated business logic if I need the best of the two cases. But I just want to be sure about this. :)

Thanks!

3 Answers

ActiveRecord itself implies a degree of coupling between your persistence and business logic. However, as much as the pattern allows, and if you don't have real performance constraints, the first option should be to keep your persistence code as dumb as possible, and move this "classification" (which is clearly a business rule) away from the database as much as possible.

The rationale is that database-related code is more expensive to change (especially as your system is already in production) and generally more difficult and slower to test than pure business logic.

Related