Ein ganz einfaches Beispiel, wie so eine ganz simple Dependency Injection Lösung aussehen könnte, findet sich jetzt unter:
[URL unfurl="true"]https://github.com/kneitzel/java-forum-algorithms/tree/master/src/main/java/de/kneitzel/simpledi[/URL]
Die Idee ist ganz einfach: Es gibt eine Klasse ApplicationContext. Die hält sozusagen im Kontext alle wichtigen Instanzen und hält diese aufrufbar.
Dabei gibt es eine Art Singleton: ApplicationContext ist zwar kein Singleton aber es wird ein default Context unterstützt, der halt über eine statische Methode getDefaultContext jederzeit aufrufbar ist und die man nutzen könnte.
Die eigentliche Methode ist dann getBean(Class) welche dann eine Instanz der übergebenen Klasse zurück gibt. Wenn schon für die Klasse eine Instanz da ist, dann wird diese übergeben.
Wenn nicht, wird der erste Constructor genommen um den dann aufzurufen. Für jeden Parameter wird dann rekursiv nach einer Instanz gesucht (und diese ggf. erstellt).
Um keine Endlosschleife zu bekommen wird in einem Set gemerkt, welche Klassen derzeit erstellt werden. Eine Ring-Abhängigkeit führt somit zu einer Exception.
Weiterhin habe ich ein Interface Initializable erstellt, welche implementiert werden kann. Alle Instanzen, die Initializable implementieren, werden nach der Erstellung der Instanzen noch zusätzlich initialisiert.
TestClassA und TestClassB zeigen, wie man das nutzen könnte.
[CODE=java]package de.kneitzel.simpledi;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Simple Dependency Injection
* Thread: https://www.java-forum.org/thema/reflection-gone-wrong.198603/
*/
public class ApplicationContext {
/**
* default application context.
*/
private static ApplicationContext defaultContext;
/**
* Already created instances.
*/
private Map<String, Object> beans = new ConcurrentHashMap<>();
/**
* List of classes that we currently build.
*/
private Set<Class> currentlyBuilding = new HashSet<>();
public ApplicationContext() {
beans.put(getClass().getName(), this);
}
/**
* Gets the default application context.
* @return Default application context.
*/
public static ApplicationContext getDefaultContext() {
if (defaultContext == null) {
synchronized (ApplicationContext.class) {
if (defaultContext == null) {
defaultContext = new ApplicationContext();
}
}
}
return defaultContext;
}
public synchronized <T> T getBean(final Class<? extends T> clazz) {
List<Initializable> toInitialize = new ArrayList<>();
try {
return getBean(clazz, toInitialize);
} finally {
toInitialize.forEach(Initializable::initialize);
}
}
private synchronized <T> T getBean(final Class<? extends T> clazz, final List<Initializable> toInitialize) {
if (currentlyBuilding.contains(clazz)) {
throw new IllegalStateException("Circular dependency when building " + clazz.getName());
}
System.out.println("Creating instance for " + clazz.getSimpleName());
T instance = null;
try {
instance = (T) beans.get(clazz.getName());
if (instance != null) {
return instance;
}
Constructor<T> constructor = (Constructor<T>) clazz.getConstructors()[0];
Class[] parameterClasses = constructor.getParameterTypes();
Object[] parameter = Arrays.stream(parameterClasses)
.map(c -> getBean(c, toInitialize))
.toArray();
instance = constructor.newInstance(parameter);
System.out.println("Added instance for " + clazz.getSimpleName() + " to beans (" + instance + ")");
beans.put(clazz.getName(), instance);
if (instance instanceof Initializable initializable) {
toInitialize.add(initializable);
}
} catch (Exception ex) {
currentlyBuilding.clear();
throw new IllegalStateException("Unable to instantiate class " + clazz.getName(), ex);
} finally {
currentlyBuilding.remove(clazz);
}
return instance;
}
}
[/CODE]
[CODE=java]package de.kneitzel.simpledi;
/**
* Interface that describes an Object that can be initialized.
*/
public interface Initializable {
/**
* Initialize the instance.
*/
void initialize();
}
[/CODE]
[CODE=java]package de.kneitzel.simpledi;
public class TestClassA {
private TestClassB b;
public TestClassA(TestClassB b) {
this.b = b;
}
public TestClassB getB() {
return b;
}
}
[/CODE]
[CODE=java]package de.kneitzel.simpledi;
public class TestClassB implements Initializable {
private ApplicationContext context;
private TestClassA a;
public TestClassB(ApplicationContext context) {
this.context = context;
}
@Override
public void initialize() {
System.out.println("Initialize: Getting instance for TestClassA");
a = context.getBean(TestClassA.class);
}
public ApplicationContext getContext() {
return context;
}
public TestClassA getA() {
return a;
}
}
[/CODE]
(Alternativ kann man auch auf den context als Parameter verzichten und ApplicationContext.getDefaultContext() verenden!)
[CODE=java]package de.kneitzel.simpledi;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ApplicationContextTest {
@Test
public void testApplicationContext() {
ApplicationContext context = ApplicationContext.getDefaultContext();
TestClassA a = context.getBean(TestClassA.class);
assertNotNull(a);
TestClassB b = a.getB();
assertAll(
() -> assertNotNull(b),
() -> assertNotNull(b.getA()),
() -> assertEquals(a, b.getA()),
() -> assertNotNull(b.getContext()),
() -> assertEquals(context, b.getContext())
);
}
}[/CODE]
Das einfach einmal als wirklich minimale Lösung. Hier werden jetzt noch keine Interfaces und Co unterstützt. Das wäre halt alles doch deutlich komplexer. Aber so ein Ansatz könnte für Dich schon gut sein. Deine Konstuktoren könnten alle Default Konstruktoren sein, die dann in initialize die anderen Elemente bekommt. Und da würde man dann den Startschuss über ein AppicationContext.getDefaultContext().getBean(GameMaster.class) oder so geben.
Ich hoffe, dass dies nun anschaulicher und einfacher zu verstehen war.