¿Quién le añade los instrumentos a la orquesta?
¿Quién asigna la partitura? ¿A la orquesta o al instrumento?
La inyección de dependencias (DI) no suele hacerse de modo manual (programando una clase que lo haga), sino que se delega en una biblioteca especial (el framework DI) que lo hace, previa configuración.
El framework DI inyecta dependencias de forma universal, no de modo particular a un programa específico y a las clases que lo componen.
En un fichero de configuración orquesta.xml le indicamos los valores inyectables:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "..." ...>
<beans>
<bean id="trompeta"
class="Viento"/>
<bean id="violin"
class="Cuerda"/>
<bean id="tambor"
class="Percusion"/>
<bean id="viola"
class="Cuerda"/>
<bean id="cuarteto"
class="Orquesta">
<property name="instrumento1">
<ref bean="trompeta"/>
</property>
<property name="instrumento2">
<ref bean="violin"/>
</property>
<property name="instrumento3">
<ref bean="viola"/>
</property>
<property name="instrumento4">
<ref bean="tambor"/>
</property>
</bean>
</beans>
La inyección de la dependencia concreta la hace el contenedor (spring en este ejemplo):
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
public class PruebaOrquesta {
public static void main(String[] args) throws Exception {
BeanFactory factory =
new XmlBeanFactory(new FileInputStream("orquesta.xml"));
Orquesta orquesta = (Orquesta)factory.getBean("cuarteto");
for (Instrumento i: orquesta)
orquesta.afinar(i);
orquesta.tocar();
}
}
Un bean es una clase/componente reutilizable en Java que tiene una interfaz bien definida, según una especificación estándar de Java, que permite a un contenedor gestionar su ciclo de vida (crearlos, cambiarles valores de sus propiedades, destruirlos, etc.)
Los beans son usados por muchos frameworks, entre otros Spring:
Más info sobre Spring DI
También se puede inyectar la dependencia en el constructor.
import java.util.logging.Logger;
public class MyClass {
private final static Logger logger;
public MyClass(Logger logger) {
this.logger = logger;
// write an info log message
logger.info("This is a log message.")
}
}
Un contenedor de dependencias en el framework debe responsabilizarse de crear las instancias de Logger e inyectarlas en su sitio (normalmente vía reflexión o introspección)
Otra manera de inyectar dependencias
import org.junit.*;
import static org.junit.Assert.*;
public class OrquestaTest { // no hace falta extends
private Orquesta orquesta;
@Before
protected void setUp() {
Orquesta orquesta = new Orquesta();
orquesta.addInstrumento(new Viento("trompeta"));
orquesta.addInstrumento(new Cuerda("guitarra"));
orquesta.addInstrumento(new Percusion("tambor"));
}
@After
protected void tearDown() {
orquesta = null;
}
@Test
public void testTocar() {
assertNotNull(orquesta.tocar());
assertsEquals(orquesta.tocar(),"GD7CD7G");
}
}
¿Es necesario usar la inyección de dependencias para especificar las partituras con las que deben funcionar los instrumentos de la orquesta?
Supongamos que queremos obtener un listado ordenado por fecha de creación de todas las cuentas bancarias.
¿Cómo afecta este cambio a la versión de BankAccount ya implementada con JDK 1.5?
Resolvemos mediante inyección de dependencias...
BankAcccount.java:
import java.util.*;
import java.io.*;
import java.time.*;
public final class BankAccount implements Comparable<BankAccount> {
private final String id;
private LocalDate creationDate;
private Comparator comparator;
public BankAccount(String number) {
this.id = number;
comparator = new BankAccountComparatorById();
}
public LocalDate getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDate date) {
this.creationDate = date;
}
public String getId() {
return id;
}
public void setComparator(Comparator cmp) {
comparator = cmp;
}
@Override
public int compareTo(BankAccount other) {
if (this == other)
return 0;
assert this.equals(other) : "compareTo inconsistent with equals.";
return comparator.compare(this, other);
}
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (!(other instanceof BankAccount))
return false;
BankAccount that = (BankAccount) other;
return this.id.equals(that.getId());
}
@Override
public String toString() {
return id.toString();
}
}
BankAcccountComparatorById.java:
import java.util.Comparator;
class BankAccountComparatorById implements Comparator<BankAccount> {
public int compare(BankAccount o1, BankAccount o2) {
return o1.getId().compareTo(o2.getId());
}
}
BankAcccountComparatorByCreationDate.java:
import java.util.Comparator;
class BankAccountComparatorByCreationDate implements Comparator<BankAccount> {
public int compare(BankAccount o1, BankAccount o2) {
return o1.getCreationDate().compareTo(o2.getCreationDate());
}
}
El motor de inyección de dependencias (por ejemplo, Spring) inyectaría la clase concreta BankAccountComparatorBy... de alguna de estas formas:
Inyección a través del constructor: la clase inyectora suministra la dependencia a través del constructor de la clase dependiente.
Inyección a través de propiedades: la clase inyectora suministra la dependenca a través de un método setter de la clase dependiente.
Inyección a través de métodos (API): la clase inyectora suministra la dependencia a través de una API determinada para la que está preparada (construida/configurada) la clase dependiente.
Ahora podría definirse una anotación del tipo @comparator(BankAccountComparatorById.className) o @compareById que inyecte a BankAccount una dependencia BankAccountComparatorById en BankAccount.comparator.
En lenguajes como C++ no es típico usar un framework DI, aunque también existen (por ejemplo, [Boost.DI](https://boost-ext.github.io/di/) En C++, para que el ejemplo de la Orquesta sea testeable, utilizaremos Interfaces (clases con métodos virtuales puros) y pasaremos las dependencias por punteros inteligentes (std::unique_ptr o std::shared_ptr). ```cpp #include <gtest/gtest.h> #include <gmock/gmock.h> #include <memory> #include <string> #include <vector> // --- Interfaces --- class IPartitura { public: virtual ~IPartitura() = default; virtual std::string obtenerNotas() const = 0; }; class IInstrumento { public: virtual ~IInstrumento() = default; virtual void setPartitura(std::shared_ptr<IPartitura> p) = 0; virtual std::string tocar() = 0; }; // --- Clase Orquesta (El Sistema Bajo Prueba) --- class Orquesta { std::vector<std::shared_ptr<IInstrumento>> instrumentos; public: void agregarInstrumento(std::shared_ptr<IInstrumento> i) { instrumentos.push_back(i); } void darConcierto() { for (auto& i : instrumentos) { i->tocar(); // La orquesta hace que los instrumentos toquen } } }; ``` ```cpp class MockPartitura : public IPartitura { public: MOCK_METHOD(std::string, obtenerNotas, (), (const, override)); }; class MockInstrumento : public IInstrumento { public: MOCK_METHOD(void, setPartitura, (std::shared_ptr<IPartitura>), (override)); MOCK_METHOD(std::string, tocar, (), (override)); }; ``` ```cpp using ::testing::Return; using ::testing::Exactly; // Test 1: Verificar que el Instrumento pide las notas a la Partitura inyectada TEST(InstrumentoTest, DebeLlamarAPartituraAlTocar) { // 1. Setup: Inyectamos el Mock de Partitura en un instrumento real (p.ej. Violin) auto partituraMock = std::make_shared<MockPartitura>(); // Configuramos la expectativa: esperamos que se llame a obtenerNotas y devuelva "SOL" EXPECT_CALL(*partituraMock, obtenerNotas()) .Times(Exactly(1)) .WillOnce(Return("SOL")); // Aquí usaríamos una clase real como 'Violin', supongamos que hereda de IInstrumento // Para el ejemplo, si no tenemos la clase real implementada, el Mock basta para probar la Orquesta. } // Test 2: Verificar que la Orquesta coordina a los instrumentos TEST(OrquestaTest, DebeLlamarATocarEnTodosLosInstrumentos) { // 1. Setup Orquesta miOrquesta; auto instrumento1 = std::make_shared<MockInstrumento>(); auto instrumento2 = std::make_shared<MockInstrumento>(); // Definimos expectativas: cada instrumento debe tocar exactamente 1 vez EXPECT_CALL(*instrumento1, tocar()).Times(1).WillOnce(Return("Sonido 1")); EXPECT_CALL(*instrumento2, tocar()).Times(1).WillOnce(Return("Sonido 2")); // 2. Inyección miOrquesta.agregarInstrumento(instrumento1); miOrquesta.agregarInstrumento(instrumento2); // 3. Ejecución miOrquesta.darConcierto(); // GTest verificará automáticamente al final del test si las expectativas se cumplieron. } ``` ¿Qué ganamos con GTest y DI? - EXPECT_CALL: No solo probamos que el código no explota, sino que interactúa correctamente. Podemos asegurar que la Orquesta no se olvida de ningún músico. - Desacoplamiento total: El test de la Orquesta no necesita que el código del Violín esté terminado. Solo necesita que la interfaz IInstrumento esté definida. - Inyección Limpia: Al usar std::shared_ptr, GTest puede mantener vivo el Mock mientras la Orquesta lo necesite y destruirlo después para limpiar la memoria de la prueba.
<details> <summary>Inyección de partituras</summary> Sólo si queremos que la orquesta pueda tocar con diferentes partituras, o si queremos probar la orquesta con diferentes partituras. </details>
--- #### Ejemplo de retención de anotaciones en Java Here we will be creating 3 annotations with RetentionPolicy as SOURCE, CLASS, & RUNTIME Obtaining the array of annotations used to annotate class A, B, and C. Array a and b will be empty as their annotation are attached before runtime while array c will contain the RuntimeRetention annotation as it was marked with RUNTIME retention policy Since the class C is annotated with an annotation which which has retention policy as runtime so it can be accessed during runtime while annotations of other two classes are discarded before runtime so they can't be accessed ```java import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; ``` --- ```java @Retention(RetentionPolicy.SOURCE) @interface SourceRetention { String value() default "Source Retention"; } @Retention(RetentionPolicy.CLASS) @interface ClassRetention { String value() default "Class Retention"; } @Retention(RetentionPolicy.RUNTIME) @interface RuntimeRetention { String value() default "Runtime Retention"; } ``` --- ```java // Annotating classes A, B, and C // with our custom annotations @SourceRetention class A { } @ClassRetention class B { } @RuntimeRetention class C { }; ``` --- ```java public class RetentionPolicyDemo { public static void main(String[] args) { Annotation a[] = new A().getClass().getAnnotations(); Annotation b[] = new B().getClass().getAnnotations(); Annotation c[] = new C().getClass().getAnnotations(); // Printing the number of retained annotations of // each class at runtime System.out.println( "Number of annotations attached to " + "class A at Runtime: " + a.length); System.out.println("Number of annotations attached to " + "class B at Runtime: " + b.length); System.out.println("Number of annotations attached to " + "class C at Runtime: " + c.length); System.out.println("Annotation attached to class C: " + c[0]); } } ```