Translate

martes, 27 de agosto de 2019

Inyección de dependencia de tiempo de compilación

Spring proporciona un mecanismo para la inyección de dependencias en tiempo de ejecución, es decir, la inyección de dependencias donde las dependencias no están conectadas hasta el tiempo de ejecución. Este enfoque tiene ventajas y desventajas, las principales ventajas son la minimización del código repetitivo, la principal desventaja es que la construcción de la aplicación no se valida en tiempo de compilación.

Un enfoque alternativo que es popular en el desarrollo de Scala es utilizar la inyección de dependencia de tiempo de compilación. Esta técnica se puede lograr mediante la construcción manual y el cableado de dependencias. Existen otras técnicas y herramientas más avanzadas, como herramientas de cableado automático basadas en macros, técnicas de cableado automático implícito y varias formas del cake pattern.

Inyectar dependencias en tiempo de compilación permite aprovechar el compilador para verificar que cada controlador en su aplicación tenga acceso a todos los componentes que necesita. Eso significa que no necesita preocuparse por los errores de tiempo de ejecución que causan bloqueos y una mala experiencia para sus usuarios. De hecho, la DI en tiempo de compilación (y la tipificación estática en general) pueden reducir la necesidad de un subconjunto de tipos comunes de pruebas unitarias.

El uso de parámetros de constructor es un enfoque simple y directo para definir dependencias en tiempo de compilación. Veamos un ejemplo del framework play:

class Controller( val controllerComponents: ControllerComponents,
  userModel: UserModel) extends BaseController {

  def user() = Action {
   request => Ok(Json.toJson(userModel.getUsernames()))
  }
}

Se puede decir que la siguiente clase "depende de" una instancia de ControllerComponents y una instancia de UserModel. Esto es tan simple como especificar dependencias. No hay magia, solo dices qué componente quieres y lo obtienes. Configurar cómo se proporcionan las dependencias requiere un poco más de trabajo. Aquí es donde entra el cargador de aplicaciones de play.

class Loader extends ApplicationLoader {
  def load(context: Context): Application = {
    LoggerConfigurator(context.environment.classLoader).foreach {
      _.configure(context.environment)
    }
    new Components(context).application
  }
}

class Components(context: Context) extends BuiltInComponentsFromContext(context) {
  override lazy val httpFilters = Nil
  Lazy val userModel: UserModel = new UserModel()
  lazy val controller: Controller = new Controller(controllerComponents, userModel)
  lazy val router: Router = new Routes(httpErrorHandler, controller)
}

La clase Loader se requiere principalmente para garantizar que la aplicación esté configurada correctamente cuando se carga. La inyección de dependencias ocurre en la clase Componentes. Una vez más, no hay magia aquí: simplemente crea y pasa instancias a componentes que las necesitan. BuiltInComponentsFromContext proporciona un puñado de componentes de Play predeterminados que son útiles. En este caso, uso el componente ControllerComponents para nuestro controlador y un HttpErrorHandler para el constructor de Rutas.

Para aplicaciones simples, la inyección manual de dependencias es bastante simple. Sin embargo, incluso en aplicaciones simples, agregar una dependencia a un controlador requiere que modifique tanto el controlador como el cargador de aplicaciones. Este proceso se ve exacerbado por el constructor de Rutas que genera Play. Cada clase a la que haga referencia en conf / routes se traducirá en un parámetro constructor del objeto Routes. Eso significa que agregar una clase de controlador requiere que crees una instancia de la clase y la pases explícitamente al constructor de Rutas. Para empeorar las cosas, si reordena su archivo conf / routes o agrega una nueva ruta en algún lugar en el medio, su objeto Routes generado tendrá un nuevo orden para sus parámetros de constructor, y tendrá que arreglarlo manualmente también.

La inyección manual en tiempo de compilación no es escalable y creará mucho trabajo adicional en aplicaciones más complejas. Aquí es donde entra MacWire.

MacWire es una macro muy ligera que genera automáticamente llamadas de constructores. Eso es casi todo lo que hace (está bien, tiene algunas otras características, pero solo nos importa esta por ahora). Usando MacWire, cambié la clase de Componentes anterior a la siguiente:

class Components(context: Context) extends BuiltInComponentsFromContext(context) {
  override lazy val httpFilters = Nil
  lazy val userModel: UserModel = wire[UserModel]
  lazy val controller: Controller = wire[Controller]
  lazy val router: Router = {
    val prefix = "/"
    wire[Routes]
  }
}

Notarás dos cambios importantes.

Primero, todas las llamadas "nuevas" han sido reemplazadas por wire [ClassName]. Esta es la macro que genera las llamadas "nuevas". Para ello, examina el constructor predeterminado para la clase especificada y luego busca valores del mismo tipo en el ámbito actual. El wire [Controlador] se expandirá a un nuevo Controlador (controllerComponents, userModel). Ese es exactamente el mismo código que escribimos manualmente arriba. Wire sigue las reglas normales de alcance con las que ya está familiarizado en Scala. Si hay varias instancias que cumplirán una dependencia, el cable fallará en tiempo de compilación diciendo que no puede decidir qué instancia usar. Tendrá que resolver esta ambigüedad manualmente. MacWire proporciona calificadores para simplificar este proceso cuando sea necesario.

En segundo lugar, la definición de enrutador cambió. El constructor predeterminado de Rutas requiere un argumento de prefijo de cadena, mientras que el constructor utilizado manualmente en nuestro ejemplo original anterior no es el constructor predeterminado y no requiere este argumento. Envolví el prefijo en el alcance del bloque de la llamada por cable para no filtrar accidentalmente el prefijo en otros componentes que podrían necesitar un parámetro de cadena. En proyectos más grandes, esto se vuelve más valioso. En este ejemplo, todo habría funcionado bien si definiera el prefijo fuera del bloque Router.

Ahora, agregar una dependencia (o eliminar una dependencia) de un controlador solo requiere que modifique el controlador en sí. Entonces, si necesita acceso a la configuración de la aplicación, puede agregar un parámetro de constructor con el tipo Configuración al controlador y, como por arte de magia, inyectará un objeto de Configuración. Aún mejor, agregar, eliminar o reordenar rutas en conf / routes se vuelve mucho más fácil porque ya no tiene que preocuparse por el orden de los parámetros de Rutas o cualquier cosa más allá de las clases específicas para las que necesita proporcionar instancias.

Una última nota sobre MacWire: se debe usar lazy val. Esto permite especificar dependencias complicadas sin preocuparse por el orden de inicialización entre los diferentes componentes. Todos se crearán a pedido y evitará posibles excepciones de puntero nulo. También puede usar defs si prefiere que cada clase que depende de su def obtenga una nueva instancia en lugar de una instancia compartida.

Y me quedo relargo el post en otro vamos a ver el cake pattern.