Protected Variations

Come progettare oggetti, sottosistemi e sistemi in modo tale che le variazioni o l'instabilità in questi elementi non abbiano un impatto indesiderato su altri elementi?

La soluzione è identificare i punti in cui sono previste variazioni o instabilità poi assegnare delle responsabilità per creare un'interfaccia stabile attorno a questi punti.

Nota

Il termine "interfaccia" è utilizzato nel senso più ampio di vista per l'accesso, e non significa solo letteralmente qualcosa come un interfaccia Java.

Esempio
...

Per esempio, il problema del servizio esterno per la contabilità risolto (e illustrato in Polymorphism) illustrano Protected Variations. Il punto di instabilità o variazione è quello corrispondente alle diverse interfacce o API dei servizi esterni. Il sistema POS deve essere in grado di integrarsi con molti servizi per la contabilità esistenti, e anche con servizi futuri prodotti da terze parti e non ancora esistenti.

Aggiungendo un livello di indirezione, un'interfaccia, e usando il polimorfismo con varie implementazioni di IAccountingAdapter, si ottiene la protezione, all'interno del sistema, da variazioni dei servizi esterni e delle loro API. Gli oggetti interni collaborano con un'interfaccia stabile; le varie implementazioni degli adattatori nascondono le variazioni nei servizi esterni.

(In fondo alla pagina un esempio con codice per una migliore comprensione)

Discussione
...

Protected Variations è un servizio molto importante. Circolava già da decenni sotto diversi nomi, tra cui information hiding.

Progettazione structure-hiding
...

Un importante principio classico della progettazione a oggetti è chiamato Don't talk to strangers o Legge di Demeter. In breve, suggerisce di evitare di creare progetti che attraversano percorsi lunghi nella struttura degli oggetti e inviano messaggi a oggetti lontani e indiretti (gli estranei).

PV (protected variations) è un principio più generale di Don't talk to strangers, poiché quest'ultimo è un caso particolare di PV.

Don't talk to strangers pone dei vincoli sugli oggetti con i quali si può comunicare all'interno di un metodo. Esso afferma che all'interno di un metodo i messaggi devono essere inviati solo ai seguenti oggetti:

  • this
  • un parametro del metodo
  • un attributo di this
  • un elemento di una collezione e la collezione è un attributo di this
  • un oggetto creato all'interno del metodo
    Lo scopo è quello di evitare di accoppiare un client alla conoscenza di oggetti indiretti e delle connessioni tra oggetti.

Gli oggetti diretti sono i familiari gli indiretti sono gli estranei.
Nel seguente esempio si viola (leggermente) Don't talk to strangers:

public class Register{
	private Sale sale;

	public void method(){
		// sale.getPayment() invia un messaggio ad un familiare (sale) per ottenere payment
		// è familiare perchè sale è un attributo di this
		// invece sale.getPayment().getTenderAmount() viola le regole
		// si sta cercando di comunicare con un attributo di sale (che è un familiare)
		// ma i suoi attributo no
		
		Money amount = sale.getPayment().getTenderAmount();
	
		// ...
	}
	// ...
}

Questo codice attraversa connessioni strutturali da un oggetto familiare (Sale) a un oggetto estraneo (il Payment), quindi invia a esso un messaggio. È solo leggermente fragile, poiché dipende dal fatto che gli oggetti Sale sono connessi a oggetti Payment. Realisticamente, è poco probabile che ciò costituisca un problema.

Tuttavia si consideri il seguente frammento di codice, che attraversa il percorso strutturale andando più lontano:

public void method(){
	AccountHolder holder = sale.getPayment().getAccount().getAccountHolder();
	// ...
}

L'esempio è forzato, ma si vede che si attraverso un lungo percorso tra oggetti andando più lontano.
Più lungo è il percorso attraversato dal programma più esso è fragile. Ciò è dovuto al fatto che la struttura degli oggetti (le connessioni) può cambiare; ciò è particolarmente vero nelle applicazioni giovani o nelle iterazioni iniziali.

La soluzione richiede l'aggiunta di nuove operazioni pubbliche ai "familiari" di un oggetto; queste operazioni forniscono le informazioni che si desiderano alla fine avere, e nascondono il modo in cui sono state ottenute. Per esempio, per supportare Don't talk to strangers per i due casi precedenti:

// caso 1
Money amount = sale.getTenderedAmountOfPayment();
// caso 2
AccountHolder holder = sale.getAccountHolderOfPayment();

Cosa non fare con PV
...

Non sprecare diverse ore nella creazione di superclassi delle classi che era davvero necessario scrivere. Ciò rende ogni cosa molto flessibile (e protetta dalle variazioni), in vista di quella situazione futura in cui il progetto ripagherà le ore perse nella creazione di superclassi, ma che immancabilmente non si verificherà.

Gli sviluppatori principianti tendono alla progettazione fragile, gli sviluppatori intermedi tendono a progetti eccessivamente fantasiosi, flessibili e generalizzati. I progettisti esperti scelgono con discernimento; magari un progetto semplice e fragile in cui il costo di un cambiamento viene bilanciato con la probabilità dello stesso.

PV è simile all'information hiding
...

PV è sostanzialmente uguale ai principi dell'information hiding e open-closed, che saranno discussi tra quale riga. Come pattern ufficiale nella comunità dei pattern, fu chiamato "Protected Variations".

Information hiding
...

David Parnas introdusse il concetto di information hiding (occultamento dell'informazione). Forse perché il termine richiama l'idea dell'incapsulamento dei dati, è stato frainteso come tale, e alcuni libri definiscono erroneamente i due concetti come sinonimi. Al contrario, con occultamento dell'infromazione Parnas intendeva nascondere informazioni sul progetto agli altri moduli, nei punti di difficoltà o di probabile cambiamento.

L'occultamento di Parnas è lo stesso principio di PV e non semplicemente l'incapsulamento dei dati, che è solo una delle numerose tecniche per nascondere le informazioni sul progetto. Tuttavia il termine è stato così ampiamente reinterpretato come sinonimo di incapsulamento dei dati, che non è più possibile usarlo nel suo significato originale senza che si creino degli equivoci.

Principio open-closed
...

Tale principio è equivalente al pattern PV e all'information hiding.

Una definizione di open-closed

I moduli devono essere sia aperti (per l'estensione, adattabili) che chiusi (il modulo è chiuso a modifiche tali da influire sui client).

Open-closed e PV sono essenzialmente due espressioni dello stesso principio. In OCP (open-closed), per "modulo" si intende ogni possibile elemento software, tra cui metodi, classi, sottosistemi, applicazioni e così via.

Nel contesto di OCP, la frase "chiuso rispetto a X" significa che i client non sono influenzati dai cambiamenti nel modulo X.
Mentre, la frase "X è aperto per le estensioni" significa che il comportamento del modulo X può essere esteso, per esempio definendo nuove classi o nuovi metodi per gestire nuovi requisiti.

Per esempio, una classe è "chiusa rispetto alle definizioni dei campi di istanza" attraverso il meccanismo dell'incapsulamento dei dati con i campi privati e l'utilizzo di metodi di accesso pubblici. Al contempo è "aperta alla modifica della definizione dei dati privati", poiché i client esterni non sono direttamente accoppiati ai dati privati.

Un esempio con il codice
...

Prima vediamo un frammento di codice che non usa protected variations.
Immaginiamo un'applicazione in cui si fruisce di un servizio di mailing e di accesso ad un database.

public class OrderProcessor{
	private DatabaseConnection dbConn;
	private EmailServive emailService;

	public OrderProcessor(){ // costruttore
		dbConn = new DatabaseConnection();
		emailService = new EmailService();
	}

	public void processOrder(Order order){
		dbConn.connect();
		dbConn.saveOrder(order);
		
		emailService.sendEmail("Order processed successfully");

		dbConn.disconnect();
	}
}

Il problema del codice sopra è che il "servizio esterno" in questo caso è EmailService e DatabaseConnection. Essi vengono implementati già in OrderProcessor ed utilizzati in loco. Il che vuol dire che se le API di questi due servizi variassero, il codice relativo a OrderProcessor dovrebbe essere ritoccato e adattarlo alle nuove API.

Ora vediamo la correzione per questo stesso frammento di codice: prima di tutto bisogna aggiungere delle interfacce una per ogni servizio.

public class ShoppingCart {
    // Supponiamo che questa classe sia il carrello di un sito di e-commerce

    private List<Item> items;

    public ShoppingCart() { // costruttore
        items = new ArrayList<>();
    }

    public void addItem(Item item) {
        items.add(item);
    }

    public void removeItem(Item item) {
        items.remove(item);
    }

    public double calculateTotalPrice() {
        double totalPrice = 0.0;
        for (Item item : items) {
            totalPrice += item.getPrice();
        }
        return totalPrice;
    }
}

Il problema di questa classe è che ShoppingCart è fortemente accoppiata a Item, se sopraggiungono dei cambiamenti in Item si rischia di dover ritoccare diverse parti del codice di ShoppingCart.
Per cui possiamo aggiungere una interfaccia:

public interface Item {
	double getPrice();
}

e fare si che, per esempio, Product sia una implementazione di Item

public class Product implements Item {
	private double price;

	public Product(double price){
		this.price = price;
	}
	
	@Override
	public double getPrice(){
		return price;
	}
}

Il codice nella prima parte resta invariato.

Ora supponiamo invece che ShoppingCart voglia fare uso di un servizio per il calcolo dello sconto degli elementi nel carrello. Verranno forniti, da terze parti, le API del modulo esterno per il calcolo dello sconto.
Dato che tali API potrebbero cambiare nel corso del tempo, anziché accoppiare ShoppingCart direttamente con una classe che implementa le API fornite. Si può creare una interfaccia con la firma dei metodi necessari e creare una classe che la implementa, nel caso in cui dovessero verificarsi dei cambiamenti, grazie all'interfaccia (e ai principi di PV) si riesce a mitigare l'impatto di tali cambiamenti.

public interface DiscountCalculator {
    double calculateDiscount(double price);
}

public class ThirdPartyDiscountCalculator implements DiscountCalculator {
    // Implementazione del calcolo dello sconto usando un servizio di terze parti

    @Override
    public double calculateDiscount(double price) {
        // Implementazione delle API del servizio di terze parti
        // ...
        return discountedPrice;
    }
}

public class ShoppingCart {
    private List<Item> items;
    private DiscountCalculator discountCalculator;

    public ShoppingCart(DiscountCalculator discountCalculator) {
        items = new ArrayList<>();
        this.discountCalculator = discountCalculator;
    }

    public void addItem(Item item) {
        items.add(item);
    }

    public void removeItem(Item item) {
        items.remove(item);
    }

    public double calculateTotalPrice() {
        double totalPrice = 0.0;
        for (Item item : items) {
            totalPrice += item.getPrice();
        }
        double discount = discountCalculator.calculateDiscount(totalPrice);
        return totalPrice - discount;
    }
}

Il punto è che se la classe ThirdPartyDiscountCalculator dovesse aggiungere una nuova feature, come segue:

public class ThirdPartyDiscountCalculator implements DiscountCalculator {
    // Implementazione del calcolo dello sconto usando un servizio di terze parti

    @Override
    public double calculateDiscount(double price) {
        // Implementazione delle API del servizio di terze parti
        // ...
        return discountedPrice;
    }

	public void newFeatureCalculation(double price) {
		// Aggiunta di una nuova feature
		// ...
	}
}

quello che succede in ShoppingCart è che l'attributo discountCalculator è del tipo DiscountCalculator (che è l'interfaccia), nell'interfaccia non è ancora stata aggiunta la firma del metodo che fa riferimento alla nuova feature, perché per il Polimorfismo verrebbe ricercata lì.
In un secondo momento si può decidere di aggiungere la firma che fa riferimento alla nuova feature e renderla disponibile anche a DiscountCalculator.