====== 13 - Dependencias entre clases ====== En el mundo del desarrollo de software, la relación entre clases es esencial para la construcción de sistemas robustos y mantenibles. La dependencia entre clases es una pieza fundamental en este entorno, donde una clase puede requerir los servicios o funcionalidades proporcionadas por otra para llevar a cabo su tarea. Sin embargo, una dependencia fuerte entre clases puede llevar a un acoplamiento excesivo, lo que dificulta la flexibilidad y la reutilización del código. Es aquí donde entra en juego el concepto de desacoplamiento, que busca reducir la dependencia entre clases y promover la modularidad y la escalabilidad en el diseño de software. En este contexto, es crucial entender los diferentes mecanismos que tenemos a nuestra disposición para lograr este desacoplamiento. Entre ellos, destacan la **Inyección de Dependencias** (**DI**) y el **Inversión de control** (**IoC**), que proporcionan técnicas efectivas para separar la configuración y la creación de objetos de su uso real, facilitando así la gestión de dependencias y la modularidad del código. En este breve recorrido, exploraremos la importancia del desacoplamiento entre clases y cómo los mecanismos de //DI// y //IoC// nos ayudan a alcanzar este objetivo. ===== Inyección de dependencias (DI) ===== La **Inyección de Dependencias** (**DI**) es un principio fundamental en el diseño de software moderno que aborda la problemática del acoplamiento entre clases al permitir que las dependencias sean proporcionadas desde el exterior en lugar de ser creadas internamente. Este enfoque promueve la flexibilidad y la reutilización del código al separar la configuración y la creación de objetos de su uso real. Para ilustrar la importancia de la DI, consideremos un ejemplo simple. Tenemos una interfaz //InterfazX// con dos implementaciones: //ClaseC// y //ClaseD//. Además, tenemos una clase //ClaseB// que depende de //InterfazX// para realizar sus operaciones. En la clase //ClaseA//, deseamos utilizar //ClaseB//, pero queremos poder elegir entre //ClaseC// y //ClaseD// como la implementación de //InterfazX// sin modificar directamente //ClaseB//. Usando //DI// podemos proporcionar flexibilidad al diseño del software al permitir la elección de implementaciones concretas de las dependencias en tiempo de ejecución, lo que facilita el mantenimiento y la evolución del código: public interface InterfazX { void metodoX(); } public class ClaseC implements InterfazX { public void metodoX() { System.out.println("Método X de ClaseC"); } } public class ClaseD implements InterfazX { public void metodoX() { System.out.println("Método X de ClaseD"); } } public class ClaseB { private InterfazX instanciaX; public ClaseB(InterfazX instanciaX) { this.instanciaX = instanciaX; } public void operar() { instanciaX.metodoX(); } } public class ClaseA { public void metodoAConClaseC() { InterfazX instanciaClaseC = new ClaseC(); ClaseB claseB = new ClaseB(instanciaClaseC); claseB.operar(); } public void metodoAConClaseD() { InterfazX instanciaClaseD = new ClaseD(); ClaseB claseB = new ClaseB(instanciaClaseD); claseB.operar(); } } En este ejemplo, //ClaseA// tiene dos métodos, //metodoAConClaseC()// y //metodoAConClaseD()//, cada uno de los cuales crea una instancia de //ClaseB// utilizando una implementación diferente de //InterfazX// mediante la inyección de dependencias. Además, la utilización de //DI// nos brinda la flexibilidad necesaria para substituir implementaciones con //mocks// durante las pruebas unitarias. Ésto nos permite simular diferentes escenarios y comportamientos de las dependencias, facilitando así la identificación y corrección de posibles fallos en el código. De esta manera, la //DI// no solo promueve la modularidad y la flexibilidad del código en nuestra arquitectura, sino que también mejora significativamente la calidad y la robustez de nuestras pruebas unitarias. En una arquitectura por capas, la //DI// emerge como una herramienta fundamental para promover la modularidad y la flexibilidad del código. Con //DI//, podemos desacoplar las capas de nuestra aplicación al proporcionar las dependencias necesarias desde el exterior en lugar de crearlas internamente. Esto facilita la sustitución de implementaciones y la realización de pruebas unitarias, lo que contribuye a un diseño más mantenible y evolutivo: @Controller public class BookController { private BookService bookService; public BookController() { BookRepository bookRepository = new BookRepositoryImpl(); this.bookService = new BookServiceImpl(bookRepository); } public interface BookService { } public class BookServiceImpl implements BookService { private BookRepository bookRepository; public BookServiceImpl(BookRepository bookRepository) { this.bookRepository = bookRepository; } } public interface BookRepository { ... } public class BookRepositoryImpl implements BookRepository { ... } En nuestra arquitectura por capas, aprovechamos el poder de //Spring// para simplificar la gestión de controladores mediante la creación automática de //Beans//. Esto nos permite centralizar la lógica de enrutamiento y la gestión de solicitudes de manera eficiente. Sin embargo, en esta etapa inicial, optamos por crear manualmente nuestros servicios en lugar de utilizar la inyección de dependencias automática que ofrece //Spring// (como por ejemplo, con la anotación** @Autowired**). Esta decisión nos brinda un mayor control sobre la configuración y la gestión de nuestras dependencias, permitiéndonos comprender mejor los fundamentos de la Inyección de Dependencias (DI). En nuestro ejemplo, //BookController// crea una instancia de //BookServiceImpl// y //BookRepositoryImpl// manualmente en su constructor. Esta aproximación, aunque más rudimentaria, nos permite comprender cómo se gestionan las dependencias en una arquitectura por capas y nos prepara para explorar en futuras etapas el uso más avanzado de la inyección de dependencias automática en //Spring//. El problema con el ejemplo anterior es que el controlador está creando objetos de tipo repositorio, con lo que la capa de presentación se está comunicando directamente con la de persistencia. Esto presenta un problema de acoplamiento indebido, ya que estamos mezclando la lógica de presentación con la lógica de acceso a datos. Para resolver este problema, podemos usar el principio de //Inversión de Control// (//IoC//). //IoC// es un patrón de diseño que busca invertir el control de la creación y gestión de objetos. En lugar de que una clase controle la creación de sus propias dependencias, estas se inyectan desde el exterior. Al aplicar //IoC// en nuestro sistema, podemos desacoplar el controlador de las implementaciones concretas de los repositorios. Esto promueve un diseño más modular y flexible, donde cada capa de la aplicación tiene una responsabilidad claramente definida. ===== Inversión de Control (IoC) ===== La **Inversión de Control** (**IoC**) es un principio de diseño de software que busca invertir el control de la creación y gestión de objetos. Tradicionalmente, en la programación orientada a objetos, una clase es responsable de crear y gestionar sus propias dependencias. Sin embargo, con //IoC//, esta responsabilidad se traslada a un componente externo, el **contenedor IoC**. El contenedor //IoC// es una infraestructura que se encarga de administrar la creación y gestión de objetos en una aplicación. Al delegar esta responsabilidad al contenedor //IoC//, las clases se vuelven más flexibles y desacopladas, lo que facilita la configuración, la extensión y el mantenimiento del sistema. Existen varios mecanismos para implementar //IoC//, siendo la //DI// uno de los más utilizados. Con //DI//, las dependencias de una clase se pasan como parámetros en lugar de ser creadas internamente. Esto permite que las clases sean más flexibles y reutilizables, ya que pueden recibir diferentes implementaciones de sus dependencias sin modificar su código interno. Por ejemplo, vamos a crear un contenedor de IoC para gestionar la creación de objetos de tipo servicio y repositorio: public class BookIoCContainer { private static BookService bookService; private static BookRepository bookRepository; public static BookService getBookService() { if(bookService == null) { bookService = new BookServiceImpl(getBookRepository()); } return bookService; } public static BookRepository getBookRepository() { if(bookRepository == null) { bookRepository = new BookRepositoryImpl(); } return bookRepository; } } En este caso, hemos creado los métodos estáticos para simplificar el proceso. Al utilizar este contenedor, podemos obtener instancias de estas clases sin tener que crearlas directamente en nuestros componentes. Fíjate que cuando creamos el servicio creamos la instancia de repositorio también, y, aprovechando la //DI//, se la pasamos al constructor del servicio. Nuestro controlador quedaría: public class BookController { private final BookService bookService; public BookController() { this.bookService = BookIoCContainer.getBookService(); } Es importante tener en cuenta que este contenedor de //IoC// es una implementación simplificada y manual. En aplicaciones más grandes y complejas, es común utilizar frameworks como //Spring Framework//, que proporcionan funcionalidades más avanzadas para la gestión de //IoC//, como la //inyección de dependencias automática// y la configuración basada en anotaciones. Estas herramientas simplifican aún más el desarrollo y la gestión de aplicaciones basadas en //IoC//. ===== Ejercicios ===== **Ejercicio 1** Modifica la práctica de la evaluación anterior para añadir //DI// y //IoC//.