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.
valvs.defin concrete factories/components for singletons.- Naming:
XFactoryvs.XComponentvs.XModule. - cake as first-class modules; deficiencies of "package" (no declared dependencies); maven as module w/o API declaration