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