Saturday, October 16, 2010

Scala exercise 3: decorator and composite design patterns

Introduction


This is the third  exercise in my Scala exercises series. If you haven’t seen it before, you may want to start from exercise 1: template method and exercise 2: observer design pattern. Below is exercise 3: decorator and composite design patterns.
Problem: In most UI frameworks including JSF, Wicket or Swing, you will need to provide a callback/listener object to handle requests from the user. Typically in such a callback, if there are some errors, you'd like to display a specific error message instead of propagating it to the framework, otherwise the framework would simply display a generic error to the user.
To hand code such a callback, you may do it like:

new Callback() {
def onCallback(ev: Any) {
try {
//perform the business logic here
} catch {
case e: LoginException => {
//assuming that error() will display the error
error("failed to login")
}
case e: SQLException => {
error("error accessing the database")
}
}
}
}

The problem with this approach is that there is a lot of boilerplate code there, while most usually we only want to say for exception class E1, display some error message M1:

new ErrorHandlingCallback(
//classOf[Foo] is the same as Foo.class in Java
classOf[LoginException], "failed to login",
classOf[SQLException], "...") {
def performBusinessLogic(ev: Any) {
//perform the business logic here
}
}

But what if you'd like to extract some information from the exception and include it into the error message or would like to do something special? Then, ideally, you should be able to specify a function as the error handler:

new ErrorHandlingCallback(
classOf[LoginException], "failed to login",
classOf[SQLException], "...",
(e: Exception) => doSomething(e)) {
def performBusinessLogic(ev: Any) {
//perform the business logic here
}
}

Finally, you should be able to pre-define an object to handle the commonly seen exceptions:

//ideally you should be able to "add" the error
//handlers together to get a compound error
//handler
val defaultErrorHandler =
(classOf[IOException], "I/O error") +
(classOf[Exception], "Unknown catch all error")

new ErrorHandlingCallback(
classOf[LoginException], "failed to login",
...,
defaultErrorHandler) {
def performBusinessLogic(ev: Any) {
//perform the business logic here
}
}

Your task is to complete the code below and create the other necessary classes as needed:

trait Callback {
def onCallback(ev: Any)
}

abstract class ErrorHandlingCallback(errorHandler: ErrorHandler) extends Callback {
//overload the constructor to take multiple error handlers (the star does that)
def this(errorHandlers: ErrorHandler*) = ...

def performBusinessLogic(ev: Any)

def onCallback(ev: Any) {
...
}
}

object ErrorHandlerUtil {
//allow you to use a function as an error handler
implicit def fromFunc(f: Exception => Boolean): ErrorHandler = ...
//allow you to use a pair (error class, error message) as an error handler
implicit def fromPair(p: (Class[_ <: Exception], String)): ErrorHandler = ...
}

Then, the following code should compile and run:

object ErrorHandlerTest {
//in order to use the implicit conversion methods, you
//must import these objects so that those methods can
//be invoked without a prefix.
import ErrorHandlerUtil._

def main(args: Array[String]) {
//assume that this is the default error handler in this context
//classOf[Foo] is the same as Foo.class in Java
val defaultErrorHandler = (classOf[IOException], "I/O error") + (classOf[Exception], "Unknown catch all error")
//create a decorator to handle additional errors
val decorator = new ErrorHandlingCallback(
//convert a pair to an error handler
(classOf[IndexOutOfBoundsException], "index out of bound"),
//ditto
(classOf[NullPointerException], "hit a null pointer"),
//you can define a custom error handler using a function to, say,
//access the info in the exception (not just its class).
(e: Exception) => if (e.getMessage.contains("xyz")) {
println(e.getMessage)
true //indicate that it has been handled
} else false,
//specify the default error handler here
defaultErrorHandler) {
def performBusinessLogic(ev: Any) {
println("called")
ev match {
//do nothing. No error.
case "foo" =>
//try to access the 100th element of an array which has only 3 elements
case "bar" => Array[Int](1, 2, 3).apply(100)
//Try to call a method on null
case "baz" => null.equals("oops!")
//throw a custom exception
case "baz2" => throw new RuntimeException("I am xyz!")
//divided by zero (something unexpected to test the ultimate fallback)
case "baz3" => 100 / 0
}
}
}
decorator.onCallback("foo")
decorator.onCallback("bar")
decorator.onCallback("baz")
decorator.onCallback("baz2")
decorator.onCallback("baz3")
}
}

Try to do it now! Then, click here to see the answer.

4 comments:

  1. Alternatively you could use case sequences as partial functions. Scala provides the PartialFunction trait for this purpose. With this approach you don't need to write the ErrorHandler class, the implicit conversion functions, or any other plumbing. The extra complexity introduced by the Java design pattern approach is eliminated.
    The callback class can be defined like this:
    abstract class ErrorHandlingCallback(handler: PartialFunction[Exception, String]) extends Callback {
    def performBusinessLogic(ev: Any)
    def onCallback(ev: Any) {
    try {
    performBusinessLogic(ev: Any)
    }
    catch {
    case e: Exception => println(handler.apply(e))
    }
    }
    }
    The default handler can be defined like this:
    val defaultHandler: PartialFunction[Exception, String] = {
    case e: IOException => "I/O error"
    case _ => "Unknown catch all error"
    }
    The custom handler can be defined like this:

    val handler: PartialFunction[Exception, String] = {
    case e: IndexOutOfBoundsException => "index out of bound"
    case e: NullPointerException => "hit a null pointer"
    case e =>
    if (e.getMessage.contains("xyz")) {
    e.getMessage
    }
    else defaultHandler.apply(e)
    }
    The callback instantiation is now simplified to this:
    val decorator = new ErrorHandlingCallback(handler) {
    def performBusinessLogic(ev: Any) {
    println("called")
    ev match {
    case "foo" =>
    case "bar" => Array[Int](1, 2, 3).apply(100)
    case "baz" => null.equals("oops!")
    case "baz2" => throw new RuntimeException("I am xyz!")
    case "baz3" => 100 / 0
    }
    }
    }
    (Please format the above code if necessary)

    ReplyDelete
  2. Thanks for pointing out the use of PartialFunction. By extending PartialFunction I can save the + method by using orElse() instead.
    However, otherwise I don't see how it can eliminate the implicit conversion or other plumbing. For example, I don't see how the code:
    val handler: PartialFunction[Exception, String] = {
    case e: IndexOutOfBoundsException => “index out of bound”
    case e: NullPointerException => “hit a null pointer”
    case e => defaultHandler.apply(e)
    }
    is simpler than:
    val decorator = new ErrorHandlingCallback(
    (classOf[IndexOutOfBoundsException], "index out of bound"),
    (classOf[NullPointerException], "hit a null pointer"),
    defaultErrorHandler)
    due to the duplicate code of "case e"; the latter expresses the intention in a clearer way to me.
    In addition, I think the proposed code shows that exactly the orElse() method can't be used to combine multiple handlers together due to the custom error handler (which on the surface accepts any exception). This seems to defeat the very purpose of PartialFunction.

    ReplyDelete
  3. You can use the orElse() method to combine multiple handlers (partial functions).
    Say you define your defaultHandler like this:
    val defaultHandler: PartialFunction[Exception, String] = {
    case e: IOException => "I/O error"
    case _ => "Unknown catch all error"
    }
    And you define the custom handler like this (note the use of the pattern guard applied to the xyz case):
    val handler: PartialFunction[Exception, String] = {
    case e: IndexOutOfBoundsException => "index out of bound"
    case e: NullPointerException => "hit a null pointer"
    case e: Exception if e.getMessage.contains("xyz") => e.getMessage
    }
    You can then combine the above two handlers like this:
    val compoundHandler = handler orElse defaultHandler
    Now you can pass the compound handler to the callback.

    ReplyDelete
  4. Thanks for the suggestion. It works.

    ReplyDelete