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 add; pop getFromTail; peek readFromTail; size 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.
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();
}
}/** TARGET: l'interfaccia di pila che il client vuole. */
public interface MyStack<T> {
void push(T el);
int size();
T pop();
T peek();
}/** 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(); }
}/** 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.
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): xL'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):
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 0Con 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>, AdapteeCircQ<T>, Adapter di oggettoMyStackObjectAdapter(ha unaCircQ), Adapter di classeMyStackClassAdapter(extends CircQ implements MyStack), Clientusa(MyStack<String>, ...). - Corrispondenza:
pushadd;popgetFromTail;peekreadFromTail;sizelen. LIFO = stessa estremità (la coda) per inserire e togliere; congetFromHeadsarebbe una coda FIFO.
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(); }
}MainClassAdapternon compila: variabile di tipoMyStack<String>ma chiamate alen()egetFromHead()(non nell'interfaccia:cannot find symbol); cast applicato al risultato del metodo (String cannot be converted...,bad operand types). Correzione: usare solosize(),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()x); l'adapter di oggetto nasconde laCircQ. - Difetti della
CircQ:addsu coda piena lanciaNoSuchElementException(meglioIllegalStateException); celle non azzerate dopogetFrom...(riferimenti raggiungibili); il casotail == 0si 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.
@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);ilBufferCircolareGirafa 25 push/pop su capacità 10. Uscita del client:[a; b; c]poic b aper 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.