Hace mucho tiempo en la tierra de Scala surgió una nueva forma segura de inyección de dependencia. A la larga, trae más problemas de los que vale la pena.
Patrón de pastel
Recordemos qué es un patrón de pastel. Para entenderlo necesitamos conocer a los tipos propios. Estaban destinados a ser utilizados como mezclas y se ven más o menos así:
trait UserRepository { def fetchUsers(userIds: Seq): Seq}class UserRepositoryDbImpl(database: Database) extends UserRepository { def fetchUsers(userIds: Seq): Seq = ...}trait UserRepositoryLogging { self: UserRepository => private lazy val logger: Logger = ... override def fetchUsers(userIds: Seq): Seq = { val users = super.fetchUsers(usersIds) logger.debug(s"""Fetched users by ids: ${users.mkString(", ")}""") users }}val userRepository: UserRepository = new UserRepositoryDbImpl(database) with UserRepositoryLogging
Aquí se puede añadir el rasgo UserRepositoryLogging
lo UserRepository
aplicación – que no compile lo contrario. Además, dentro de UserRepositoryLogging
estamos suponiendo que es la aplicación de UserRepository
. Por lo tanto, todo lo accesible dentro de UserRepository
también es accesible allí.
Ahora, ya que podemos acceder a cualquier tipo declarado utilizado para el tipo propio, se nos permite hacer esto:
trait UserRepository { def fetchUsers(userIds: Seq): Seq}trait UserRepositoryComponent { def userRepository: UserRepository}trait UserRepositoryComponentImpl extends UserRepositoryComponent { lazy val userRepository: UserRepository = new UserRepository { // ... }}trait SomeController { self: UserRepositoryComponent => def handleRequest(userIds: Seq): Response = { // ... userRepository.fetchUsers(userIds) // ... }}object SomeController extends SomeController with UserRepositoryComponentImpl
Dentro de SomeController
declaramos auto-tipo, que asegura, que su instantiable aplicación tendrá userRepository
método. Así que al agregar el mixin proporcionamos la implementación y, por lo que nos aseguramos de que la dependencia se inyecte en el tiempo de compilación. Sin reflexión de tiempo de ejecución, con seguridad de tipo, sin configuraciones ni bibliotecas adicionales.
Cada uno de estos componentes podría ser una capa de nuestra aplicación (capa de lógica de negocio, capa de infraestructura, etc.), que alguien comparó con las capas del pastel. Por lo tanto, al crear su aplicación de tal manera que esté horneando un pastel de manera efectiva. Por lo tanto, patrón de pastel.
Ahora why ¿por qué es un problema?
Agregar dependencias
En proyectos más grandes, el número de servicios, repositorios y utilidades seguramente será mayor de 1-2. Por lo tanto, su pastel comenzará a verse como:
object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl
De hecho, una vez vi un pastel dondeComponentImpl
pasó por 2-3 pantallas. No estabas inyectando allí solo los componentes necesarios a la vez, estabas combinando todo a la vez: infraestructura, servicios de dominio, persistencia, lógica de negocio
Ahora, imagine lo que sucede cuando necesita agregar una dependencia. Comienza agregando otro with Dependency
a su propio tipo. A continuación, comprueba dónde se utiliza el rasgo actualizado. Posiblemente también tenga que agregar el tipo propio aquí. Subes al árbol de los mixins hasta llegar a una clase o a un objeto. Uff.
Excepto que, esta vez lo agregaste tú mismo, para que sepas qué es lo que necesitas agregar. Pero cuando estás haciendo rebase o resolviendo un conflicto, es posible que termines en una situación en la que aparezca una nueva dependencia que no tienes. El error del compilador solo dice que self-type X does not conform to Y
. Con más de 50 componentes, también puedes adivinar cuál falla. (O comience a hacer una búsqueda binaria eliminando componentes hasta que el error desaparezca).
Cuando el tamaño de la torta y la cantidad crecen más, es posible que desee implementar la regla SECA y dividirla en pasteles más pequeños, que se juntarán más adelante. Esto descubre nuevas profundidades del infierno.
Eliminar dependencias
Con DI normal cuando deja de usar algún objeto, su IDE / compilador puede decirle que ya no es necesario, por lo que puede eliminarlo. Con el patrón de pastel, no se le informará sobre tales cosas.
Como resultado, la limpieza es mucho más difícil y es muy posible que en algún momento termines con muchas más dependencias de las que realmente necesitas.
Tiempo de compilación
Todo esto se suma al tiempo de compilación. Puede que no sea obvio a primera vista, pero puede terminar con:
trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }
que es un ciclo de dependencia. (Todos sabemos que son malas, pero es posible que ni siquiera te des cuenta de que acabas de crear una. Especialmente si inicializas todo perezosamente).
Por lo tanto, esos 2 rasgos se compilarán muy a menudo juntos, y el compilador incremental de Zinc no ayudará con eso. Además, los cambios en cualquiera de los componentes de un pastel activarán la recompilación de cosas por encima de él en el gráfico de dependencias.
Probar
Las dependencias cíclicas crean otro problema. Es muy posible que termines con algo como:
trait AComponentImpl { self: BComponent => lazy val a: A = new A { b.someMethod() // use b }}trait BComponentImpl { self: AComponent => lazy val b: B = new B { a.someMethod() // use a }}object Cake extends AComponentImpl with BComponentImplCake.a // NPE!Cake.b // NPE!
Cada componente será en su propio archivo, por lo que no cuenta esta dependencia. Pruebas con simulacros de no ayudar con eso:
trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!
así que todo estará bien y dandy hasta que realmente ejecute el código. Ya sabes, el código es seguro para el tipo, lo que significa que el valor que construirás seguirá las restricciones de tipo. Pero estas restricciones no significan nada, ya que Nothing
(excepción lanzada) es un subtipo perfectamente válido del tipo requerido.
Orden de inicialización
El problema anterior está relacionado con este: ¿cuál es el orden de inicialización del componente?
Con DI normal es obvio: antes de poner una cosa en la otra, necesita crearla. Así que basta con mirar el orden en el que se crean los objetos.
Con un pastel puedes olvidarte de él. Los valores dentro de los componentes a menudo serán lazy val
s o object
s. Por lo tanto, el primer atributo al que se accede intentará crear instancias de sus dependencias, que intentará crear instancias de sus dependencias, etc.
Incluso si vamos con val
es una cuestión de orden en la que compusimos pastel, ya sabes, linealización de rasgos. Siempre y cuando cada componente se use exactamente una vez, no hay problema. Pero si se secó el pastel, y algunos componentes se repiten, cuando los fusiona
En un mundo perfecto, cosas como el orden de inicialización no deberían importar, pero a menudo lo hace, y nos gustaría saber, por ejemplo, cuándo se inició alguna conexión a la base de datos o suscripción al tema Kafka, y registrar cosas para depurar posibles fallas.
Repetitivo y de bloqueo
Como probablemente haya notado, este patrón genera una gran cantidad de repetitivo-básicamente, cada repositorio o servicio tendría que tener un contenedor, solo para anotar las dependencias.
Una vez que vaya por ese camino, notará que es bastante difícil deshacerse del patrón de pastel. Cada implementación se coloca dentro de un rasgo, y sus dependencias provienen del ámbito, no a través de parámetros. Por lo tanto, para cada clase, primero tendría que realizar el refactor para poder instanciarlo sin el envoltorio.
Pero entonces las dependencias también tendrían que ser refactorizadas y así sucesivamente. El patrón de pastel es un compromiso feo, que te duele y se opone cuando intentas retirarte de él gradualmente.
Literalmente, cualquier otra forma de DI en Scala, que yo conozca (reflexión en tiempo de ejecución a’la Guice, anotaciones macro a’la MacWire, implicitos, combinaciones de esos 3) le permite decir ¡alto! en cualquier momento, y cambie gradualmente a otra solución.
Resumen
El patrón de pastel es una solución a un problema de DI, que a la larga hace más daño que bien. Por lo que escuché, se usó en el compilador de Scala, y lo lamentan. Era el mecanismo de DI predeterminado para el Framework de juego, pero debido a todos estos problemas, los autores decidieron cambiar a Guice – sí, los programadores funcionales preferían la reflexión en tiempo de ejecución a la cake segura de tipos, por lo que dice mucho.
Entonces, ¿cuáles son las alternativas?
- pasar argumentos manuales antiguos, sin necesidad de explicar que
- implicita, tiene sus ventajas hasta que su proyecto es mantenido por más que usted mismo, y los compañeros de trabajo comienzan a quejarse de no saber, lo que está sucediendo, compilación lenta, no comprender implicita
- reflexión en tiempo de ejecución: Guice es un marco maduro que hace DI exactamente de la manera que odio. Se usó en muchos proyectos, a los programadores de Java les encanta , al igual que a la Primavera. Algunos de ellos dejan que la abstracción se escape al pasar el inyector a la clase. Algunos de ellos quieren que su configuración sea flexible, tan flexible que el cableado de clase pueda fallar en tiempo de ejecución, bleh!
- MacWire – a la mitad de pasar argumentos manualmente y usar macros para todo-escanea el ámbito actual en tiempo de compilación e inyecta dependencias por usted. Si faltan algunos, falla la compilación. Mientras tanto, escribe solo
wire
- Airframe: marco basado en macros donde crea una receta para DI con un DSL. Tiene soporte para la gestión del ciclo de vida de los objetos
- pulp-my shameless auto-promotion. Utilizó implicitos y anotaciones de macro para generar proveedores para clases
en mi humilde opinión, cada una de ellas debería funcionar mejor que el patrón de pastel.