When faced with real-life complexity, there is rarely 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 into 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 | CONS |
---|---|
purist | can be slow |
allows for modular design | 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 that we’re looking for - already prepared.
Let’s 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 the last week, who want to take the Java test, and who haven’t applied for any other tests but have logged in at least three times from Canada). This is trivial to fetch directly from the database but could be pretty complex otherwise.
PROS | CONS |
---|---|
SQL is 4GL | occurrence of massive SQL queries possible |
using the database directly is usually the fastest (performance-wise) | effort needed to isolate from the rest of application |
shines when used with a properly built ER data model | 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 the data we store inside 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) that could prevent the building of a 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.
“In my experience, 9 out of 10 times I was better off using SQL.”
Example
I’ll show a real-life example from our GymTrainer application which at the time of writing is 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 than something substantive.