El camino hacia una arquitectura software limpia - Hexagonal, Onion y Clean Architecture
¿Te interesa la arquitectura software? Espero que sí porque éste es uno de esos aspectos que deberían interesarte sea cual sea tu nivel de experiencia dentro...
Este podría ser el código:
public class MySqlDatabase {
public void persist(Data data){
// Guarda el dato en una base de datos MySql
}
}
public class Logic {
public void operation(Data input) {
// Manipular el dato
Data output = calculateOutput(input);
// Persistir el resultado
MySqlDatabase db = new MySqlDatabase();
db.persist(output);
}
}
Algunos problemas de este planteamiento son:
Las clases están fuertemente acopladas. Si ahora quiero almacenar el dato en una base de datos PostgreSQL tengo que modificar el código de la clase Logic. Es decir, me veo obligado a hacer cambios en una clase importante y estable, Logic, por culpa de una modificación en una menos estable, el modo de almacenar esos datos.
Dificultad para testear. No puedo probar fácilmente el método ‘operation’ de la clase ‘Logic’ sin hacer uso de la base de datos.
Incita a la mezcla de responsabilidades. Cuando un desarrollador, sobretodo si no tiene demasiada experiencia, deba añadir una nueva funcionalidad no tendrá claro dónde hacerlo y porqué: ¿Quién debe tratar las Excepciones lanzadas por MySQL?
Además, si replicamos este planteamiento en todas las clases de lógica y en todas las clases de servicio llegará un momento en que tengamos todo acoplado a todo, resultando en el típico “código cebolla” donde cualquier mínimo cambio puede resultar traumático.
Si nos fijamos, la clase ‘Logic’, que realiza la lógica de negocio (importante) lo único que necesita es utilizar los servicios de otra clase para persistir los datos (menos importante), así que a la clase ‘Logic’ en realidad le da igual si estos datos se guardan en MySql, PostgreSql o MongoDB, lo único que le importa es que se guarden.
La mejor manera de hacer que la clase ‘Logic’ no conozca las peculiaridades del servicio de base de datos es usando una interfaz. Las interfaces definen qué es lo que puede hacerse con el servicio, actúan como contratos o protocolos de comunicación. En este caso Logic simplemente usará una interfaz y luego, el sistema de persistencia, estará implementado de una forma u otra según lo que indique dicha interfaz.
Ahora nuestro código quedará algo así:
public interface Database {
void persist(Data data);
}
public class MySqlDatabase implements Database {
@Override
public void persist(Data data){
// Guarda el dato en una base de datos MySql
}
}
public class Logic {
private final Database database;
public Logic(Database database) {
this.database = database;
}
public void operation(Data input) {
// Manipular el dato
Data output = calculateOutput(input);
// Persistir el resultado
database.persist(output);
}
}
Hemos conseguido que ‘Logic’ ya no dependa de una implementación concreta de ‘Database’. Puede que cambie la base de datos o el API de acceso a datos, pero el concepto de “persistir un dato” es mucho más estable en el tiempo.
Pero ¿hemos invertido la dependencia? Bueno, se invertirá si es Logic quien define la interfaz. Es decir, Logic, que es la clase importante, es quién dice qué es lo que necesita de un servicio de persistencia, es el cliente del contrato. Y luego MySqlDatabase es el proveedor de ese servicio y tendrá que hacer las cosas según esté especificado en ese contrato, llamado en este caso Database. Poco a poco mi lógica de negocio crecerá y mis requisitos de persistencia también, así que iré ampliando el contrato y si el día de mañana resulta que el señor MySqlDatabase no puede cumplir mis expectativas siempre podré invitar a PostgreSqlDatabase, OracleDatabase, o incluso MongoDbDatabase y ver qué tal se comportan. Y todo ello sin modificar mis clases de lógica.
Y aún podríamos elevar un grado más nuestro diseño haciendo que ambas partes dependiesen de abstracciones (que es lo que originalmente propone este principio). De esta manera nuestro sistema estaría preparado para que distintos clientes de un mismo tipo usasen un servicio sin preocuparse de quién es su proveedor.
Pero, como habrás observado, ahora la implementación concreta de Database es recibida por constructor. Es decir, se necesita de un tercero que le diga a la clase Logic cuál es la implementación de Database que tiene que usar. ¿Quién se encarga de la creación de esa implementación?
La Inyección de Dependencias (Dependency Injection, DI) es un mecanismo que se encarga de extraer la responsabilidad de creación de instancias de un componente para delegarla en otro. En definitiva no deja de ser una técnica más para separar la construcción de la ejecución.
Existen varias técnicas para tratar los problemas de construcción de objetos. La primera y más obvia es que sea la propia aplicación quien se encargue de esa tarea actuando como controlador (por ejemplo en el método Main). Otra opción es usar algún patrón de diseño creacional como Builder o Abstract Factory. En algunos sitios se menciona también al patrón Service Locator. Bien, pues la Inyección de Dependencias es un patrón más avanzado que éstos donde se delega la función de construcción a un módulo independiente llamado contenedor de inyección de dependencias, el cual se encargará de instanciar todos los objetos y setear las dependencias entre ellos (“cablear” instancias) sin que los propios objetos sepan siquiera que existe dicho contenedor. Es más, ni siquiera la propia aplicación tendrá que preocuparse de ese contenedor.
Un ejemplo ilustrativo de contenedor (muy lejos de la realidad) podría ser el siguiente:
public class InjectContainer {
private Map<String, Object> objects;
public init() {
...
Database db = new MySqlDatabase();
Logic logic = new LogicCar(db);
objects = new HashMap<String, Object>();
objects.put("logic", logic);
...
}
}
Ahora la aplicación podrá solicitar al contenedor el objeto Logic que necesita, lo único que debe saber es su nombre: ‘logic’.
Bien, pero ¿cómo sabe el contenedor que mi aplicación requiere la inyección de MySqlDatabase en LogicCar y no un OracleDatabase en un LogicBus? Esto se resuelve a través de configuración. Podemos tener, por ejemplo, un fichero XML donde le indiquemos estas dependencias. El contenedor de inyección lo leerá y actuará en consecuencia.
Por ejemplo podríamos tener algo como esto:
<configInjectContainer>
<bean>
<name>logic</name>
<type>LogicCar</type>
<inject>MySqlDatabase</inject>
<bean>
</configInjectContainer>
El contenedor se ejecutará junto con mi aplicación, leerá el fichero XML de configuración, instanciará los objetos necesarios usando Reflection, inyectará las dependencias y le ofrecerá esos objetos a mi aplicación a través de una HashMap.
Los contenedores reales, como CDI o Spring, pueden ser configurados con archivos XML o con anotaciones en las propias clases implicadas. Además están preparados para controlar más cosas aparte de la creación e inyección de instancias.
La Inversión de Control (Inversion of Control, IoC) se refiere a que la aplicación delega algún tipo de flujo de control en un tercero, generalmente un framework. El framework se encargará de gestionar el ciclo de vida de la aplicación e irá notificando eventos a la propia aplicación para que ésta actúe en consecuencia.
Mejor con un ejemplo ;) Supongamos que tenemos una aplicación que se encarga de dar de alta un usuario. Esta aplicación solicitará los datos por consola, los validará y finalmente los almacenará.
...
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter Name: ");
String name = br.readLine();
System.out.print("Enter Login: ");
String login = br.readLine();
System.out.print("Enter Password: ");
String pwd = br.readLine();
validateUser(name, login, pwd);
persistUser(name, login, pwd);
...
En este ejemplo mi aplicación tiene todo el control: decide cuándo pedir los datos, cuando leer las respuestas y cuando procesar los datos.
Ahora imaginemos que queremos hacer lo mismo pero en vez de pedir los datos por consola usamos un panel gráfico de Swing.
...
JTextField nameField = new JTextField(25);
JTextField loginField = new JTextField(25);
JTextField pwdField = new JTextField(25);
...
JButton okButton = new JButton("Aceptar");
okButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String name = nameField.getText();
String login = nameField.getText();
String pwd = nameField.getText();
validateUser(name, login, pwd);
persistUser(name, login, pwd);
}
});
...
JPanel myPanel = new JPanel();
myPanel.add(new JLabel("Enter Name: "));
myPanel.add(nameField);
myPanel.add(new JLabel("Enter Login: "));
myPanel.add(loginField);
myPanel.add(new JLabel("Enter Password: "));
myPanel.add(pwdField);
myPanel.add(okButton);
...
Ahora el flujo de control no lo tiene mi aplicación. La aplicación inicializa un panel gráfico, declara un método que hace de callback y entrega el control al sistema de ventanas, el cual invocará a mi método cuando el usuario pulse el botón “Aceptar”. Hemos invertido el control delegándolo a una pantalla gráfica. Es decir, acabamos de hacer IoC.
IoC, también conocido como el Principio de Hollywood (“No nos llame, ya le llamaremos nosotros”), es una de las principales características que diferencian un framework de un API. Un framework normalmente se encarga de la coordinación y secuenciación de actividades de la aplicación. En cada acción el framework realiza algún tipo de trabajo y devuelve el control al cliente. En cambio un API no es más que un conjunto de rutinas, protocolos y herramientas que nos facilitan la ejecución de ciertas tareas.
Inversión de Control es un término demasiado genérico. La pregunta es: ¿qué aspecto del control se está invirtiendo? En el ejemplo anterior se está invirtiendo el control gráfico, pero también se podría invertir el control de la persistencia de datos, el control del ámbito de ejecución web (scope)… e incluso el control de la creación de objetos… en cuyo caso estaremos hablando de Inyección de Dependencias. Es decir, DI es una especialización de IoC.
La inversión de control se puede implementar de múltiples maneras. Como ocurría con la inyección de dependencias aquí también podemos hacer uso de diferentes patrones como por ejemplo Observer o Strategy. O podemos usar alguna técnica más avanzada como los “contenedores” (generalmente denominados “contenedores de contexto” o “contenedores de aplicación”). ¿Te suena esto de algo? Si has leído y comprendido el apartado anterior sobre la Inyección de Dependencias apreciarás las similitudes con el “contenedor de inyección de dependencias”. En este caso el contenedor gestionará el ciclo de vida de nuestra aplicación, no la inyección de dependencias.
Y ya que estamos, ¿por qué no usamos el mismo contenedor para que se encargue de la inyección de dependencias y además de la inversión de control? Enhorabuena, empiezas a acercarte a las entrañas de frameworks como CDI y Spring.
Hoy en día tenemos IoC por todas partes: JSF, EJB, CDI, Spring framework. Incluso ciertos componentes definidos como API, como por ejemplo JPA (Java Persistence API) definen ciertas anotaciones como @PrePersist, @PostPersist, @PreRemove, @PostRemove, @PreUpdate o @PostUpdate que pueden ser incluíadas en métodos de nuestra aplicación para que el contenedor de persistencia los llame antes y después de realizar las operaciones de persistencia, borrado o actualización de datos.
Primero porque coinciden en siglas, ambas son referidas habitualmente como DI, aunque es cierto que la inversión de dependencias, al ser un principio SOLID, es más habitual verla como DIP (Dependency Inversion Principle). De hecho en este post, para no liar más la madeja, he usado DIP para referirme a “Dependency Inversion Principle” y DI para “Dependency Injection”, pero no es extraño encontrarse por ahí las siglas DI refiriéndose simplemente a “Dependency Inversion”.
Y segundo porque suelen ser conceptos muy relacionados a día de hoy. Podemos usar el uno sin el otro, pero se ha demostrado que la inyección de dependencias es el mejor mecanismo que podemos usar para aplicar el principio de inversión de dependencias, y además es el mecanismo que implementan frameworks Java EE como CDI o Spring. Muchos desarrolladores trabajan con estos frameworks y están usando estos conceptos sin saberlo. Pero no hay que olvidar que son cosas distintas y que la inyección es un mecanismo que nos permite aplicar el principio de inversión de dependencias.
IoC es un concepto más genérico que la inyección de dependencias. DI es un tipo de IoC. La principal confusión, a mi entender, es que hoy en día los principales frameworks implementan ambos conceptos con el mismo contenedor. Tanto CDI como Spring tienen contenedores que asumen el control del ciclo de vida de los objetos (IoC) y además son capaces de inyectar dependencias (DI) que nos permiten a su vez aplicar el principio de inversión de las dependencias (DIP) en nuestra aplicación.
Y si a esto le sumamos que hay mucha información por la red donde aseguran que ambos conceptos son lo mismo (algunos de ellos de cierto prestigio) pues ya tenemos el debate servido.
Te recomiendo que le eches un vistazo a este artículo de Martin Fowler sobre la Inversión de Control y este otro donde compara IoC con la Inyección de Dependencias.
Deja un comentario