Salta al contenuto
Note per Studenti Esercizio - Adapter di classe e di oggetto per la pila (esempi del corso)

Esercizio - Adapter di classe e di oggetto per la pila (esempi del corso)

Esercizio basato sugli esempi del corso (cartella adapt: CircQ, MyStack, MyStackClassAdapter, MyStackObjectAdapter, MainClassAdapter, MainObjectAdapter, TestMyStackObjectAdapter, MyTestRunner). Teoria: Pattern strutturali - Adapter, Decorator, Composite, Facade e ProxyI pattern strutturali compongono classi e oggetti in strutture più grandi: Adapter converte l'interfaccia di una classe in quella attesa dal client (di oggetto, per composizione, o di classe, per ereditarietà multipla di classe e interfaccia); Decorator aggiunge responsabilità a un oggetto con un involucro che ha la stessa interfaccia (l'ordine dei decoratori conta); Composite tratta in modo uniforme foglie e contenitori in una struttura ad albero; Facade offre un'interfaccia semplice a un sottosistema (non cerca il polimorfismo, a differenza dell'Adapter); Proxy è un sostituto con la stessa interfaccia che controlla l'accesso (virtuale, di protezione, di cache, dinamico); Bridge e Flyweight completano le sette forme del catalogo.Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy →, Software testing - progettazione dei casi di test e JUnitIl testing esegue il programma con dati artificiali per mostrare che soddisfa i requisiti (validation testing) e per scoprirne i difetti (defect testing), ma può rivelare la presenza di errori e non la loro assenza; il test di sviluppo comprende unit test (singole classi e metodi), test dei componenti (le interfacce) e test di sistema (le interazioni); i casi di test si ricavano con le classi di equivalenza (una per partizione) e i valori limite (ai bordi delle partizioni), più linee guida per sequenze e input anomali; JUnit automatizza i test con @Test, fixture @Before/@After (@BeforeClass/@AfterClass), le asserzioni (assertEquals con atteso per primo), eccezioni attese, timeout e @Ignore; nel TDD il test si scrive prima del codice (red, green, refactor) e i test automatici diventano la suite di regressione; seguono i test di rilascio (black box sui requisiti, prestazioni) e dell'utente (alpha, beta, accettazione).Software testing - progettazione dei casi di test e JUnit →; per le code e le pile vedi anche Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →. Il codice è stato compilato ed eseguito con JDK 21; i test sono JUnit 4, eseguiti con un runner minimo (vedi la nota sul testing).

Testo

Il corso fornisce una coda circolare a due estremità CircQ<T> (metodi add, readFromHead, getFromHead, readFromTail, getFromTail, len) e un'interfaccia di pila MyStack<T> (push, pop, peek, size). Si vuole usare CircQ come implementazione di MyStack, senza modificarla.

Si richiede di: 1) realizzare l'adapter di oggetto e l'adapter di classe; 2) mostrare che il MainClassAdapter del corso non compila e correggerlo; 3) discutere i difetti della CircQ e dei test del corso (TestMyStackObjectAdapter) e scrivere un insieme di test valido per entrambi gli adapter, scritto solo contro l'interfaccia MyStack.

Soluzione

1. Ruoli e adapter

ruolo classe
Target MyStack<T>
Adaptee CircQ<T>
Adapter di oggetto MyStackObjectAdapter<T> (ha una CircQ)
Adapter di classe MyStackClassAdapter<T> (estende CircQ e implementa MyStack)
Client MainAdapters.usa(MyStack<String>, ...)

Corrispondenza dei metodi: push →\to add; pop →\to getFromTail; peek →\to readFromTail; size →\to len. L'ordine LIFO si ottiene usando la stessa estremità (la coda) per inserire e togliere: si inserisce con add (alla coda) e si toglie con getFromTail (dalla stessa coda). Usando getFromHead si avrebbe una coda FIFO.

java
import java.util.NoSuchElementException;

/** ADAPTEE (dal corso, con una correzione): coda circolare a due estremita'. */
public class CircQ<T> {
    private T[] storage;
    protected int size, head, tail;       // protected: li legge il test, come nel corso

    @SuppressWarnings("unchecked")
    public CircQ(int max) {
        storage = (T[]) new Object[max];
    }

    public CircQ() { this(10); }

    public int add(T elem) {
        if (size == storage.length) {
            // il corso lancia NoSuchElementException anche qui, ma "coda piena" non e' "elemento mancante"
            throw new IllegalStateException("coda piena");
        }
        storage[tail] = elem;
        size++;
        tail = (tail + 1) % storage.length;
        return size;
    }

    public T readFromHead() {
        if (size == 0) throw new NoSuchElementException();
        return storage[head];
    }

    public T getFromHead() {
        if (size == 0) throw new NoSuchElementException();
        T retval = storage[head];
        storage[head] = null;
        head = (head + 1) % storage.length;
        size--;
        return retval;
    }

    public T readFromTail() {
        if (size == 0) throw new NoSuchElementException();
        return storage[(tail - 1 + storage.length) % storage.length];
    }

    public T getFromTail() {
        if (size == 0) throw new NoSuchElementException();
        tail = (tail - 1 + storage.length) % storage.length;
        T retval = storage[tail];
        storage[tail] = null;
        size--;
        return retval;
    }

    public int len() { return size; }

    @Override public String toString() {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < size; i++) {
            if (i != 0) sb.append("; ");
            sb.append(storage[(i + head) % storage.length]);
        }
        return sb.append("]").toString();
    }
}
java
/** TARGET: l'interfaccia di pila che il client vuole. */
public interface MyStack<T> {
    void push(T el);
    int size();
    T pop();
    T peek();
}
java
/** ADAPTER di oggetto: ha una CircQ (composizione) e traduce le chiamate. */
public class MyStackObjectAdapter<T> implements MyStack<T> {
    final CircQ<T> adaptee = new CircQ<>();       // package-private: i test di white-box lo ispezionano

    public void push(T el) { adaptee.add(el); }
    public int size()      { return adaptee.len(); }
    public T pop()         { return adaptee.getFromTail(); }
    public T peek()        { return adaptee.readFromTail(); }

    @Override public String toString() { return adaptee.toString(); }
}
java
/** ADAPTER di classe: eredita CircQ (extends) e realizza MyStack (implements). */
public class MyStackClassAdapter<T> extends CircQ<T> implements MyStack<T> {
    public MyStackClassAdapter() { super(); }

    public void push(T el) { add(el); }
    public int size()      { return len(); }
    public T pop()         { return getFromTail(); }
    public T peek()        { return readFromTail(); }
}

2. Il MainClassAdapter del corso non compila

Il main del corso dichiara MyStack<String> msca = new MyStackClassAdapter<String>(); e poi scrive (MyStackClassAdapter<String>)msca.toString(), (MyStackClassAdapter<String>)msca.len() e ...getFromHead(). Il compilatore dà quattro errori: (1) il cast è applicato al risultato del metodo (precedenza: la chiamata si lega prima del cast), quindi String cannot be converted to MyStackClassAdapter<String>; (2) len() e getFromHead() non esistono nell'interfaccia MyStack, che è il tipo della variabile (cannot find symbol); (3) (MyStackClassAdapter<String>)msca.len() > 0 confronta un oggetto con un intero (bad operand types). Il cast va messo tra parentesi attorno alla variabile, ((MyStackClassAdapter<String>) msca).len(), ma è meglio non servirsene: il client deve usare solo l'interfaccia MyStack (size(), pop()), come usa qui sotto. Dopo la correzione lo stesso client funziona con entrambi gli adapter.

java
public class MainAdapters {
    /** Il client lavora solo con MyStack: non sa quale adapter c'e' dietro. */
    static void usa(MyStack<String> s, String[] elementi) {
        for (String e : elementi) s.push(e);
        System.out.println(s);
        while (s.size() > 0) System.out.print(s.pop() + " ");
        System.out.println();
    }

    public static void main(String[] argv) {
        String[] dati = {"a", "b", "c"};
        System.out.println("-- adapter di oggetto");
        usa(new MyStackObjectAdapter<>(), dati);
        System.out.println("-- adapter di classe");
        MyStackClassAdapter<String> ca = new MyStackClassAdapter<>();
        usa(ca, dati);
        // Con l'adapter di classe i metodi di CircQ restano visibili a chi usa il tipo concreto:
        ca.push("x");
        System.out.println("getFromHead() e' accessibile (e rompe la disciplina LIFO): " + ca.getFromHead());
    }
}

Uscita:

-- adapter di oggetto
[a; b; c]
c b a 
-- adapter di classe
[a; b; c]
c b a 
getFromHead() e' accessibile (e rompe la disciplina LIFO): x

L'ultima riga mostra il difetto dell'adapter di classe: chi usa il tipo concreto (MyStackClassAdapter) vede anche getFromHead(), che toglie dalla testa e rompe la disciplina LIFO. L'adapter di oggetto nasconde la CircQ e non ha questo problema.

3. Difetti del codice del corso e test

Nella CircQ del corso: (a) add lancia NoSuchElementException quando la coda è piena, ma l'eccezione dice "elemento mancante": per "piena" è corretta IllegalStateException (come in java.util.Queue.add); (b) getFromHead e getFromTail non azzerano la cella (storage[...] = null): l'elemento tolto resta raggiungibile e non si libera finché non è sovrascritto (memory leak logico); (c) readFromTail/getFromTail gestiscono tail == 0 con un caso a parte, che si può scrivere in una riga con (tail - 1 + len) % len.

Nei test del corso (TestMyStackObjectAdapter): (1) leggono i campi interni (msoa.adaptee.size, .head, .tail): sono test white box che si rompono se cambia la struttura interna, e non possono essere riusati per l'adapter di classe; (2) assertEquals(messaggio, effettivo, atteso) ha gli argomenti scambiati (JUnit vuole prima l'atteso): il test passa, ma i messaggi di errore sarebbero invertiti; (3) manca ogni caso di errore (pila vuota, pila piena) e il giro del buffer circolare; (4) @Before contiene un'asserzione, che appartiene a un test; (5) il MyTestRunner dipende da JUnit 4 (org.junit.runner.JUnitCore).

I test sotto usano solo MyStack, provano il comportamento (LIFO, peek, errori, giro del buffer) e valgono per entrambi gli adapter (basta cambiare la riga del @Before; i due casi sono stati eseguiti):

java
import static org.junit.Assert.assertEquals;

import java.util.NoSuchElementException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/** Stesso insieme di test su entrambi gli adapter, scritto SOLO contro l'interfaccia MyStack (black-box). */
public class TestMyStacks {
    private MyStack<String> pila;

    @Before
    public void setup() {
        pila = new MyStackObjectAdapter<>();       // cambiare in new MyStackClassAdapter<>() e rieseguire: stessi risultati
    }

    @After
    public void tearDown() { pila = null; }

    @Test
    public void pilaNuovaVuota() { assertEquals(0, pila.size()); }

    @Test
    public void pushSuPilaVuota() {
        pila.push("test");
        assertEquals(1, pila.size());
        assertEquals("test", pila.peek());          // JUnit: assertEquals(ATTESO, EFFETTIVO)
    }

    @Test
    public void ordineLifo() {
        pila.push("a"); pila.push("b"); pila.push("c");
        assertEquals("c", pila.pop());
        assertEquals("b", pila.pop());
        assertEquals("a", pila.pop());
        assertEquals(0, pila.size());
    }

    @Test
    public void peekNonRimuove() {
        pila.push("x");
        pila.peek();
        assertEquals(1, pila.size());
    }

    @Test(expected = NoSuchElementException.class)
    public void popSuPilaVuota() { pila.pop(); }

    @Test
    public void ilBufferCircolareGira() {
        for (int i = 0; i < 25; i++) { pila.push("e" + i); assertEquals("e" + i, pila.pop()); }   // oltre la capacita' 10
        assertEquals(0, pila.size());
    }

    @Test(expected = IllegalStateException.class)
    public void pilaPiena() {
        for (int i = 0; i < 11; i++) pila.push("e" + i);      // capacita' di default 10
    }
}

Esecuzione:

TestMyStacks: eseguiti 7, falliti 0, ignorati 0

Con new MyStackClassAdapter<>() nel @Before l'esito è identico (7 test, 0 falliti): è la prova che i due adapter realizzano lo stesso contratto. Il test ilBufferCircolareGira esegue 25 coppie push/pop su una capacità di 10, quindi forza head e tail a ricominciare da 0.

Versione ripasso

  • Ruoli: Target MyStack<T>, Adaptee CircQ<T>, Adapter di oggetto MyStackObjectAdapter (ha una CircQ), Adapter di classe MyStackClassAdapter (extends CircQ implements MyStack), Client usa(MyStack<String>, ...).
  • Corrispondenza: push →\to add; pop →\to getFromTail; peek →\to readFromTail; size →\to len. LIFO = stessa estremità (la coda) per inserire e togliere; con getFromHead sarebbe una coda FIFO.
java
public class MyStackObjectAdapter<T> implements MyStack<T> {
    final CircQ<T> adaptee = new CircQ<>();                   // composizione
    public void push(T el) { adaptee.add(el); }
    public int size()      { return adaptee.len(); }
    public T pop()         { return adaptee.getFromTail(); }
    public T peek()        { return adaptee.readFromTail(); }
}
public class MyStackClassAdapter<T> extends CircQ<T> implements MyStack<T> {     // ereditarietà
    public void push(T el) { add(el); }
    public int size()      { return len(); }
    public T pop()         { return getFromTail(); }
    public T peek()        { return readFromTail(); }
}
  • MainClassAdapter non compila: variabile di tipo MyStack<String> ma chiamate a len() e getFromHead() (non nell'interfaccia: cannot find symbol); cast applicato al risultato del metodo (String cannot be converted..., bad operand types). Correzione: usare solo size(), pop() dell'interfaccia (usa(MyStack<String> s, ...)).
  • Differenza: con l'adapter di classe chi usa il tipo concreto vede anche getFromHead() (rompe il LIFO: getFromHead() →\to x); l'adapter di oggetto nasconde la CircQ.
  • Difetti della CircQ: add su coda piena lancia NoSuchElementException (meglio IllegalStateException); celle non azzerate dopo getFrom... (riferimenti raggiungibili); il caso tail == 0 si scrive con (tail - 1 + len) % len.
  • Difetti dei test del corso: leggono campi interni (white box, non riusabili per l'altro adapter); assertEquals(msg, effettivo, atteso) con argomenti scambiati; mancano pila vuota, pila piena, giro del buffer; asserzione nel @Before.
java
@Test public void ordineLifo() {
    pila.push("a"); pila.push("b"); pila.push("c");
    assertEquals("c", pila.pop()); assertEquals("b", pila.pop()); assertEquals("a", pila.pop());
}
@Test(expected = NoSuchElementException.class) public void popSuPilaVuota() { pila.pop(); }
@Test(expected = IllegalStateException.class)  public void pilaPiena() { for (int i = 0; i < 11; i++) pila.push("e" + i); }
  • Esito: TestMyStacks: eseguiti 7, falliti 0, ignorati 0, con entrambi gli adapter (cambiando una riga del @Before); ilBufferCircolareGira fa 25 push/pop su capacità 10. Uscita del client: [a; b; c] poi c b a per entrambi.
  • Errori tipici: cast sul risultato invece che sulla variabile; test legati ai campi interni; atteso ed effettivo scambiati; adapter di classe che espone l'interfaccia dell'adaptee; non testare i casi di errore.

Teoria collegata