Y como dije en el post anterior vamos a ver el cake pattern.
En algún momento, crear todo el grafo de objetos de "todo el mundo" será poco práctico y el código será grande y difícil de leer. Entonces deberíamos dividirlo de alguna manera en pedazos más pequeños. Afortunadamente, los trait de Scala encajan perfectamente para esa tarea; se pueden usar para dividir el código de creación del grafo de objetos.
En cada trait, que para el propósito de esta tarea también se llama "módulo", se crea parte del grafo de objetos. Todo se vuelve a combinar más tarde al unir todos los traits necesarios.
Puede haber varias reglas sobre cómo dividir el código en módulos. Un buen lugar para comenzar es considerar crear un módulo precableado por paquete. Cada paquete debe contener un grupo de clases que compartan o implementen alguna funcionalidad específica. Lo más probable es que estas clases cooperen de alguna manera y, por lo tanto, pueden conectarse.
El beneficio adicional de enviar un paquete no solo con el código, sino también con un fragmento de grafo de objeto conectado, es que es más claro cómo se debe usar el código. No hay requisitos para usar el módulo.
Sin embargo, tales módulos generalmente no pueden existir de manera independiente: muy a menudo dependerán de algunas clases de otros módulos. Hay dos formas de expresar dependencias.
Expresar dependencias a través de miembros abstractos
Como cada módulo es un trait, es posible dejar algunas dependencias sin definir, como miembros abstractos. Dichos miembros abstractos se pueden usar al realizar la conección (ya sea manualmente o mediante la macro de MacWire), pero no es necesario dar la implementación específica.
Cuando todos los módulos se combinan en la aplicación final, el compilador verificará que todas las dependencias definidas como miembros abstractos estén definidas.
Tenga en cuenta que podemos declarar a todos los miembros abstractos como defs, ya que pueden implementarse más tarde como vals, vals lazy o dejarse como defs. Usar un def mantiene todas las opciones posibles.
La conexión para nuestro código de ejemplo lo vamos a dividir de la siguiente manera; las clases ahora se agrupan en paquetes:
package shunting {
class PointSwitcher()
class TrainCarCoupler()
class TrainShunter(
pointSwitcher: PointSwitcher,
trainCarCoupler: TrainCarCoupler)
}
package loading {
class CraneController()
class TrainLoader(
craneController: CraneController,
pointSwitcher: PointSwitcher)
}
package station {
class TrainDispatch()
class TrainStation(
trainShunter: TrainShunter,
trainLoader: TrainLoader,
trainDispatch: TrainDispatch) {
def prepareAndDispatchNextTrain() { ... }
}
}
Cada paquete tiene un módulo de trait correspondiente. Tenga en cuenta que la dependencia entre los paquetes de derivación y carga se expresa utilizando un miembro abstracto:
package shunting {
trait ShuntingModule {
lazy val pointSwitcher = wire[PointSwitcher]
lazy val trainCarCoupler = wire[TrainCarCoupler]
lazy val trainShunter = wire[TrainShunter]
}
}
package loading {
trait LoadingModule {
lazy val craneController = wire[CraneController]
lazy val trainLoader = wire[TrainLoader]
// dependency of the module
def pointSwitcher: PointSwitcher
}
}
package station {
trait StationModule {
lazy val trainDispatch = wire[TrainDispatch]
lazy val trainStation = wire[TrainStation]
// dependencies of the module
def trainShunter: TrainShunter
def trainLoader: TrainLoader
}
}
object TrainStation extends App {
val modules = new ShuntingModule
with LoadingModule
with StationModule
modules.trainStation.prepareAndDispatchNextTrain()
}
Para implementar dependencias de esta manera, se necesita una convención de nomenclatura coherente, ya que el miembro abstracto se concilia con el nombre de implementación. Nombrar los valores igual que las clases, pero con la letra inicial en minúscula es un buen ejemplo de tal convención.
Expresar dependencias a través de autotipos
Otra forma de expresar dependencias es mediante auto-tipos o extendiendo otros módulos de traits. De esta manera, se crea una conexión mucho más fuerte entre los dos módulos, en lugar del enfoque de miembro abstracto acoplado más flexible, sin embargo, en algunas situaciones es deseable (por ejemplo, cuando se tiene una interfaz de módulo con implementaciones múltiples).
Por ejemplo, podríamos expresar la dependencia entre los módulos de derivación y carga y el módulo de estación extendiendo el módulo de rasgos, en lugar de usar los miembros abstractos:
package shunting {
trait ShuntingModule {
lazy val pointSwitcher = wire[PointSwitcher]
lazy val trainCarCoupler = wire[TrainCarCoupler]
lazy val trainShunter = wire[TrainShunter]
}
}
package loading {
trait LoadingModule {
lazy val craneController = wire[CraneController]
lazy val trainLoader = wire[TrainLoader]
// dependency expressed using an abstract member
def pointSwitcher: PointSwitcher
}
}
package station {
// dependencies expressed using extends
trait StationModule extends ShuntingModule with LoadingModule {
lazy val trainDispatch = wire[TrainDispatch]
lazy val trainStation = wire[TrainStation]
}
}
object TrainStation extends App {
val modules = new ShuntingModule
with LoadingModule
with StationModule
modules.trainStation.prepareAndDispatchNextTrain()
}
Se lograría un efecto muy similar utilizando un auto-tipo.
Este enfoque también puede ser útil para crear módulos más grandes a partir de múltiples más pequeños, sin la necesidad de volver a expresar las dependencias de los módulos más pequeños. Simplemente defina un trait de módulo más grande que extienda un número de trait de módulo más pequeño.
Composing modules
Los módulos también se pueden combinar usando la composición, es decir, puede anidar módulos como miembros y usar dependencias definidas en los módulos anidados para conectar objetos.
Por ejemplo, podemos agregar un complemento a nuestra aplicación de gestión de trenes que permitirá recopilar estadísticas:
package stats {
class LoadingStats(trainLoader: TrainLoader)
class ShuntingStats(trainShunter: TrainShunter)
class StatsModule(
shuntingModule: ShuntingModule,
loadingModule: LoadingModule) {
import shuntingModule._
import loadingModule._
lazy val loadingStats = wire[LoadingStats]
lazy val shuntingStats = wire[ShuntingStats]
}
}
Tenga en cuenta las declaraciones de importación, que traen cualquier dependencia definida en los módulos anidados al alcance.
Esto se puede acortar aún más mediante el uso de una anotación experimental @Module para los rasgos / clases del módulo; Los miembros de módulos anidados con esa anotación se tendrán en cuenta automáticamente durante la inyección:
package loading {
@Module
trait LoadingModule { ... }
}
package shunting {
@Module
trait ShuntingModule { ... }
}
package stats {
class LoadingStats(trainLoader: TrainLoader)
class ShuntingStats(trainShunter: TrainShunter)
class StatsModule(
shuntingModule: ShuntingModule,
loadingModule: LoadingModule) {
lazy val loadingStats = wire[LoadingStats]
lazy val shuntingStats = wire[ShuntingStats]
}
}
En este escenario, no se necesitan importaciones.