En ingeniería de software , la inyección de dependencias es una técnica de programación en la que un objeto o función recibe otros objetos o funciones que necesita, en lugar de crearlos internamente. La inyección de dependencias tiene como objetivo separar las preocupaciones de construir objetos y usarlos, lo que lleva a programas débilmente acoplados . [1] [2] [3] El patrón garantiza que un objeto o función que desea utilizar un servicio determinado no debería tener que saber cómo construir esos servicios. En cambio, el " cliente " receptor (objeto o función) recibe sus dependencias mediante un código externo (un "inyector"), del que no es consciente. [4] La inyección de dependencias hace explícitas las dependencias implícitas y ayuda a resolver los siguientes problemas: [5]
La inyección de dependencia se utiliza a menudo para mantener el código en línea con el principio de inversión de dependencia . [6] [7]
En lenguajes tipados estáticamente, el uso de inyección de dependencia significa que un cliente solo necesita declarar las interfaces de los servicios que utiliza, en lugar de sus implementaciones concretas, lo que hace más fácil cambiar los servicios que se usan en tiempo de ejecución sin tener que volver a compilar.
Los marcos de aplicación suelen combinar la inyección de dependencias con la inversión de control . En la inversión de control, el marco primero construye un objeto (como un controlador) y luego le pasa el flujo de control . Con la inyección de dependencias, el marco también instancia las dependencias declaradas por el objeto de aplicación (a menudo en los parámetros del método constructor) y pasa las dependencias al objeto. [8]
La inyección de dependencias implementa la idea de "invertir el control sobre las implementaciones de dependencias", por lo que ciertos marcos de Java nombran genéricamente el concepto "inversión de control" (que no debe confundirse con inversión del flujo de control ). [9]
Inyección de dependencia para niños de cinco años
Cuando vas a buscar cosas del refrigerador para ti, puedes causar problemas. Podrías dejar la puerta abierta, podrías encontrar algo que mamá o papá no quieren que tengas. Incluso podrías estar buscando algo que ni siquiera tenemos o que está vencido.
Lo que debes hacer es expresar una necesidad: "Necesito algo para beber con el almuerzo", y luego nos aseguraremos de que tengas algo cuando te sientes a comer algo.
John Munsch, 28 de octubre de 2009. [2] [10] [11]
La inyección de dependencia involucra cuatro roles: servicios, clientes, interfaces e inyectores.
Un servicio es cualquier clase que contiene una funcionalidad útil. A su vez, un cliente es cualquier clase que utiliza servicios. Los servicios que requiere un cliente son las dependencias del cliente .
Cualquier objeto puede ser un servicio o un cliente; los nombres se relacionan únicamente con el papel que desempeñan los objetos en una inyección. El mismo objeto puede incluso ser tanto un cliente (utiliza servicios inyectados) como un servicio (se inyecta en otros objetos). Tras la inyección, el servicio pasa a formar parte del estado del cliente y está disponible para su uso. [12]
Los clientes no deberían saber cómo se implementan sus dependencias, solo sus nombres y API . Un servicio que recupera correos electrónicos , por ejemplo, puede usar los protocolos IMAP o POP3 en segundo plano, pero es probable que este detalle sea irrelevante para el código de llamada que simplemente desea recuperar un correo electrónico. Al ignorar los detalles de implementación, los clientes no necesitan cambiar cuando lo hacen sus dependencias.
El inyector , a veces también llamado ensamblador, contenedor, proveedor o fábrica, introduce servicios al cliente.
La función de los inyectores es construir y conectar gráficos de objetos complejos, donde los objetos pueden ser tanto clientes como servicios. El propio inyector puede ser muchos objetos que trabajan juntos, pero no debe ser el cliente, ya que esto crearía una dependencia circular .
Debido a que la inyección de dependencias separa cómo se construyen los objetos de cómo se utilizan, a menudo disminuye la importancia de la new
palabra clave que se encuentra en la mayoría de los lenguajes orientados a objetos . Debido a que el marco se encarga de la creación de servicios, el programador tiende a construir directamente solo objetos de valor que representan entidades en el dominio del programa (como un Employee
objeto en una aplicación empresarial o un Order
objeto en una aplicación de compras). [13] [14] [15] [16]
Como analogía, los automóviles pueden considerarse servicios que realizan la útil tarea de transportar personas de un lugar a otro. Los motores de los automóviles pueden requerir gasolina , diésel o electricidad , pero este detalle no es importante para el cliente (el conductor), a quien solo le importa si puede llegar a su destino.
Los coches presentan una interfaz uniforme a través de sus pedales, volantes y otros controles. Por lo tanto, ya no importa con qué motor se les haya "inyectado" en la línea de producción y los conductores pueden cambiar de coche según sus necesidades.
Un beneficio básico de la inyección de dependencia es la disminución del acoplamiento entre las clases y sus dependencias. [17] [18]
Al eliminar el conocimiento del cliente sobre cómo se implementan sus dependencias, los programas se vuelven más reutilizables, comprobables y mantenibles. [19]
Esto también da como resultado una mayor flexibilidad: un cliente puede actuar sobre cualquier cosa que admita la interfaz intrínseca que el cliente espera. [20]
De manera más general, la inyección de dependencia reduce el código repetitivo , ya que toda la creación de dependencia es manejada por un componente singular. [19]
Por último, la inyección de dependencias permite el desarrollo concurrente. Dos desarrolladores pueden desarrollar de forma independiente clases que se utilicen entre sí, y solo necesitan conocer la interfaz a través de la cual se comunicarán las clases. Los complementos suelen ser desarrollados por terceros que nunca se comunican con los desarrolladores del producto original. [21]
Muchos de los beneficios de la inyección de dependencia son particularmente relevantes para las pruebas unitarias .
Por ejemplo, la inyección de dependencias se puede utilizar para externalizar los detalles de configuración de un sistema en archivos de configuración, lo que permite reconfigurar el sistema sin tener que volver a compilarlo. Se pueden escribir configuraciones independientes para distintas situaciones que requieran distintas implementaciones de componentes. [22]
De manera similar, debido a que la inyección de dependencia no requiere ningún cambio en el comportamiento del código, se puede aplicar al código heredado como una refactorización . Esto hace que los clientes sean más independientes y sea más fácil realizar pruebas unitarias de forma aislada, utilizando stubs u objetos simulados , que simulan otros objetos que no están bajo prueba.
Esta facilidad de prueba es a menudo el primer beneficio que se nota al utilizar la inyección de dependencia. [23]
Los críticos de la inyección de dependencia argumentan que:
Hay tres formas principales en las que un cliente puede recibir servicios inyectados: [29]
En algunos marcos, los clientes no necesitan aceptar activamente la inyección de dependencias. En Java , por ejemplo, la reflexión puede hacer públicos los atributos privados al probar e inyectar servicios directamente. [30]
En el siguiente ejemplo de Java , la Client
clase contiene una Service
variable miembro inicializada en el constructor . El cliente construye y controla directamente qué servicio utiliza, creando una dependencia codificada.
clase pública Cliente { servicio privado servicio ; Cliente () { // La dependencia está codificada. this . service = new ExampleService (); } }
La forma más común de inyección de dependencias es que una clase solicite sus dependencias a través de su constructor . Esto garantiza que el cliente siempre esté en un estado válido, ya que no se puede crear una instancia sin sus dependencias necesarias.
clase pública Cliente { servicio privado servicio ; // La dependencia se inyecta a través de un constructor. Cliente ( Servicio servicio ) { if ( servicio == null ) { throw new IllegalArgumentException ( "el servicio no debe ser nulo" ); } this . servicio = servicio ; } }
Al aceptar dependencias a través de un método de establecimiento , en lugar de un constructor, los clientes pueden permitir que los inyectores manipulen sus dependencias en cualquier momento. Esto ofrece flexibilidad, pero dificulta garantizar que todas las dependencias se inyecten y sean válidas antes de que se use el cliente.
clase pública Cliente { servicio privado servicio ; // La dependencia se inyecta a través de un método setter. public void setService ( Service service ) { if ( service == null ) { throw new IllegalArgumentException ( " el servicio no debe ser nulo" ); } this.service = service ; } }
Con la inyección de interfaz, las dependencias ignoran completamente a sus clientes, pero aún así envían y reciben referencias a nuevos clientes.
De esta manera, las dependencias se convierten en inyectores. La clave es que el método de inyección se proporciona a través de una interfaz.
Aún se necesita un ensamblador para presentar al cliente y sus dependencias. El ensamblador toma una referencia al cliente, la convierte en la interfaz de configuración que establece esa dependencia y la pasa a ese objeto de dependencia que, a su vez, pasa una referencia a sí mismo al cliente.
Para que la inyección de interfaz tenga valor, la dependencia debe hacer algo además de simplemente pasar una referencia a sí misma. Esto podría ser actuar como una fábrica o un subensamblador para resolver otras dependencias, abstrayendo así algunos detalles del ensamblador principal. Podría ser un conteo de referencias para que la dependencia sepa cuántos clientes la están utilizando. Si la dependencia mantiene una colección de clientes, más tarde podría inyectarles a todos una instancia diferente de sí misma.
Interfaz pública ServiceSetter { void setService ( Service service ); } clase pública Cliente implementa ServiceSetter { servicio privado ; @Override public void setService ( Service service ) { if ( service == null ) { throw new IllegalArgumentException ( " el servicio no debe ser nulo" ) ; } this.service = service ; } } clase pública ServiceInjector { privada final Set < ServiceSetter > clientes = nuevo HashSet <> (); public void inject ( ServiceSetter cliente ) { this.clientes.add ( cliente ) ; cliente.setService ( new ExampleService ( ) ) ; } public void switch ( ) { for ( Cliente cliente : this.clientes ) { cliente.setService ( new OtroEjemploServicio ( ) ) ; } } } La clase pública ExampleService implementa Service {} La clase pública AnotherExampleService implementa el servicio {}
La forma más sencilla de implementar la inyección de dependencia es organizar manualmente los servicios y clientes, lo que normalmente se hace en la raíz del programa, donde comienza la ejecución.
clase pública Programa { public static void main ( String [] args ) { // Construye el servicio. Servicio service = new ExampleService ( ); // Inyectar el servicio en el cliente. Cliente cliente = new Cliente ( servicio ) ; // Utilice los objetos. System . out . println ( client . greeting ()); } }
La construcción manual puede ser más compleja e involucrar constructores , fábricas u otros patrones de construcción .
La inyección manual de dependencias suele ser tediosa y propensa a errores en proyectos de gran envergadura, por lo que se promueve el uso de marcos que automatizan el proceso. La inyección manual de dependencias se convierte en un marco de inyección de dependencias una vez que el código de construcción ya no es personalizado para la aplicación y, en cambio, es universal. [31] Si bien son útiles, estas herramientas no son necesarias para realizar la inyección de dependencias. [32] [33]
Algunos marcos, como Spring , pueden usar archivos de configuración externos para planificar la composición del programa:
importar org.springframework.beans.factory.BeanFactory ; importar org.springframework.context.ApplicationContext ; importar org.springframework.context.support.ClassPathXmlApplicationContext ; clase pública Inyector { public static void main ( String [] args ) { // Los detalles sobre qué servicio concreto utilizar se almacenan en una configuración separada del programa en sí. BeanFactory beanfactory = new ClassPathXmlApplicationContext ( "Beans.xml" ); Client client = ( Client ) beanfactory . getBean ( "client" ); System . out . println ( client . greeting ()); } }
Incluso con un gráfico de objetos potencialmente largo y complejo, la única clase mencionada en el código es el punto de entrada, en este caso Client
. Client
no ha sufrido ningún cambio para trabajar con Spring y sigue siendo un POJO . [34] [35] [36] Al evitar que las anotaciones y llamadas específicas de Spring se distribuyan entre muchas clases, el sistema sigue dependiendo solo vagamente de Spring. [27]
El siguiente ejemplo muestra un componente AngularJS que recibe un servicio de saludo a través de la inyección de dependencia.
función SomeClass ( greeter ) { this.greeter = greeter ; } SomeClass . prototipo . hacerAlgo = función ( nombre ) { this . saludo . saludo ( nombre ); }
Cada aplicación AngularJS contiene un localizador de servicios responsable de la construcción y búsqueda de dependencias.
// Proporcionar la información de cableado en un módulo var myModule = angular . module ( 'myModule' , []); // Enseñe al inyector cómo crear un servicio de saludo. // El servicio de saludo depende del servicio $window. myModule . factory ( 'greeter' , function ( $window ) { return { greeting : function ( text ) { $window . alert ( text ); } }; });
Luego podemos crear un nuevo inyector que proporcione los componentes definidos en el myModule
módulo, incluido el servicio de recepción.
var injector = angular . injector ([ 'miMódulo' , 'ng' ]); var saludo = injector . get ( 'saludo' );
Para evitar el antipatrón del localizador de servicios , AngularJS permite la notación declarativa en plantillas HTML que delega la creación de componentes al inyector.
< div ng-controller = "MyController" > < botón ng-click = "sayHello()" > Hola </ botón > </ div >
función MyController ( $scope , greeter ) { $ scope.sayHello = function () { greeter.greeter ( ' Hola mundo' ) ; } ; }
La ng-controller
directiva activa el inyector para crear una instancia del controlador y sus dependencias.
Este ejemplo proporciona un ejemplo de inyección de constructor en C# .
usando Sistema ; espacio de nombres DependencyInjection ; // Nuestro cliente solo conocerá esta interfaz, no qué gamepad específico está usando. interface IGamepadFunctionality { string GetGamepadName (); void SetVibrationPower ( float power ); } // Los siguientes servicios proporcionan implementaciones concretas de la interfaz anterior.clase XBoxGamepad : IGamepadFunctionality { float vibratorPower = 1.0f ; public string GetGamepadName () => " Mando Xbox " ; public void SetVibrationPower ( float power ) = > this.vibrationPower = Math.Clamp ( power , 0.0f , 1.0f ) ; } clase PlaystationJoystick : IGamepadFunctionality { float vibratingPower = 100.0f ; public string GetGamepadName () => "Mando de PlayStation" ; public void SetVibrationPower ( float power ) => this . vibratingPower = Math . Clamp ( power * 100.0f , 0.0f , 100.0f ); } clase SteamController : IGamepadFunctionality { double vibrating = 1.0 ; public string GetGamepadName () => " Mando de Steam " ; public void SetVibrationPower ( float power ) = > this.vibrating = Convert.ToDouble ( Math.Clamp ( power , 0.0f , 1.0f ) ) ; } // Esta clase es el cliente que recibe un servicio. class Gamepad { IGamepadFunctionality gamepadFunctionality ; // El servicio se inyecta a través del constructor y se almacena en el campo anterior. public Gamepad ( IGamepadFunctionality gamepadFunctionality ) => this . gamepadFunctionality = gamepadFunctionality ; public void Showcase () { // Se utiliza el servicio inyectado. var gamepadName = this . gamepadFunctionality . GetGamepadName (); var message = $"Estamos usando el {gamepadName} en este momento, ¿quieres cambiar la potencia de vibración?" ; Console . WriteLine ( message ); } } clase Program { static void Main () { var steamController = new SteamController (); // También podríamos haber pasado un XboxController, PlaystationJoystick, etc. // El gamepad no sabe lo que está usando y no lo necesita. var gamepad = new Gamepad ( steamController ); gamepad . Showcase (); } }
Go no admite clases y, por lo general, la inyección de dependencias se abstrae mediante una biblioteca dedicada que utiliza reflexión o genéricos (estos últimos se admiten desde Go 1.18 [37] ). [38] Un ejemplo más simple sin utilizar bibliotecas de inyección de dependencias se ilustra con el siguiente ejemplo de una aplicación web MVC .
Primero, pase las dependencias necesarias a un enrutador y luego del enrutador a los controladores:
enrutador de paquetes importar ( "base de datos/sql" "net/http" "ejemplo/controladores/usuarios""github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware""github.com/redis/go-redis/v9" "github.com/rs/zerolog" )tipo RoutingHandler struct { // pasar los valores por puntero más abajo en la pila de llamadas // significa que no crearemos una nueva copia, ahorrando memoria log * zerolog . Logger db * sql . DB cache * redis . Client router chi . Router } // La conexión, el registrador y el caché se inicializan normalmente en la función principal func NewRouter ( log * zerolog.Logger , db * sql.DB , cache * redis.Client , ) ( r * RoutingHandler ) { rtr : = chi.NewRouter ( ) devolver & RoutingHandler { registro : registro , base de datos : base de datos , caché : caché , enrutador : rtr , } } func ( r * RoutingHandler ) SetupUsersRoutes ( ) { uc : = users.NewController ( r.log , r.db , r.cache ) r . router . get ( "/users/:name" , func ( w http . ResponseWriter , r * http . Request ) { uc . get ( w , r ) }) }
Luego, puedes acceder a los campos privados de la estructura en cualquier método que sea su puntero receptor, sin violar la encapsulación.
usuarios del paquete importar ( "base de datos/sql" "net/http" "ejemplo/modelos""github.com/go-chi/chi/v5" "github.com/redis/go-redis/v9" "github.com/rs/zerolog" )tipo Controlador estructura { log * zerolog . Logger modelos de almacenamiento . UserStorage caché * redis . Cliente } func NewController ( log * zerolog.Logger , db * sql.DB , cache * redis.Client ) * Controller { return & Controller { log : log , almacenamiento : models.NewUserStorage ( db ) , cache : cache , } } func ( uc * Controller ) Get ( whttp.ResponseWriter , r * http.Request ) { // tenga en cuenta que también podemos envolver el registro en un middleware, esto es para fines de demostración uc.log.Info ( ) . Msg ( " Obteniendo usuario " ) parámetro de usuario : = chi . URLParam ( r , "nombre" ) var usuario * modelos . Usuario // obtener el usuario del caché err := uc . cache . Get ( r . Context (), userParam ). Scan ( & usuario ) si err != nil { uc . log . Error (). Err ( err ). Msg ( "Error al obtener el usuario del caché. Recuperando del almacenamiento SQL" ) } usuario , err = uc.storage.Get ( r.Context ( ), " johndoe" ) si err ! = nil { uc.log.Error ( ) . Err ( err ) .Msg ( " Error al obtener el usuario del almacenamiento SQL" ) http.Error ( w , " Error interno del servidor " , http.StatusInternalServerError ) devolver } }
Finalmente puedes utilizar la conexión de base de datos inicializada en tu función principal en la capa de acceso a datos:
modelos de paquetes importar ( "base de datos/sql" "tiempo" ) tipo ( estructura UserStorage { conn * sql . DB } Estructura de usuario { Nombre cadena `json:"name" db:"name,primarykey"` JoinedAt hora . Hora `json:"joined_at" db:"joined_at"` Correo electrónico cadena `json:"email" db:"email"` } ) func NewUserStorage ( conn * sql.DB ) * UserStorage { devuelve & UserStorage { conn : conn , } } func ( us * UserStorage ) Get ( nombre cadena ) ( usuario * Usuario , err error ) { // asumiendo que 'nombre' es una clave única consulta : = "SELECT * FROM users WHERE nombre = $1" si err := us . conn . QueryRow ( consulta , nombre ). Scan ( & usuario ); err != nulo { devolver nulo , err } devolver usuario , nulo }