This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 nobody that needs to instantiate a Query object | |
// 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 | |
// 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 set your timeout around your most expensive query not your most | |
// common query. But for a production system, rare expensive queries are less likely to cause performance problems than frequent | |
// cheap queries getting slightly less cheap. 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment