Saturday, August 25, 2012

scala companion package

I wrote a scala trait the other day with a "companion package" instead of the usual "companion object". I was pretty happy with how the code came together, so thought I'd write a quick post to my poor neglected bLog.

I often write a scala trait that defines an interface to some tool that manages access to a data repository or manipulates data in some way. I usually define the interface traits to different tools provided by some module "bla" in a "bla.controller" sub-package. I define bla's data model in a "model" sub-package, and classes that simply support a tool's interface in the tool's companion object, so something like this:

package bla
package controller

trait Tool {
    def get( id:String ):model.Data
    def search( query:String ):Tool.SearchResult
    ...
}

object Tool {
    case class SearchResult( query:String, matches:Iterable[model.IndexEntry] ) {}

    object SomeEnum extends Enumeration {
       val A, B, C = Value
    }

    trait Builder {
       var creds:Credentials = null
       def creds( value:Credentials ):this.type = {creds = value; this }
       
       var endPoint:URL = new URL( "http://default.end.point" )
       def endPoint( value:URL ):this.type = { endPoint = value; this }

       ...
       def build():Tool
    }

    /** Factory necessary if not running in IOC container - ugh */
    object Factory extends Supplier[Builder] {
        def get():Builder = internal.SimpleTool.Factory.get()
    }
}

Anyway - that pattern works great most of the time, but the other day I was working on a tool with several supporting types in the companion object, and the Tool.scala file was growing fatter than I like. Fortunately - it turns out we can accomplish the same semantics as a companion object with a companion package like this:

package bla
package controller

trait Tool { ... }

package Tool {
    // ...
    object Factory extends Supplier[Builder] { ... }
}

The companion package approach has the advantage that we can split out some of the types defined in the companion package to their own files, so we could have a bla/DataHelper/Factory.scala like this:

package bla
package controller
package Tool

object Factory extends Supplier[Tool] { ... }

The companion package can define "static" constants in a package object, and we can move the tool-specific implementation classes to its own bla.controller.Tool.internal package rather than the shared bla.controller.internal package.

Finally - changing companion object code to the companion package pattern does not require changes to client code (code that uses Tool) - just a recompile.

Anyway - I like the way things fit together with the companion package pattern.

1 comment:

Unknown said...

Ugh - I thought this was working great, but I've since run into a weird problem where scalac doesn't like the "companion package" approach without a special "resolve-conflicts" flag - which I can't stand:
http://lilcactus.tumblr.com/post/21393391520/resolve-term-conflict
,
http://stackoverflow.com/questions/8984730/package-contains-object-and-package-with-same-name

Anyway - we can make the companion-package pattern work by renaming our trait to "ITool" (C# style) or whatever, so something like:

trait ITool { ... }
package Tool { ... }
...
val tool:ITool = Tool.Factory.get()

Frick - it was almost so nice! I should have known ... ;-)