La inyección de dependencias es una de esas técnicas que tienen mala reputación por ser demasiado complicadas y difíciles de entender. En realidad, DI (ya adivinaste, dependency injection) no necesita ningún framework complejo o librería sofisticada para ser útil, puedes comenzar a usarla muy fácilmente.
Empecemos con una clase de ejemplo Robot, que utiliza un objeto antena para enviar información a una ubicación remota.
// In Robot.java
public class Robot {
private String model;
private String serial;
private Antenna antenna;
public Robot(String model, String serial, double antennaLenghtInMeters){
this.model = model;
this.serial = serial;
antenna = new Antenna(antennaLenghtInMeters);
}
public void sendRobotInformation(){
String robotInfo = getRobotInformation();
antenna.sendInformation(robotInfo);
}
private String getRobotInformation(){
String robotInfo = String.format("Model: %s\nSerial: %s", model, serial);
// Any extra steps for gathering/formatting info
return robotInfo;
}
// Other methods
}
// In Antenna.java
public class Antenna {
private int lengthInMeters;
public Antenna(double lengthInMeters){
lengthInMeters = lengthInMeters;
}
public void sendInformation(String information){
// Code to send information using the antenna
System.out.println(information);
}
// Other methods and important things
}
Esta clase tiene un gran problema: sabe demasiados detalles sobre Antenna, creando una dependencia hacia esta clase. Un cambio en cualquiera de las siguientes características de Antenna obligará a Robot a cambiar en respuesta:
- El nombre de la clase
- Los argumentos para el constructor
- El nombre del método para enviar información
- Los argumentos que requiere sendInformation
La clase Robot no debería saber todas estas cosas, todo lo que le importa es que Antenna responda al mensaje sendInformation. Entonces, ¿cómo podemos resolver esto?
DI al rescate
Ya hemos visto que Robot depende de Antenna, ¿qué tal si en lugar de instanciarla en el constructor de Robot la inyectamos?
// In Robot.Java
public class Robot {
private String model;
private String serial;
private Antenna antenna;
public Robot(String model, String serial, Antenna antenna){
this.model = model;
this.serial = serial;
this.antenna = antenna;
}
// The other methods remain unchanged
// Other methods
}
//Antenna remains unchanged
//In Main
Antenna antenna = new Antenna(7);
Robot robbie = new Robot("awesomebot", "awb-46465", antenna);
robbie.sendRobotInformation();
Este código es ligeramente mejor, ya que la responsabilidad de crear un nuevo método Antenna no pertenece a Robot, en cambio, es inyectada por una fuente externa como una dependencia.
Aún podemos hacerlo mejor.
Si prestas atención, hay un problema más sutil con el código: implícitamente te dice que Robot solo está dispuesto a trabajar con un objeto de la clase Antenna, cuando en realidad, todo lo que necesita es un objeto que responda al mensaje sendInformation. Esto limita la flexibilidad que tienen los objetos robot para enviar información al vincular ese comportamiento a una clase concreta (Antenna).
En su lugar, podemos crear una abstracción para el comportamiento sendInformation y permitir que otras clases la implementen.
// MessageSender.java will contain the interface that represents our abstraction for message sending
public interface MessageSender {
public void sendInformation(String information);
}
// Robot now references the MessageSender type, instead of Antenna
public class Robot {
private String model;
private String serial;
private MessageSender messageSender;
public Robot(String model, String serial, MessageSender messageSender){
this.model = model;
this.serial = serial;
this.messageSender = messageSender;
}
public void sendRobotInformation(){
String robotInfo = getRobotInformation();
messageSender.sendInformation(robotInfo);
}
// Other methods
}
//Antenna now implements MessageSender
public class Antenna implements MessageSender {...}
La ventaja es que en lugar de depender de una clase concreta (una entidad volátil que es más probable que cambie), Robot dependerá de una abstracción (una entidad mucho más robusta con menos probabilidad de cambio). Robot ya no se preocupa por los detalles de implementación de Antenna, como el hecho de que necesitaba un valor para lengthInMeters.
Enviando datos de nuevas maneras
Ahora podemos enviar información del robot usando otras opciones. Podemos, por ejemplo, enviar mensajes usando un cable que invierte el orden del mensaje enviado. Desde el punto de vista de la clase Robot, todo lo que le importa es que la nueva clase implemente la interfaz MessageSender.
// Inverter.java
public class InverterWire implements MessageSender{
public InverterWire(){
}
public void sendInformation(String information){
// Code for sending information using the antenna
System.out.println(new StringBuilder(information).reverse().toString());
}
// Other methods and important things
}
// In Main
InverterWire wire= new InverterWire();
Robot robbie = new Robot("awesomebot", "awb-46465", wire);
robbie.sendRobotInformation();
Este enfoque nos da mucha más flexibilidad, las opciones disponibles para que nuestro Robot pueda enviar mensajes ahora son mucho más amplias.
Ah, entendido. ¿Es esa la única forma?
Acabamos de ver la dependencia siendo inyectada al construir un nuevo objeto Robot, pero no es la única forma de lograr DI. En su lugar, podemos inyectar la dependencia usando un método mutador.
public Robot(String model, String serial){
this.model = model;
this.serial = serial;
}
public void setMessageSender(MessageSender messageSender){
this.messageSender = messageSender;
}
¿Ves qué simple es DI?
Ahora puedes ir y usarla en tus propios proyectos, te ayudará a desacoplar muchas dependencias entre clases.
DIP: El principio de inversión de dependencias
La inyección de dependencias está estrechamente relacionada con un importante principio de diseño de software: el Principio de Inversión de Dependencias (la I de los principios SOLID). El principio te dice que los módulos de alto nivel no deberían depender de módulos de bajo nivel, ambos deberían depender de abstracciones.
Las abstracciones son entidades mucho más estables que las clases concretas, al depender de ellas, estamos honrando la sabia noción de que es buena idea depender de cosas que cambian menos frecuentemente de lo que cambias tú. Ahora, no necesitamos preocuparnos por la naturaleza volátil de una clase concreta, solo nos importa la interfaz.
Otra ventaja importante es que al depender de abstracciones, dejamos de preocuparnos la clase a la que pertenece un objeto y en su lugar miramos la lista de mensajes (métodos) a los que puede responder el objeto. En el ejemplo de código anterior, la clase Robot ya no se preocupa por la Antenna, todo lo que le importa es recibir un objeto que responda al mensaje sendInformation. Esto aumenta la flexibilidad del código al permitirnos usar la clase original en otros contextos.
De manera realista, no puedes hacer esto todo el tiempo, tarde o temprano encontrarás que parte de tu código necesitas depender de cosas concretas como las clases integradas String y Array. Lo que pasa es que clases como String son entidades estables en las que puedes depender; no esperas cambios drásticos en el comportamiento de tal clase. El DIP es una forma de protegerte contra clases inestables durante el desarrollo.
¿Qué pasa con los lenguajes de tipado dinámico?
Representar abstracciones en lenguajes de tipado dinámico como Ruby o Python funciona un poco diferente. No tienes acceso a herramientas como el Interface de Java o C#. En su lugar, implementas esa interfaz pública en las clases definiendo métodos comunes e implementándolos. Podemos reescribir el ejemplo anterior en ruby:
# In robot.rb
class Robot
attr_accessor :model, :serial, :message_sender
def initialize(model, serial, message_sender)
@model = model
@serial = serial
@message_sender = message_sender
end
def send_robot_information
robot_info = get_robot_information();
message_sender.send_information(robot_info)
end
# Other important methods
private
def get_robot_information
"Model: #{model} \nSerial: #{serial}"
end
end
# In antenna.rb
class Antenna
attr_accessor :length_in_meters
def initialize(length_in_meters)
@length_in_meters = length_in_meters
end
def send_information(information)
# code for sending information using the antenna
puts information
end
# Other methods and important things
end
# In main.rb
require_relative 'robot'
require_relative 'antenna'
antenna = Antenna.new(7)
robbie = Robot.new("awesomebot", "46465", antenna)
robbie.send_robot_information();
Robot solo necesita un objeto que responda a send_information. Como puedes notar, el enfoque de tipado dinámico no puede confiar en un compilador para asegurar que los tipos sean correctos, por eso necesitas documentar tus interfaces con pruebas y documentación tradicional.
¿Y que tal son los frameworks de DI?
Hay diferentes frameworks por ahí para manejar la inyección de dependencias. Lo que la mayoría de ellos hacen es usar un lenguaje de marcado (XML por ejemplo) donde especificas la estructura de tu aplicación. Cuando ejecutas tu aplicación, el framework leerá el marcado e instanciará+inyectará todos los objetos necesarios.
Parte del desdén que algunos programadores sienten contra DI proviene de malas experiencias con estos frameworks aparentemente complicados. Mientras que creo que DI puede ser usada en la mayoría de proyectos sin la necesidad de tales frameworks, los proyectos grandes pueden beneficiarse mucho de poner la estructura en marcado y componer rápidamente diferentes versiones de la aplicación. Los frameworks de DI son una herramienta poderosa, pero no son la única forma de usar DI, acabas de aprender qué fácil es implementarla, así que ve y haz buen uso de ella.
Qué hacer a continuación:
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
- Puedes encontrar las muestras de código para este artículo aquí.
- Lee el artículo de Martin Fowler sobre DI e inversión de control.