Skip to content

@nkallen /MODULARITY_OLYMPICS.markdown
Created

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

2010 Modularity Olympics

This is a contest, open to programming languages from all nations, to write modular and extensible code to solve the following problem: Implement a service that can run queries on a database.

The Challenge

Sounds simple right? Wrong! A programmer without control over the source-code of that service must be able to later add enhancements such as statistics collecting, timeouts, memoization, and so forth. There are a few more requirements:

  1. the “enhancements” must be specified in a configuration object which is consumed at run-time (e.g., it could be based on user-input).
  2. The enhancements are ordered (stats collecting wraps timeouts, not the other way around) but it must be possible to reverse the order of the enhancements at run-time.
  3. The enhancements must be “surgical” and not “global”. That is, it must be possible to simultaneously have two query services, one reversed and one not reversed, and even have a query service without any enhancements.
  4. Your code must be thread-safe or at least support concurrency using some technique. There is a reason there is a connection pool here. The simultaneity requirement from #3 must mean your queries can be run in different threads/concurrently. (You can assume that basic datastructures like hashes are thread-safe for simplicity).

Most programming contests emphasize things that are not modularity. This contest emphasizes things that are modularity. And its the olympics and don’t you want to be olympic? So go ahead and prove to the world that Design Patterns are no longer necessary, or that Haskell is the raddest thing since the movie Rad, or that Peanut Butter is better than Jelly. This is much more fun than inflamatory blog posts and angry reddit comments, yes? Yes.

How to Play

Fork this gist!! See my sample

http://magicscalingsprinkles.wordpress.com/2010/02/16/2010-modularity-olympics/

Output

Your program must produce exactly this output. Note the reversing order of the enhancements and the memoization of Query instantiation:

Forward:
Instantiating Query Object
Selecting SELECT ... FROM ... FOR UPDATE ... on #<Object:0x11f6750>
Did not timeout! Yay fast database!
Measured select at 1.00 seconds
Instantiating Query Object
Executing INSERT ... on #<Object:0x11f6750>
Did not timeout! Yay fast database!
Measured select at 1.00 seconds
Executing INSERT ... on #<Object:0x11f6750>
Did not timeout! Yay fast database!
Measured select at 1.00 seconds

Backward:
Instantiating Query Object
Selecting SELECT ... FROM ... FOR UPDATE ... on #<Object:0x11f4ea0>
Measured select at 1.00 seconds
Did not timeout! Yay fast database!
Instantiating Query Object
Executing INSERT ... on #<Object:0x11f4ea0>
Measured select at 1.00 seconds
Did not timeout! Yay fast database!
Executing INSERT ... on #<Object:0x11f4ea0>
Measured select at 1.00 seconds
Did not timeout! Yay fast database!
require 'rubygems'
require 'activesupport'
require 'timeout'
class MemoizingQueryFactory
def initialize(query_factory)
@memo = Hash.new do |h, k|
puts "Instantiating Query Object"
h[k] = query_factory.new(*k)
end
end
def new(connection, query_string, *args)
@memo[[connection, query_string, args]]
end
end
class QueryEvaluator
def initialize(connection_pool, query_factory = Query)
@query_factory = query_factory
@connection_pool = connection_pool
end
def select(query_string, *args)
@connection_pool.with_connection do |connection|
@query_factory.new(connection, query_string, *args).select
end
end
def execute(query_string, *args)
@connection_pool.with_connection do |connection|
@query_factory.new(connection, query_string, *args).execute
end
end
def transaction
@connection_pool.with_connection do |connection|
yield TransactionalQueryEvaluator.new(connection, @query_factory)
end
end
end
class TransactionalQueryEvaluator
def initialize(connection, query_factory)
@connection = connection
@query_factory = query_factory
end
def select(query_string, *args)
@query_factory.new(@connection, query_string, *args).select
end
def execute(query_string, *args)
@query_factory.new(@connection, query_string, *args).execute
end
def transaction
yield self
end
end
class QueryProxy
attr_accessor :query
def initialize(query)
@query = query
end
def select
delegate("select") { @query.select }
end
def execute
delegate("execute") { @query.execute }
end
def reverse
case @query
when QueryProxy
reverse = @query.reverse
inner = @query.query
@query = inner
reverse.query = self
reverse
else
self
end
end
end
class ReversingQueryFactory
def initialize(query_factory)
@query_factory = query_factory
end
def new(connection, query_factory, *args)
@query_factory.new(connection, query_factory, *args).reverse
end
end
class TimingOutQueryFactory
def initialize(query_factory, timeout)
@query_factory = query_factory
@timeout = timeout
end
def new(connection, query_string, *args)
TimingOutQuery.new(@query_factory.new(connection, query_string, *args), @timeout)
end
end
class TimingOutQuery < QueryProxy
def initialize(query, timeout)
super(query)
@timeout = timeout
end
def delegate(method)
result = Timeout.timeout(@timeout.to_i) { yield }
puts "Did not timeout! Yay fast database!"
result
end
end
class StatsCollectingQueryFactory
def initialize(query_factory, stats)
@query_factory = query_factory
@stats = stats
end
def new(connection, query_string, *args)
StatsCollectingQuery.new(@query_factory.new(connection, query_string, *args), @stats)
end
end
class StatsCollectingQuery < QueryProxy
def initialize(query, stats)
super(query)
@stats = stats
end
def delegate(method)
@stats.measure(method) { yield }
end
end
class Query
def initialize(connection, query_string, *args)
@connection = connection
@query_string = query_string
@args = args
end
def select
sleep 1
puts "Selecting #{@query_string} on #{@connection}"
[1, 2, 3]
end
def execute
sleep 1
puts "Executing #{@query_string} on #{@connection}"
1
end
end
class ConnectionPool
def initialize(size)
@size = size
end
def with_connection
yield Object.new
end
end
class Stats
def measure(name)
result = nil
bm = Benchmark.measure { result = yield }
puts "Measured #{name} at #{"%.2f" % bm.real} seconds"
result
end
end
config = [
[:memoizing, []],
[:timing_out, [2.seconds]],
[:stats_collecting, [Stats.new]]
]
query_factory = config.inject(Query) do |factory, (decorator_name, settings)|
"#{decorator_name.to_s.classify}QueryFactory".constantize.new(factory, *settings)
end
def run_test(query_evaluator)
query_evaluator.transaction do |t|
t.select("SELECT ... FROM ... FOR UPDATE ...")
t.execute("INSERT ...")
t.execute("INSERT ...")
end
end
puts "Forward:"
run_test(QueryEvaluator.new(ConnectionPool.new(20), query_factory))
puts
puts "Backward:"
run_test(QueryEvaluator.new(ConnectionPool.new(20), ReversingQueryFactory.new(query_factory)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.