Let’s consider the problem of organizing the human-readable messages in an application. We have a number of desirable features:
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:
And the benefits are:
I had originally wanted to call this the “Swiss Army Knife” pattern, but that name has already been attributed to an antipattern.