Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Java OOP: disaccoppiamento totale con protezione implicita

Come disaccoppiare due classi? Esaminiamo in questo articolo un utilizzo mirato delle interfacce come "facade", rispetto alle classi contenenti l'implementazione della logica applicativa.
Come disaccoppiare due classi? Esaminiamo in questo articolo un utilizzo mirato delle interfacce come "facade", rispetto alle classi contenenti l'implementazione della logica applicativa.
Link copiato negli appunti

Il disaccoppiamento (decoupling) tra le classi consente una elevata scalabilità del software, consentendo di ridurre le dipendenze tra classi differenti. Un basso grado di accoppiamento tra le classi garantisce un impatto minimo delle future modifiche e una migliore redistribuzione delle responsabilità e riusabilità delle classi: il concetto di decoupling assume allora un ruolo di primaria importanza nella progettazione software, e può essere affrontato da vari punti di vista.

Se nell'articolo precedente ("disaccoppiamento lost-identity") abbiamo introdotto il concetto ed una strategia di implementazione, qui cercheremo di sviluppare un altro tipo di approccio al problema. Per disaccoppiare le classi il metodo migliore consiste nell'utilizzare le interfacce, l'interfaccia costituisce un elemento facade fa cioè da proxy rispetto alla classe di utilizzo.

In realtà esistono almeno due livelli di accoppiamento:

  1. accoppiamento tra classi: in tal caso le interfacce rappresentano una soluzione ottimale se non la migliore
  2. accoppiamento tra metodi: il punto 1 deve tener conto del fatto che le classi sono accoppiate con i metodi della classe target

facciamo un esempio del punto 1, creiamo una interfaccia con un metodo di prova foo():

package prova;
public interface MyInterface {
	public void foo();
}

e la classe che la implementa:

package prova;
public class Target implements MyInterface {
@Override
public void foo() {
	// TODO...
}

infine una classe di utilizzo:

package prova;
public class Source {
private MyInterface ref;
	public Source(MyInterface ref) {
		super();
		this.ref = ref;
	}
}

Quello che segue è il corrispondente diagramma UML:

uml: disaccoppiamento tramite interfacce

uml: disaccoppiamento tramite interfacce

e se volessimo modificare un metodo?

Le classi Target e Source sono disaccoppiate tra loro. Il problema però è che esiste ancora l'accoppiamento tra i metodi: supponiamo infatti di voler cambiare il nome del metodo da foo() a foo2() o, peggio ancora , di voler aggiungere (o eliminare) un metodo come nella figura seguente:

aggiunta di un metodo all'interfaccia

aggiunta di un metodo all'interfaccia

In sostanza non si può parlare ancora di disaccoppiamento totale.

Vediamo a questo punto una possibile soluzione al problema: creiamo prima di tutto una classe che elenca le attività (activity); ciascuna di esse rappresenta concettualmente un metodo:

package com.business;
public class ActivityDescription {
	public final static String activity1 = "call foo()";
	public final static String activity2 = "call foo1()&foo2()";
	public final static String activity3 = "call foo3()";
	public final static String activity4 = "call foo1()&foo2()";
	public final static String activity5 = "call foo3()";
}

creiamo quindi la seguente interfaccia, che farà da "tramite":

package com.business;
import java.util.HashMap;
public interface ActivityProxyInterface {
public HashMap
	executeActivity(String activity, HashMap params);
}

Quest'ultima interfaccia prevede un unico metodo execute() che riceve il nome di una attività da eseguire e una HashMap che contiene i parametri di input dell'attività, mentre una ulteriore hashmap restituisce i parametri di output. Il metodo invocato quindi non si lega al tipo dei parametri sia di input che di output (si tratta di Object).

Ovviamente ci servirà una classe target che la implementa:

package com.business;
import java.util.HashMap;
public class TargetClass implements ActivityProxyInterface{
	private HashMap outParams = new HashMap();
	private void foo() {
		System.out.println("foo!");
	}
	private void foo1() {
		System.out.println("foo1");
	}
	private void foo2() {
		System.out.println("foo2");
	}
	private String getMessage() {
		return "messaggio";
	}
	@Override
	public HashMap executeActivity(String activity, HashMap params) {
		if (activity.equals(ActivityDescription.activity1)) {
		foo();
		return null;
	} else if (activity.equals(ActivityDescription.activity2)) {
		foo1();
		foo2();
		return null;
	} else if (activity.equals(ActivityDescription.activity3)) {
		outParams.clear();
		outParams.put(ActivityDescription.activity3,
		getMessage());
		System.out.println(outParams.size());
		return outParams;
	}
		return null;
	}
}

Il metodo principale sarà executeActivity() che, in funzione della attività da eseguire chiama uno o più metodi della classe stessa.

É interessante notare come i metodi della logica di business (qui simulati, ovviamente) abbiano tutti qualificatori private, quindi il metodo executeActivity() è l'unico visibile ed agisce da "dispatcher" per gli altri metodi.

Ora creiamo una ulteriore classe come quella precedente ma che implementa diversi metodi business sia come numero che come firma:/p>

package com.business;
import java.util.HashMap;
public class TargetClass implements ActivityProxyInterface{
	private HashMap outParams = new HashMap();
	private void foo() {
		System.out.println("foo!");
	}
	private void foo1() {
		System.out.println("foo1");
	}
	private void foo2() {
		System.out.println("foo2");
	}
	private String getMessage() {
		return "messaggio";
	}
	@Override
	public HashMap executeActivity(String activity, HashMap params) {
		if (activity.equals(ActivityDescription.activity1)) {
			foo();
			return null;
		} else if (activity.equals(ActivityDescription.activity2)) {
			foo1();
			foo2();
			return null;
		} else if (activity.equals(ActivityDescription.activity3)) {
			outParams.clear();
			outParams.put(ActivityDescription.activity3,
			getMessage());
			System.out.println(outParams.size());
			return outParams;
		}
		return null;
	}
}

a questo punto ci servirà un executor simile alla classe seguente:

package com.business;
import java.util.HashMap;
import java.util.Vector;
public class Executor {
	private ActivityProxyInterface proxy;
	private HashMap outParams = new HashMap();
	public void setProxy(ActivityProxyInterface proxy) {
		this.proxy = proxy;
	}
	// Attività 1
	public void exec1() {
		proxy.executeActivity(ActivityDescription.activity1, null);
		proxy.executeActivity(ActivityDescription.activity2, null);
		outParams = proxy.executeActivity(
		ActivityDescription.activity3, null);
		System.out.println((String) outParams.get(ActivityDescription.activity3));
	}
	// Attività 2
	public void exec2() {
		outParams = proxy.executeActivity(ActivityDescription.activity4, null);
		Vector res = (Vector) outParams.get(ActivityDescription.activity4);
		System.out.println(res.get(0));
		proxy.executeActivity(ActivityDescription.activity5, null);
	}
}

ed utilizzeremo per i test una classe come la seguente:

package com.business;
import java.util.HashMap;
import java.util.Vector;
public class Test {
	public void execute(ActivityProxyInterface proxy) {
		proxy.executeActivity(ActivityDescription.activity1, null);
		proxy.executeActivity(ActivityDescription.activity2, null);
		HashMap outParams = proxy.executeActivity(
		ActivityDescription.activity3, null);
		System.out.println(outParams.size());
		System.out.println(outParams.containsKey(ActivityDescription.activity3));
		System.out.println(outParams.get(ActivityDescription.activity3));
	}
	public static void main(String[] args) {
		TargetClass tc = new TargetClass();
		Executor executor = new Executor();
			executor.setProxy(tc);
			executor.exec1();
		TargetClass2 tc2 = new TargetClass2();
			executor.setProxy(tc2);
			executor.exec2();
		System.out.println(outParams.get(ActivityDescription.activity3));
	}
}

eseguendo il codice otteremo:

esecuzione dell'executor

esecuzione dell'executor

Cerchiamo ora di analizzare nel dettaglio il funzionamento dell'esempio fin qui costruito, partendo dallo schema UML che lo rappresenta:

UML: disaccoppiamento con executor

UML: disaccoppiamento con executor

UML: activity descriptor

UML: activity descriptor

verifichiamo il disaccoppiamento

Ciascuna classe Target esegue le attività descritte non i metodi, quindi l'accoppiamento tra i metodi non esiste, il disaccoppiamento riguarda anche i parametri di I/O inoltre allo stesso modo si possono gestire le eccezioni (un'unica eccezione con un riferimento indiretto nella gerarchia di ereditarietà a quella specifica, allo stesso modo di Spring DAO).

Facciamo una modifica al codice, supponiamo che la classe TargetClass2:

disaccoppiamento: modifiche ad un metodo

disaccoppiamento: modifiche ad un metodo

e ancora aggiungere:

disaccoppiamento: modifiche ad un metodo in base ad activity

disaccoppiamento: modifiche ad un metodo in base ad activity

mentre nella classe ActivityDescription potremmo procedere come evidenziato:

disaccoppiamento: modifiche all'ActivityDescriptor

disaccoppiamento: modifiche all'ActivityDescriptor

A questo punto ci manca solo la classe Executor:

disaccoppiamento: modifiche executor

disaccoppiamento: modifiche executor

É interessante notare come le modifiche vanno sempre fatte in realtà su varie classi, però è facile osservare come sia possibile caricare il nome della attività da eseguire da un file esterno (ad esempio un file di properties), invece di scriverlo nel codice come nell'esempio.

Anche la gestione degli errori è qui più semplice: risulterebbe facile ignorare eventuali nomi di attività non corrispondenti ad implementazioni (perché errati, o facenti riferimento ad implementazioni da realizzare o eliminate), così come creare meccanismi di controllo sofisticato su questo genere di errori che non ci interessa trattare in questa sede.

considerazioni finali

In generale per come abbiamo realizzato il tutto l'architettura disaccoppia totalmente le classi, oltretutto i metodi di business sono tutti protetti, e ogni componente viene visto nello stesso modo:

un componente: executeActivity

un componente: executeActivity

Infine ciascuna attività può essere composta da chiamate a più metodi (ad esempio 3 dei quali solo il secondo restituisce un valore in return).

In questa catena di chiamate a metodi basta che uno solo tra di essi restituisca un valore per determinare un valore in uscita dell'attività.

Ti consigliamo anche