Programming ≈ Fun

Written by Krešimir Bojčić

Taming of Complex Business Logic

When faced with real life complexity there rarely is an easy way out or a silver bullet solution. Here I will describe the model I’ve come up with during years of trial and error. The solution presented here is as far as I know not a mainstream one and it could be considered somewhat controversial.

Object Oriented Approach

Here we’re living in OO land. Each and every problem can be broken down to objects - we test them out both individually and jointly. We combine them in beautiful ways and then they produce the result we aim for.

Pros

  • purist
  • allows for modular design

Cons

  • can be slow
  • complicated for complex cases
  • ravioli code syndrome
  • additional layer of complexity by wrapping raw data into other structures

Database Driven Approach

Also known as writing big SQL queries. Here we rely on SQL to capture business requirements and we ignore objects altogether. Our aim is to join multiple database entities based on their relation and business rules. What we end up with is just the data we that we’re looking for - already prepared.

Lets look at an example: Imagine we are building a new programmer marketplace (but better than existing ones) and we need to list users who have applied for our platform during last week, who want to take the Java test and who haven’t applied for any other tests but have logged in at least 3 times from Canada). This is trivial to fetch directly from database but could be pretty complex otherwise.

Pros:

  • SQL is 4GL
  • using the database directly is usually the fastest (performance-wise)
  • shines when used with a properly built ER data model

Cons

  • occurrence of massive SQL queries possible
  • effort needed to isolate from the rest of application
  • needs an adapter so the rest of the application can use it

What to choose

It is not a question of ‘what to choose’. It is a question of ‘when to choose’

If the problem is mostly based on data we store inside of the database then I would definitely go with the database approach. The way I see it - get it working, find any possible holes in the data model (happens quite often with new functionality) which could prevent the building of solution and then if it looks like a bad fit I can always switch to a different approach (which in turn happens very rarely). This allows me to do a reality check without investing too much in “beautiful” OO structures that are needed otherwise.

If the problem is algorithmic and needs extensive testing I mostly go with the OO approach. This gives us the shortest route to the solution. In all the projects that I’ve worked on so far the ratio was somewhere around 90% in favor of the database-driven approach

From my experience 9 out of 10 times I was better off with using SQL.

Example

I’ll show a real life example from our GymTrainer application which is at the time of writing still being built. We are trying to figure out the weight being lifted during workouts for our visualization.

class ProgressBusinessObject
  def self.weight_graph_data(user_id, category)
    #express logic in 4GL language
    #easy to reason about
    #easy to change and adjust
    progress_sql = <<-SQL
      SELECT Date(logged_at),
             Max(logged_weight)                AS weight,
             workout_items.unit,
             workout_set_groups.set_group_name AS category
      FROM   workout_items
             INNER JOIN workout_set_groups
                     ON workout_set_groups.id = workout_items.set_group_id
      WHERE  user_id = #{ActiveRecord::Base.connection.quote(user_id)}
             AND logged = true
             AND workout_set_groups.set_group_name = #{ActiveRecord::Base.connection.quote(category)}
      GROUP  BY Date(logged_at),
                workout_set_groups.set_group_name,
                workout_items.unit
      ORDER  BY Date(logged_at)
    SQL

    #put results in object for the rest of application to seamlessly use
    (ActiveRecord::Base.connection.execute progress_sql).map { |i| WeightGraphItem.new(i) }
  end
end

In a sense we just get the view of the data that we need. In order to use this more or less normally I am wrapping SQL query based results to a “data view” object named WeightGraphItem

#weight_graph_item.rb
class WeightGraphItem < KodiusPoro
  #attributes and types, hydrated from business object
  attr_accessor :date, :weight, :category, :unit
  define_types date: Date, weight: Float

  #helper method
  def prepare_for_highstock_charts
    [ date.to_time.to_i.to_s.ljust(13, '0').to_i , weight]
  end
end

This is a tangential remark but this KodiusPoro is here just to initialize attributes without manual assignment and to ensure correct data types (date, float etc..)

#kodius_poro.rb
class KodiusPoro
  def self.define_types(type_hash)
    @@types = type_hash
  end

  def initialize(h)
    @@types ||= {}
    h.each do |k,v|
       if @@types[k.to_sym] != nil
         tip = @@types[k.to_sym]
         v = v.to_f if tip == Float
         v = tip.parse(v) if tip == Date
       end
       public_send("#{k}=",v)
    end
  end
end

Conclusion

I don’t see any monumental downsides to this approach compared to the OO approach. It can certainly look a tad odd and perhaps slightly “unclean”, but as far as I’m concerned this is more of a superficial issue and has more to do with existing prejudices against the approach rather than something substantive.

Comments