DynamicObjectAdapter - Builder
DynamicObjectAdapter - klassisch
Mit den DynamicProxies kann man auch das Pattern DynamicObjectAdapter realisieren. Hierbei geht es um einen Adapter der einzelne Methoden adaptiert. Sicherlich kann man Adapter mit Delegator von Hand schreiben, aber das ist eine Menge Arbeit. Ebenfalls muss man die Adapter erweitern wenn man die Interfaces erweitert. Ein weiterer Vorteil ist, das der Adapter nicht in der Vererbungslinie der zu adaptierenden Klassen liegen muss.
Der Ansatz liegt auch hier in der Implementierung des InvocationHandlers. Bei jedem Aufruf einer Methode auf dem Proxy wird überprüft, ob es einen Adapter gibt, der die selbe Signatur besitzt. Existiert dieses, wird der Methodenaufruf an den Adapter weitergeleitet.
Die notwendigen Informationen können mittels Reflection ermittelt werden. Die Klasse des Adapters liefert alle deklarierten Methoden. Diese werden dann in der (Hash)Map adaptedMethods abgelegt. Der Schlüssel in der (Hash)Map besteht aus dem Namen der Methode und den Parametern, bzw den Typen der Parameter.
private Map<MethodIdentifier, Method> adaptedMethods = new HashMap<>();
final Class<?> adapterClass = adapter.getClass();
Method[] methods = adapterClass.getDeclaredMethods();
for (Method m : methods) {
adaptedMethods.put(new MethodIdentifier(m), m);
}
public class MethodIdentifier {
private final String name;
private final Class[] parameters;
public MethodIdentifier(Method m) {
name = m.getName();
parameters = m.getParameterTypes();
}
// we can save time by assuming that we only compare against
// other MethodIdentifier objects
public boolean equals(Object o) {
MethodIdentifier mid = (MethodIdentifier) o;
return name.equals(mid.name) && Arrays.equals(parameters, mid.parameters);
}
public int hashCode() {
return name.hashCode();
}
}
Nun kann man bei einem Auruf einer Methode nachsehen, ob in der HashMap eine korrespondierende Methode vorhanden ist. Wenn dem so ist, erfolgt ein method.invoke(..) auf dem Adapter, wenn nicht dann auf dem Original.
public class BaseInvocationHandler implements InvocationHandler {
private Map<MethodIdentifier, Method> adaptedMethods = new HashMap<>();
private Object adapter;
private Object adaptee;
@Override
public Object invoke(Object proxy, @NotNull Method method, Object[] args) throws Throwable {
if (adaptedMethods.isEmpty()) {
final Class<?> adapterClass = adapter.getClass();
Method[] methods = adapterClass.getDeclaredMethods();
for (Method m : methods) {
adaptedMethods.put(new MethodIdentifier(m), m);
}
}
try {
Method other = adaptedMethods.get(new MethodIdentifier(method));
if (other != null) {
return other.invoke(adapter, args);
} else {
return method.invoke(adaptee, args);
}
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
public BaseInvocationHandler adapter(final Object adapter) {
this.adapter = adapter;
return this;
}
public BaseInvocationHandler adaptee(final Object adaptee) {
this.adaptee = adaptee;
return this;
}
}
Wichtig ist an der Stelle natürlich, dass es keine Klassenattribute gibt, auf die bei einem Aufruf einer Methode zugegriffen wird. Es darf also kein Zustand in dem zu adaptierenden Objekt vorhanden sein.
DynamicObjectAdapter-Builder
Da es sich bei dem DynamicObjectAdapter um ein generisches Muster handelt, kann man sich überlegen ob es mittels Builder ein wenig komfortabler gestaltet werden kann. So ist die Verwendung für den Entwickler einfacher, und man hat auch hier wieder die Möglichkeit an zentralen Punkten der Erzeugung des Objekte einzugreifen.
Eine erste Erweiterung die hier auffallen wird, ist die Möglichkeit mit withMethod_XXX Methoden ein wenig mehr Sicherheit beim erzeugen der Adapter zu erlangen. In dem Basismodell des DynamicObjectAdapter kann man erst einmal alles Mögliche unterbringen. Angenehmer ist es jedoch, wenn man eine Liste der möglichen Signaturen haben würde. Nehmen wir nun an, dass wir ein Interface in der folgenden Form haben würden.
public interface Service {
String doWork_A(String txt);
String doWork_B(String txt);
}
Hier gibt es nun zwei Methoden, die mit einem Adapter versehen werden könnnten. Erzeugen wir nun zwei FunctinalInterfaces dazu. In jedem der Interfaces ist genau eine Methode definiert.
public interface ServiceAdapter_A {
String doWork_A(String txt);
}
public interface ServiceAdapter_B {
String doWork_B(String txt);
}
Nun können wir den Builder um die Methoden zum adaptieren erweitern.
private Map<MethodIdentifier, Method> adaptedMethods = new HashMap<>();
private Map<MethodIdentifier, Object> adapters = new HashMap<>();
private T original ;
private void addAdapter(Object adapter) {
final Class<?> adapterClass = adapter.getClass();
Method[] methods = adapterClass.getDeclaredMethods();
for (Method m : methods) {
final MethodIdentifier key = new MethodIdentifier(m);
adaptedMethods.put(key, m);
adapters.put(key, adapter);
}
}
public AdapterBuilder<T> withDoWork_A(ServiceAdapter_A adapter) {
addAdapter(adapter);
return this;
}
public AdapterBuilder<T> withDoWork_B(ServiceAdapter_B adapter) {
addAdapter(adapter);
return this;
}
Einer der Unterschiede zu dem ursprünglichen DynamicObjectAdapter ist nun, das wir für jede adaptierte Methode auch eine Instanz eines Adapters haben. Daraus ergibt sich eine leicht abgewandelte Version des InvocationHandlers
final InvocationHandler invocationHandler = new InvocationHandler() {
private Map<MethodIdentifier, Method> adaptedMethods = AdapterBuilder.this.adaptedMethods;
private Map<MethodIdentifier, Object> adapters = AdapterBuilder.this.adapters;
private final T original = AdapterBuilder.this.original;
@Override
public Object invoke(Object proxy, @NotNull Method method, Object[] args) throws Throwable {
try {
final MethodIdentifier key = new MethodIdentifier(method);
Method other = adaptedMethods.get(key);
if (other != null) {
return other.invoke(adapters.get(key), args);
} else {
return method.invoke(original, args);
}
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
};
Die Vewendung sieht nun wie folgt aus.
Service service = AdapterBuilder.<Service>newBuilder()
.withTarget(Service.class)
.withOriginal(new ServiceImpl())
.withDoWork_A((txt) -> txt + "_part")
.build();
System.out.println("service.doWork_A(\"Hallo Adapter\") = " + service.doWork_A("Hallo Adapter"));
ExtendedInvocationHandler
Es gibt ein paar nette Erweiterungen der Klasse Proxy die einiges mehr an Flexibilität bringen können. Zuvor extrahieren wir nun die generischen Teile. Als erstes erzeugen wir eine Klasse AdapterBuilder
public abstract class AdapterBuilder<T> {
private Class<T> target;
public AdapterBuilder<T> withTarget(Class<T> target) {
this.target = target;
return this;
}
public T build() {
return (T) Proxy.newProxyInstance(
target.getClassLoader(),
new Class[]{target},
getInvocationHandler()
);
}
protected abstract <I extends ExtendedInvocationHandler<T>> I getInvocationHandler();
}
Hier ist nun der Teil zum erzeugen der Instanz des DynamicProxy separiert. In der Klasse ExtendedInvocationHandler
public abstract class ExtendedInvocationHandler<T> implements InvocationHandler {
private Map<MethodIdentifier, Method> adaptedMethods = new HashMap<>();
private Map<MethodIdentifier, Object> adapters = new HashMap<>();
private T original;
public void setOriginal(T original) {
this.original = original;
}
public void addAdapter(Object adapter) {
final Class<?> adapterClass = adapter.getClass();
Method[] methods = adapterClass.getDeclaredMethods();
for (Method m : methods) {
final MethodIdentifier key = new MethodIdentifier(m);
adaptedMethods.put(key, m);
adapters.put(key, adapter);
}
}
@Override
public Object invoke(Object proxy, @NotNull Method method, Object[] args) throws Throwable {
try {
final MethodIdentifier key = new MethodIdentifier(method);
Method other = adaptedMethods.get(key);
if (other != null) {
return other.invoke(adapters.get(key), args);
} else {
return method.invoke(original, args);
}
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
}
Nun fehlen noch die jeweiligen Anpassungen an die zu adaptierende Klasse. Der Builder erbt von AdapterBuilder
public static class ServiceAdapterBuilder extends AdapterBuilder<Service> {
public static ServiceAdapterBuilder newBuilder() {
return new ServiceAdapterBuilder();
}
private final ServiceInvocationHandler invocationHandler = new ServiceInvocationHandler();
//evtl auch statisch initialisiert, wenn gewuenscht
// {
// invocationHandler.setOriginal(new ServiceImpl());
// }
//delegate , modify
public ServiceAdapterBuilder withDoWork_A(ServiceAdapter_A adapter) {
invocationHandler.withDoWork_A(adapter);
return this;
}
public ServiceAdapterBuilder withDoWork_B(ServiceAdapter_B adapter) {
invocationHandler.withDoWork_B(adapter);
return this;
}
public ServiceAdapterBuilder setOriginal(Service original) {
invocationHandler.setOriginal(original);
return this;
}
@Override
protected ServiceInvocationHandler getInvocationHandler() {
return invocationHandler;
}
}
Hier erkennt man schon eine Veränderung im Vergleich zu der Implementierung des vorherigen Builders. Die Methoden zum hinzufügen der Adapter gehen nicht mehr auf die generische Methode addAdapter(..) sondern auf spezielisierte Methoden. Was aber rechtfertigt den Mehraufwand?
switching Adapter
Sehen wir uns die Implementierung des InvocationHandler´s an.
public static class ServiceInvocationHandler extends ExtendedInvocationHandler<Service> {
public void withDoWork_A(ServiceAdapter_A adapter) {
addAdapter(adapter);
}
public void withDoWork_B(ServiceAdapter_B adapter) {
addAdapter(adapter);
}
}
Hier wird nun festgelegt, welche Adapter zulässing sind. Jedoch gibt es noch einen anderen Grund.
Die Klasse Proxy hat zum Beispiel die Methode isProxyClass(Class
Service service = ServiceAdapterBuilder.newBuilder()
.setOriginal(new ServiceImpl())
.withDoWork_A((txt) -> txt + "_part")
// .withTarget(Service.class) //leider als letztes.....
.buildForTarget(Service.class);
System.out.println(service.doWork_A("Hallo Adapter"));
final boolean proxyClass = Proxy.isProxyClass(service.getClass());
System.out.println("proxyClass = " + proxyClass);
//Interface auf den InvocactionHandler
final InvocationHandler invocationHandler = Proxy.getInvocationHandler(service);
final ServiceInvocationHandler serviceInvocationHandler = (ServiceInvocationHandler) invocationHandler;
serviceInvocationHandler.withDoWork_A((txt) -> txt + "_part_modified");
System.out.println(service.doWork_A("Hallo Adapter"));
Fazit
Mit ein wenig Mehraufwand kann man den DynamicObjectAdapter einem Entwickler per Builder sehr komfortabel zur Verfügung stellen. Ebenfalls ist es möglich zur Laufzeit die Adapter auszutauschen, was der erste Schritt zu einer dynamischen rekonfiguration darstellt. Die folgenden Schritte sind notwendig:
- Erzeuge für die zu adaptierenden Methoden FunctionalInterfaces
- Implementiere einen ExtendedInvocationHandler
für das Ziel-Interface - nur für die Methoden die ein korrespondierendes FunctionalInterface besitzen
- Implementiere einen AdapterBuilder
für das Ziel-Interface