Adam Warski

21 Jun 2010

Object Services in Scala

java
scala

Using Scala’s implicits it’s possible to implement Object Services in a much more “user-friendly” way. Just to remind, the goal is to extend a class hierarchy with a method, polymorphically. E.g. we have:

trait Animal
class Elephant extends Animal
class Ant extends Animal

and we want to add a paint method, which has a different implementation for an elephant, and for an ant (quite obviously :) ). In another words, we want to have a polymorphic extension method. One approach in Scala is to use pattern-matching. And in fact pattern-matching can be used to implement object services as described here (below is a method which uses reflection, but it can be easily swapped).

Before we’ll go to the actual implementation, here’s how you can use the object services. We already have the objects, so it’s time to create the services:

trait PaintService[O <: Animal] extends OS[O] {
   def paint(c: Canvas): Unit
}

class ElephantPaintService(elephant: Elephant) extends PaintService[Elephant] {
   def paint(c: Canvas): Unit { ... }
}

class AntPaintService(ant: Ant) extends PaintService[Ant] {
   def paint(c: Canvas): Unit { ... }
}

Now we need a place, where we register the services. In the Weld implementation, this was done automatically at container startup time. Here we’ll need one object (which could be auto-generated by a compiler plugin):

object PaintServiceReg extends OSP[Animal, PaintService] {
   register[Elephant, ElephantPaintService]
   register[Ant, AntPaintService]
}

The type bound’s of the register method will make sure that you can register only appropriate services for appropriate object types. Finally, we can use our services. If we want to use the PaintService, we’ll need to import the content of the PaintServiceReg object. That way we have control, on what services are available when. Usage is quite simple and looks like a regular method invocation, as if the method was in the class hierarchy:

import PaintServiceReg._

val animal1: Animal = new Elephant
animal1.paint(c)  // ElephantPaintService is called

val animal2: Animal = new Ant
animal2.paint(c)  // AntPaintService is called

What’s left to show is the actual code for OS and OSP:

trait OS[O]

trait OSP[O <: AnyRef, S[_ <: O] <: OS[_]] {
   implicit def oToS(obj: O): S[O] = {
      val bestService = findBestService(obj.getClass())
      bestService.getConstructors()(0).newInstance(obj).asInstanceOf[S[O]]
   }

   protected def register[RO <: O, RS <: S[RO]](implicit manifestRO: Manifest[RO], 
            manifestRS: Manifest[RS]) {
      serviceMap += manifestRO.erasure -> manifestRS.erasure
   }

   private var serviceMap: Map[Class[_], Class[_]] = Map()

   private def findBestService(objectCls: Class[_]): Class[_] = {
      serviceMap.foldLeft[(Class[_], Class[_])]((classOf[AnyRef], classOf[AnyRef]))
         ((curr, mapping) => {
         // Checking if the mapping is appropriate for the given object class 
         // and more specific than the current one
         if (mapping._1.isAssignableFrom(objectCls) &&
                curr._1.isAssignableFrom(mapping._1))
            mapping
         else curr
      })._2
   }
}

The implementation can be improved e.g. to always return the same object service reference for a given object (something like Eclipse Adapters - thanks for the link), or to provide alternative ways to instantiate the services - not necessarily with a one-arg constructor.

The biggest problem here is that completeness is not checked - that is, if there’s a service missing for a class in the hierarchy, there will be a run-time error. But I think this also can be checked with a compiler plugin.

comments powered by Disqus

Any questions?

Can’t find the answer you’re looking for?