Skip to content

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
/* This is when a refactoring really pays off.
*
* In order to make your code more modular, avoid hard-coding assumptions (or refactor them away).
* The most fundamental, anti-modular assumption in Object-Oriented software is the concrete type of objects.
* Any time you write "new MyClass" in your code (or in Ruby MyClass.new) you've hardcoded
* an assumption about the concrete class of the object you're allocating. These makes it impossible, for example,
* for someone to later add logging around method invocations of that object, or timeouts, or whatever.
*
* In a very dynamic language like Ruby, open classes and method aliasing mitigate this problem, but
* they don't solve it. If you manipulate a class to add logging, all instances of that class will have
* logging; you can't take a surgical approach and say "just objects instantiated in this context".
*
* There are standard design patterns to mitigate this, namely Dependency Injection and Factories.
* By taking a Factory (a function that manufactures objects) as a parameter to a function (or
* a constructor) you allow a programmer to later change his mind about what Factory to provide; and
* this means the programmer can change the concrete types of objects as his heart desires.
*
* There is another pattern, the Decorator pattern, that makes this even more powerful. Instead of
* instantiating class A instead of class B, you instantiate an A with a B, where A delegates all
* of its methods to B except where it needs to add "decorations" such as logging.
*/
// In this example, I have a Query object, with methods like #execute(). I want to add timeouts around all queries. I start
// by creating a QueryProxy that routes all method invocations through an over-ridable method: #delegate:
abstract class QueryProxy(query: Query) extends Query {
def select[A](f: ResultSet => A) = delegate(query.select(f))
def execute() = delegate(query.execute())
def cancel() = query.cancel()
protected def delegate[A](f: => A) = f
}
// Then, to implement timeouts, I create a Query Decorator:
class TimingOutQuery(timeout: Duration, query: Query) extends QueryProxy(query) {
override def delegate[A](f: => A) = {
try {
Timeout(timeout) {
f
} {
cancel()
}
} catch {
case e: TimeoutException =>
throw new SqlTimeoutException
}
}
}
// The implementation of the Timeout function is provided here for the sake of curiosities. It uses threads and is weird but cool.
object Timeout {
val timer = new Timer("Timer thread", true)
def apply[T](timeout: Duration)(f: => T)(onTimeout: => Unit): T = {
@volatile var cancelled = false
val task = if (timeout.inMillis > 0) Some(schedule(timeout, { cancelled = true; onTimeout })) else None
try {
f
} finally {
task map { t =>
t.cancel()
timer.purge()
}
if (cancelled) throw new TimeoutException
}
}
private def schedule(timeout: Duration, f: => Unit) = {
val task = new TimerTask() {
override def run() { f }
}
timer.schedule(task, timeout.inMillis)
task
}
}
// The thing that ties this all together is to make sure that everybody that needs to instantiate a Query object
// never ever calls "new Query" directly. Provide instead a Factory:
class TimingOutQueryFactory(queryFactory: QueryFactory, timeout: Duration) extends QueryFactory {
def apply(connection: Connection, query: String, params: Any*) = {
new TimingOutQuery(timeout, queryFactory(connection, query, params: _*))
}
}
// Of course, this Factory takes another Factory, allowing Factories to be composed (so we have Factory Decorators that
// take Decorated Factories that make Decorated Queries! So meta!
// Actually, "meta" in Greek means nothing like "meta" in English. "Meta" plus
// the dative means "after" so Aristotle's Metaphysics is actually just a book "after [the book on] physics". Anyway.
// The "root" QueryFactory in this onion is the simplest QueryFactory and it just calls "new Query" etc.
// The thing that made me excited tonight, though, is that I had to add a new feature: per-query timeouts. We had a global
// 3-second timeout and this was proving to be stupid given that our most common query has a latency of 0.5ms and a standard
// deviation of 2ms. If you have a global timeout you must set your timeout around your most expensive query not your most
// common query (otherwise your most expensive query will always timeout!). But for a production system, cheap frequent queries,
// if they start exceeding 2 standard deviations, can take down your site. So a sensible timeout is like 5ms. But we had it
// set to 3,000 ms!! Yikes. Fortunately, this has not yet caused a problem but it's a time bomb.
// So anyway, how many lines of code is it to make per-query timeouts??
class PerQueryTimeoutSimpleTimingOutQueryFactory(queryFactory: QueryFactory, timeouts: Map[String, Duration]) extends QueryFactory {
def apply(connection: Connection, query: String, params: Any*) = {
new TimingOutQuery(timeouts(query), queryFactory(connection, query, params: _*)) // YAY!
}
}
// BOOM. 1 LOC. That's what modularity means in practice. By injecting the timeout value itself into the constructor of the
// TimingOutQuery, we can actually programatically modify it. Contrast this to the previous implementation, which effectively
// reached up into the air and snatched out a global variable. NOT modular.
// What's more, it's even more amazing not how short this code is but that it could be
// added by any programmer anywhere, regardless of whether they have access to the source code that actually instantiates and
// executes queries. And they can add it "surgically" -- just in this context or that -- not globally as the
// open classes / alias_method_chain pattern in Ruby entails.
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.