Skip to content

Instantly share code, notes, and snippets.

@nkallen
Created February 15, 2010 19:25
Show Gist options
  • Save nkallen/304913 to your computer and use it in GitHub Desktop.
Save nkallen/304913 to your computer and use it in GitHub Desktop.

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. Please bear in mind the use of connection pools and such.

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) # let's pretend this is thread-safe, kthx ruby!
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 = reverse.query
clone = dup
clone.query = inner
reverse.query = clone
reverse
else
self
end
end
def dup
clone = super
clone.query = @query.dup
clone
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
[query_factory, ReversingQueryFactory.new(query_factory)].each do |qf|
query_evaluator = QueryEvaluator.new(ConnectionPool.new(20), qf)
query_evaluator.transaction do |t|
t.select("SELECT ... FROM ... FOR UPDATE ...")
t.execute("INSERT ...")
t.execute("INSERT ...")
end
puts
end
# let me know if you see any bugs but I think this is good and thread-safe now (with caveats noted
# inline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment