Utility Belt pattern

Let’s consider the problem of organizing the human-readable messages in an application. We have a number of desirable features:

  • Extensibility of existing message bundles
  • Flexibility and choice of messages
  • Separation of message code from logic
  • Easy internationalization and localization
  • Ensure we haven’t forgotten to write all the necessary messages
  • Reuse code where possible

We have a number of message bundles (think of these as capabilities), possibly written by different people, or in different projects.

Define an abstract trait for each capability:

trait Counting {
  def cardinalNumber(n : Int) : String
  def ordinalNumber(n : Int) : String
}
 
trait Dates {
  def monthName(n : Month.Value) : String  // Month is an Enumeration
  def longDate(n : Date)
  def shortDate(n : Date)
}
 
trait MyProgramAccountMessages {
  def accountCreated : String
  def accountDeleted : String
}
 
trait MyProgramUserMessages {
  def userCreated(name : String) : String
  def userDeleted(name : String) : String
}
 
// etc

Now, throughout your application, whenever you need to produce some human-readable text, add an implicit parameter to your method, for example:

def createNewUser(name: String, age : Int,
    sex : Boolean)(messages : MyProgramUserMessages) {
  Db.exec("INSERT INTO ...")
  Screen.inform(messages.userCreated(name))
}
 
def deleteAccount(account : Account)(implicit
    messages : MyProgramAccountMessages with Dates) {
  Db.exec("DELETE ...")
  Screen.inform(messages.accountDeleted)
}

So we’ve got no message code mixed up with the logic, and the nice thing is that each of the methods above only asks for (by means of the implicit parameter) the capabilities it needs.

So finally, to make this work, we have to define our message bundles, and we’ve got a lot of options:

class English
  extends Counting
  with Dates
  with MyProgramAccountMessages
  with MyProgramUserMessages
{
  def cardinalNumber(n : Int) = n match {
    case 1 => "one"
    case 2 => "two"
    case 3 => "three"
    case _ => "lots"
  }
  def accountCreated = "A new account has been added to the database."
  def userDeleted(n : String) = "User '"+n+"' has been deleted."
  // and the rest...
}

Or maybe someone has already done some of the work for you:

class French
  extends scalax.locale.Locale.FR
  with MyProgramAccountMessages
  with MyProgramUserMessages
{
  def accountCreated = "Un nouveau dossier a été créé."
  def accountCreated = "Un nouvel utilisateur a été créé."
  // etc...
}

Or maybe we only want to make a small localization tweak:

class SwissFrench extends French {
  override def cardinalNumber(n : Int) = n match {
    case 70 => "septante"
    case 80 => "huitante"
    case 90 => "nonante"
    // etc...
    case x => super.cardinalNumber(x)
  }
}

Finally, we just need to make sure that an instance of one of our message bundles is available to the message use sites. This may or may not be fixed at compile time, but it’s up to the requirements how it’s done.

If we’re doing Inversion of Control with a generous helping of Cake, we might put it in a trait like this:

trait Configuration {
  implicit val messages = new English
}

So, to summarize, to use this pattern you need to:

  1. Declare the capabilities you want from your Swiss Army Knife as traits
  2. Add an implicit parameter to each method declaring the capabilities it needs, and start using the capabilities
  3. Declare one or more Swiss Army Knife objects/classes grouping the capabilities
  4. Make sure an implicit instance of your Swiss Army Knife is available in your application.

And the benefits are:

  • Separation of crosscutting capabilities
  • The full configuration flexibility of mixins
  • Bundling of different capabilities together, without the overhead of multiple objects.
  • Fully typesafe

I had originally wanted to call this the “Swiss Army Knife” pattern, but that name has already been attributed to an antipattern.

 
patterns/utility-belt.txt · Last modified: 2010/02/11 09:10
 
Recent changes RSS feed Valid XHTML 1.0 Driven by DokuWiki