/ design patterns

IoC y DI | De la practica a la teoria

En esta serie, analizaremos algunas practicas que llevamos a cabo en nuestro día a día y los ideas detrás de ellos

En este post revisaremos el concepto de Inversión de Control e Inyección de Dependencias, así como los problemas que resuelven

problema

Dentro del desarrollo de nuestra aplicación, es algo muy común tener dependencias entre nuestras clases, por ejemplo:

public class UserService {

  UserDao userDao;
  
  public String login(final String username, final String passowrd) {
    final var user = userDao.findUserByUsername(username);
    // login logic
    return "token-for-the-session";
  }
}

public class UserDao {
  public UserModel findUserByUsername(final String username) {
    final var user = // your code to fetch the user
    return user;
  }
}

En este ejemplo, podemos observar que la clase UserService depende de UserDao, es decir, en algun momento es necesario crear una instancia de UserDao para que UserService pueda utilizarla

palo y piedra

La forma mas sencilla de resolver este tipo de dependencias es con una instanciación manual

public class UserService {

  UserDao userDao = new UserDao();

O bien, vía constructor

public class UserService {

  UserDao userDao;
  
  public UserService() {
    this.userDao = new UserDao();
  }

Una de las desventajas de este metodo es que cualquier modificacion que realicemos a la clase de UserDao impactará directamente a todas las clases que lo usen, este es un problema para el mantenimiento de la aplicacion, porque entonces nuestro sistema cuenta con un alto acoplamiento

IoC (Inversión de Control)

Inversion of Control es una idea en la cual dejamos de generar las instancias necesarias desde los componentes que los necesitan y esperamos que algún componente externo se encargue de eso. Esta idea en nuestro ejemplo sugiere que la instancia de UserDao no la creemos desde UserService, si no que la recibiremos de alguna forma, ya alguna clase externa la creará y proveerá a nuestra implementación, por lo tanto, el control de las dependencias se invierte

public class UserService {

  UserDao userDao;
  
  public UserService(final UserDao userDao) {
    // asginamos la instancia (cual sea)
    // que venga del exterior para esta instancia
    this.userDao = userDao;
  }
  
  public String login(final String username, final String passowrd) {
    final var user = userDao.findUserByUsername(username);
    // login logic
    return "token-for-the-session";
  }
}

gfj7r020r6b21

Existen distintos métodos para realizar esta inyección de la dependencia en nuestro código, pero al final, la idea es recibir la instancia del exterior. La razón por la cual mostramos este ejemplo mediante el uso de constructores, es porque de esta forma indicamos que para crear una instancia de UserService necesitamos una instancia de UserDao

Entonces, podríamos describir este componente que mantiene las referencias de nuestras clases y las dependencias entre ellas de la siguiente manera:

/**
 * Handles all the objects that we have within our application
 * and their dependencies.
 */
public class ApplicationContext {
  // quick and dirty way to keep track
  // of the instances we have for the application
  private static final Map<String, Object> objects = new HashMap<String, Object>();
  
  public void injectDependencies() {
    // manually create the instances
    final var userDao = new UserDao();
    final var userService = new UserService(userDao);
    
    // add them to the actual context
    objects.put("userDao", userDao);
    objects.put("userService", userService);
  }

  /**
   * Gets an instance based on the id.
   * @param id The id of the bean within the application context.
   * @return The corresponding object for that id or null in case it does not exist in the application
   */
  public static Object getObjectById(final String id) {
    return objects.get(id);
  }
}

public class Application {

  public static void main(final String[] args) {
    // initialize the context
    final var appContext = new ApplicationContext();
    appContext.injectDependencies();
    
    // get and use the instances as we want
    final var userService = ApplicationContext.getObjectById("userService");
    final var token = userService.login("user", "password");
    
    System.out.println("the token is", token);
  }
}

Aqui seguimos generando instancias manualmente desde la clase ApplicationContext, pero con la ventaja de poder cambiar la instancia de UserDao que se utiliza en UserSerice (o cualquier otro componente/clase) sin impactar al resto de la aplicación, y lo podemos hacer desde un único lugar, asi que el costo de mantenibilidad se reduce

dependencyinjectionidareyou

Aún así, existen algunos riesgos, por ejemplo, si no hacemos buen uso del componente ApplicationContext para indicar la relacion entre las instancias, es posible que tengamos dependencias nulas, por ejemplo: new UserService(null);

conclusión

El concepto de inversión de control ayuda a tener un sistema más flexible, esto también permite generar código más explícito referente a los componentes necesarios para su funcionamiento mediante el uso de constructores

En los siguientes posts de esta serie seguiremos viendo ejemplos sobre código limpio y más conceptos de la programación orientada a objetos desde un punto de vista práctico