Dependency injection fatta in casa

JavaVisto il moltiplicarsi dei framework per la dependency injection in Java negli ultimi anni (CDI, Guice…), sembra ormai scontato che questo pattern sia fondamentale per la realizzazione di applicazioni enterprise complesse.

Creare un dependency injector, al contrario, è qualcosa di molto semplice. Basta usare un po’ di refection.

Prendiamo un esempio: PrizeManager è una semplice classe che, dati degli utenti e dei premi, assegna questi randomicamente, informando un altro oggetto (MainController) dello stato delle cose.

package it.nerdammer;

import java.util.Collections;
import java.util.List;

public class PrizeManager {

	@Inject
	private MainController mainController;
	
	@Inject
	private List<User> users;
	
	@Inject
	private List<Prize> prizes;
	
	public void assignPrizes() {
		mainController.start();
		
		Collections.shuffle(users);
		Collections.shuffle(prizes);
		
		for(int i=0; i<users.size() && i<prizes.size(); i++) {
			// assegna i premi
			users.get(i).winPrize(prizes.get(i));
		}
		mainController.stop();
	}
}

Il compito del dependency injector è quello di impostare i riferimenti dei campi annotati con l’annotazione @Inject.

package it.nerdammer;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DependencyInjector {

	private Map<Class<?>, Object> references;
	private Map<Class<?>, List<?>> listReferences;
	
	public DependencyInjector() {
		this.references = new HashMap<Class<?>, Object>();
		this.listReferences = new HashMap<Class<?>, List<?>>();
	}
	
	public void register(Object reference) {
		if(reference instanceof List) {
			throw new IllegalArgumentException("Usa registerList(Class, List) per le liste");
		}
		this.references.put(reference.getClass(), reference);
	}
	
	public <T> void registerList(Class<T> genericType, List<T> reference) {
		this.listReferences.put(genericType, reference);
	}
	
	public void injectReferences(Object obj) throws IllegalArgumentException, IllegalAccessException {
		Field[] fields = obj.getClass().getDeclaredFields();
		
		for(Field f : fields) {
			if(f.isAnnotationPresent(Inject.class)) {
			// Annotazione Inject presente
			
				if(f.getType().isAssignableFrom(List.class) && f.getGenericType() instanceof ParameterizedType) {
					// Injection di liste
					ParameterizedType type = (ParameterizedType) f.getGenericType();
					if(type.getActualTypeArguments().length>0 && type.getActualTypeArguments()[0] instanceof Class) {
						Class<?> runtimeClass = ((Class<?>) type.getActualTypeArguments()[0]);
					
						for(Class<?> clazz : this.listReferences.keySet()) {
							if(runtimeClass.isAssignableFrom(clazz)) {
								injectField(obj, f, this.listReferences.get(clazz));
								break;
							}
						}
					}
				} else {
					// Injection di oggetti
					for(Class<?> clazz : this.references.keySet()) {
						if(f.getType().isAssignableFrom(clazz)) {
							injectField(obj, f, this.references.get(clazz));
							break;
						}
					}
				}
			}
		}
	}
	
	protected void injectField(Object target, Field field, Object value) throws IllegalArgumentException, IllegalAccessException {
		boolean wasAccessible = true;
		if(!field.isAccessible()) {
			field.setAccessible(true);
			wasAccessible = false;
		}
	
		field.set(target, value);
	
		if(!wasAccessible) {
			field.setAccessible(false);
		}
	}
}

Per impostare le dipendenze, il dependency injector analizza tutti i campi annotati con @Inject e verifica se sono stati registrati degli oggetti di quel tipo nella variabile references. Le liste vengono trattate in maniera particolare a causa di un fenomeno strano chiamato Type Erasure, un’anomalia inserita nel compilatore Java al fine di mantenere compatibilità del bytecode con le versioni di Java precedenti alla 5.

A questo punto, il dependency injector imposta i campi senza utilizzare i metodi set (da notare, non sono presenti nella classe PrizeManager) ma semplicemente forzandone il valore.

Incollo il main per utilizzare il codice, la composizione degli altri oggetti è facile da immaginare.

package it.nerdammer;

import java.util.ArrayList;
import java.util.List;

public class CustomInjectorProgram {
	
	public static void main(String[] args) throws Exception {
	
	List<User> users = new ArrayList<User>();
	users.add(new User());
	users.add(new User());
	users.add(new User());
	users.add(new User());
	
	List<Prize> prizes = new ArrayList<Prize>();
	prizes.add(new Prize());
	prizes.add(new Prize());
	prizes.add(new Prize());
	
	MainController mainController = new MainController();
	
	// Injector setup
	DependencyInjector injector = new DependencyInjector();
	injector.register(mainController);
	injector.registerList(User.class, users);
	injector.registerList(Prize.class, prizes);
	// End setup
	
	PrizeManager manager = new PrizeManager();
	injector.injectReferences(manager);
	
	manager.assignPrizes();
	}
}

Ovviamente, così com’è, il dependency injector è poco utile ma, integrato con uno strato di attivazione degli oggetti, un object pool e un sistema di risoluzione delle dipendenze cicliche, può fare la sua bella figura.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *