Skip to content

Right-sizing your domain model

by Frank Sauer on August 30th, 2014

When we implement domain models using Scala case classes it is tempting to stop after writing a bunch of one-liner case class definitions like these:


case class Source(kind : String, keys: (String,String)*)(tags: (String,String)*)

case class Metric(entity: String, columns: (String,Number)*)

case class Measurement(source: Source, time:Long, metrics: List[Metric]) 

We are glad to have freed ourselves from Java’s boilerplate, and case classes are so compact! Awesome! Is it?

If our entire domain model consists of simple case class definitions like this we get what Martin Fowler calls an Anemic Domain Model, an anti-pattern often found in service-oriented environments, where somehow the design rules came to include that domain objects shall have no behavior – all behavior shall be implemented in services, using the domain model objects as simple bags of data.

Of course, I am not advocating that your entire application should be written as methods in your domain objects, but as shown in my previous post, there is some behavior that makes sense to implement in your domain objects. One of the examples shown there was:


case class Source(kind : String, keys: (String,String)*)(tags: (String,String)*) {
  
   /**
    * Create a new source by appending a new key->value pair at the end
    */
   def + (tuple: Tuple2[String,String]):Source = Source(kind, (keys.toArray ++ Array(tuple)):_*)(tags:_*)
 
}

This is a good example of the kind of behavior that belongs in a domain object. It is completely application independent and simply transforms one domain object into another one (of the same kind in this case, we’ll see different ones in a moment).

Another good example of behavior that belongs directly in the domain model is factory methods to create instances, in this case most likely alternative apply() methods on companion objects to the case classes. An example was shown in the previous post where an apply method with a simplified signature was defined to create instances of Source by parsing a String parameter.

A simple rule of thumb I use to decide what behaviors to add to the domain classes themselves is:

  • Transformations to and from other domain objects or standard Scala types like collections, String, etc.
  • Factory methods

All other behaviors like creating HTML representations or other application specific behaviors do not belong in the domain model, but we’ll see a nifty Scala feature that will allow us to write code in a way that makes it look like it actually does live there…

Sometimes it’s a judgement call and sometimes we get it wrong and that’s OK. Take for example the seriesName method I added to Source in the previous post:

case class Source(kind : String, keys: (String,String)*)(tags: (String,String)*) {
   def asList = List(kind) ++ keys.unzip._2
   def seriesName(sep: String) = asList.mkString(sep)
}

This one is debatable as it could be argued that this is application (or technology) specific behavior (dictated by graphite). If, however, we decide that a dot-separated path for metric names is intrinsic to our domain, then it belongs here.

This Source class is part of a monitoring application and represents the source of a Measurement. A Measurement is a collection of Metrics gathered from the same Source at the same time. Here are some more domain classes for this application (simplified):


case class AlertRule(name:String, matcher: Regex, trigger: (Number,Number)=>Boolean, threshold: Number, msg: String)

case class Alert(time:Long, name:String, source: Source, value:Number, msg:String)

Here is an example of a behavior that – in my opinion – belongs on the domain object, even though it appears to be very application specific, it does transform one kind of domain object (Measurement) into another (Alert) given a third (AlertRule):


case class AlertRule(name:String, matcher: Regex, trigger: (Number,Number)=>Boolean, threshold: Number, msg: String) {
   
   def matches(path: String) = matcher.findFirstIn(path).isDefined
   
   def alerts(m: Measurement): List[Alert] = for {
      metric <- m.metrics
      (key,value) <- metric.columns
      path = s"${m.source.seriesName(".")}.${metric.name}.$key"
      if matches(path) && trigger(value,threshold)
   } yield Alert(time, name, m.source, value, msg)
 
}

An AlertRule creates a list of alerts based on the metrics in a measurement for any metric that matches the regular expression in the rule and for which the trigger function returns true based on the metric's value and the rule's threshold. Given the domain, I don't see how this could ever work differently when (re-)used in a different context, hence my decision to write this code in the domain class itself.

So where do we write the code that does not belong here? As an example, lets see how we could add a toHTML method to the alert class without actually writing it there. The trick is to use an implicit class:

implicit class AlertHTMLNotifications(a:Alert) {
   def toHTML = views.html.alertview(a.name, a.source.seriesName("."), a.value, a.msg)
   def subject = s"${a.name} from ${a.source.seriesName(".")}" 
}

This implicit class is defined in the context of the alerting component of the application. By using an implicit class with a single non-implicit constructor argument the Scala compiler will use it when looking up implicit conversions. This allows us to invoke the toHTML method as-if it was defined on the Alert class itself. (Apple coders: think of this as categories in Objective-C):

class AlertNotifier extends Actor with ActorLogging with Mailer {
   def receive = {
      case a: Alert => mail(a.subject, a.toHTML) 
   }
}

As long as the AlertHTMLNotifications class is in scope for the AlertNotifier actor, it can invoke subject and toHTML on alerts directly. Using implicit classes, each application component or layer can augment the domain objects with as much behavior as they see fit, while at the same time keeping the domain classes themselves pure and context-free.

Note that this application is a Play! Framework application and the toHTML makes use of a Play! template to render the HTML - yet another reason to not have this code in the Alert class itself.

I'm open for feedback and love to find out if I'm totally off-base here. Leave a comment with your feedback.

In the next post I will describe how the AlertRule objects are created by parsing an alert rule DSL with Scala's awesome parser combinators... Stay tuned!

From → programming, scala

One Comment

Trackbacks & Pingbacks

  1. Parser combinators FTW | Frankly Sauer

Comments are closed.