We're going to be linking the following traits and classes together:
trait FileSystem {
def roots: Traversable[File]
}
class PathWalker(fs: FileSystem) {
def find(globPattern: String): Traversable[File] = {
// do something with fs.roots()
}
}
Then we want to make a PathWalker
, somehow, and do something with it:
// Client code: make a PathWalker and do something with it.
val pathWalker: PathWalker = ... // TODO
pathWalker.find("*.scala")
As you can see, a PathWalker
needs a FileSystem
. Let's abstract the choice of implementation of FileSystem
into into a function, wrapping everything in a code block to represent some scope:
{
trait FileSystem {
def roots: Traversable[File]
}
def fileSystem: FileSystem // abstract
class PathWalker {
val fs = fileSystem // Calls the factory method. No need for a constructor parameter anymore.
def find(globPattern: String): Traversable[File] = {
// do something with fs.roots()
}
}
// Client code: make a PathWalker and do something with it.
val pathWalker = new PathWalker
pathWalker.find("*.scala")
}
You can't have an abstract method in a code block, so we'll make the block into a trait:
trait PathWalkerModule {
trait FileSystem {
def roots: Traversable[File]
}
def fileSystem: FileSystem // abstract
class PathWalker {
val fs = fileSystem
def find(globPattern: String): Traversable[File] = {
// do something with fs.roots()
}
}
// Client code: make a PathWalker and do something with it.
val pathWalker = new PathWalker
pathWalker.find("*.scala")
}
Now fileSystem
can remain abstract. But it's obviously silly to have the client code coupled with the class definitions, so we'll separate them:
trait PathWalkerModule {
trait FileSystem {
def roots: Traversable[File]
}
def fileSystem: FileSystem // abstract
class PathWalker {
val fs = fileSystem
def find(globPattern: String): Traversable[File] = {
// do something with fs.roots()
}
}
}
// Some other code block, somewhere...
// Client code: make a PathWalker and do something with it.
{
val module = new PathWalkerModule {
def fileSystem: FileSystem // TODO!!!
}
val pathWalker = new module.PathWalker
pathWalker.find("*.scala")
}
Now we can get a PathWalker
that uses any kind of FileSystem
,
as long as we can define the fileSystem
method in the PathWalkerModule
.
But this is kind of weird: the choice of a FileSystem
instance
has nothing to do with creating a PathWalker
, we just need an instance,
and we're only linking the two instances together. To fix this we can
refactor the code to separate the creation of a FileSystem
from
the creation of a PathWalker
:
trait FileSystemModule {
trait FileSystem {
def roots: Traversable[File]
}
def fileSystem: FileSystem // abstract
}
trait PathWalkerModule {
self: FileSystemModule => // Require FileSystemModule to be mixed-in.
class PathWalker {
val fs = fileSystem // i.e. FileSystemModule.fileSystem()
def find(globPattern: String): Traversable[File] = {
// do something with fs.roots()
}
}
}
// Some other code block, somewhere...
// Client code: make a PathWalker and do something with it.
{
val module = new PathWalkerModule with FileSystemModule {
def fileSystem: FileSystem // TODO!!!
}
val pathWalker = new module.PathWalker
pathWalker.find("*.scala")
}
Let's get rid of the TODO
and create a "non-abstract" FileSystemModule
trait:
/** Creates a "real" [FileSystem]. */
trait DefaultFileSystemModule extends FileSystemModule {
override def fileSystem: FileSystem = new DefaultFileSystem
class DefaultFileSystem extends FileSystem {
// ...
}
}
So we finally have:
trait FileSystemModule {
trait FileSystem {
def roots: Traversable[File]
}
def fileSystem: FileSystem // abstract
}
trait PathWalkerModule {
self: FileSystemModule => // Require FileSystemModule to be mixed-in.
class PathWalker {
val fs = fileSystem // i.e. FileSystemModule.fileSystem()
def find(globPattern: String): Traversable[File] = {
// do something with fs.roots()
}
}
}
/** Creates a "real" [FileSystem]. */
trait DefaultFileSystemModule extends FileSystemModule {
override def fileSystem: FileSystem = new DefaultFileSystem
class DefaultFileSystem extends FileSystem {
// ...
}
}
// Some other code block, somewhere...
// Client code: make a PathWalker and do something with it.
{
val module = new PathWalkerModule with DefaultFileSystemModule
val pathWalker = new module.PathWalker
pathWalker.find("*.scala")
}
So the general pattern looks like:
trait XModule { class X }
trait YModule { class Y }
trait ZModule {
self: XModule with YModule =>
class Z {
val x = new X
val y = new Y
// do stuff with x and y
}
}
Thus the name "cake": The ZModule
"layer" is layered with the other "layers" containing ways to get other definitions and instances.
val
vs.def
in concrete factories/components for singletons.- Naming:
XFactory
vs.XComponent
vs.XModule
. - cake as first-class modules; deficiencies of "package" (no declared dependencies); maven as module w/o API declaration