Bancarelle#
La traccia#
Se siete andati in vacanza al mare, vi sarà senz’altro capitato di vedere che (specialmente alla fine della stagione) i bambini organizzano sulle spiagge, o nelle piazzette, delle bancarelle per vendere i giocattoli usati. Spesso si trova lo stesso giocattolo in bancarelle diverse e ciascun bambino decide a modo suo i prezzi, talvolta inventandosi sconti e offerte per attrarre i compratori.
Scopo del progetto è modellare le entità coinvolte in questa attività in modo da poter rappresentare più giocattoli offerti in diverse bancarelle secondo diverse politiche di prezzo e dei compratori che acquistino, potenzialmente seguendo differenti strategie d’acquisto, un certo numero di giocattoli.
Ad esempio, date queste bancarelle
Bancarella di: Massimo
num. 2 bilia di vetro, prezzo: 2
num. 3 cane di pezza, prezzo: 10
num. 10 elastico di gomma, prezzo: 1
num. 1 soldatino di stagno, prezzo: 3
Bancarella di: Carlotta
num. 4 bilia di vetro, prezzo: 1
num. 10 braccialetti di perline, prezzo: 3
num. 1 soldatino di stagno, prezzo: 5
Bancarella di: Federico
num. 10 bilia di vetro, prezzo: 3
num. 1 cane di pezza, prezzo: 5
num. 10 soldatino di stagno, prezzo: 2
un compratore che volesse acquistare 11
giocattoli soldatino di stagno
potrebbe, ad esempio, effettuare il seguente acquisto
Acquisto di: soldatino di stagno, per un costo di: 23, numero: 11 di cui:
10 da Federico
1 da Massimo
La bancarella: giocattolo, inventario e listino prezzi#
Per semplicità assumeremo che ciascun giocattolo abbia un nome (rappresentato con una stringa) e sia fatto di un dato materiale (anch’esso rappresentato da una stringa); ad esempio, una bambola di pezza, una bilia di vetro e una bilia di marmo sono tre giocattoli gli ultimi due differiscono nel materiale, ma non nel nome. Due giocattoli sono uguali se e solo se hanno lo stesso nome e sono fatti dello stesso materiale.
Ogni bancarella offre un certo insieme di giocattoli, per tener traccia di quanti e quali giocattoli offra in un certo momento è utile usare un inventario: una classe in grado di tener traccia dei giocattoli man mano aggiunti ed eliminati (ad esempio perché venduti) dalla bancarella.
Il gestore di ogni bancarella può decidere diverse politiche di prezzo per
ciascun giocattolo: ad esempio, può fissare un prezzo unitario U
per un dato
giocattolo e stabilire che il prezzo di N
giocattoli identici a esso sia dato
moltiplicativamente da U * N
, oppure applicare degli sconti (per esempio,
se N
supera la decina, applicare un 15% di sconto sulle unità eccedenti la
decina, in modo che il prezzo finale sia 10 * U + (N - 10) * U * 85 / 100
), o
vendere “tre giocattoli al prezzo di due”, e così via.
Un modo ragionevole di rappresentare queste politiche è definire un listino
che, dato un giocattolo e la quantità da acquistare, restituisca il prezzo
complessivo; più precisamente, descrivete una interfaccia Listino
e almeno
una classe che la implementi (ad esempio, quella che descriva la semplice
politica moltiplicativa).
Ogni bancarella è identificata da un proprietario (che è rappresentato tramite una stringa) e ha i suoi inventario e listino; evidentemente il listino deve permettere di conoscere il prezzo di ciascun giocattolo presente nell’inventario.
Una bancarella deve poter indicare quanti giocattoli di un certo tipo è in grado di vendere e a che prezzo, nonché procedere alla vendita (aggiornando l’inventario).
Compratore e acquisto#
Se più bancarelle offrono lo stesso giocattolo, il compratore che intenda acquistarne una certa quantità, può comporre il suo acquisto in modi diversi, decidendo di acquistare un diverso numero di giocattoli dalle varie bancarelle che lo offrono, magari cercando di minimizzare il prezzo totale.
L’acquisto (di un determinato giocattolo) è pertanto caratterizzato da: il giocattolo stesso, la quantità acquistata e il prezzo pagato, nonché dall’elenco delle bancarelle, ciascuna accompagnata dal numero di giocattoli che ha venduto.
Implementate la classe Acquisto
che consenta di gestire tali informazioni. Un
esempio di acquisto potrebbe essere
Acquisto di: soldatino di stagno, per un costo di: 23, numero: 11 di cui:
10 da Federico
1 da Massimo
Finalmente è arrivato il momento di occuparsi del compratore. Questo, una volta noto l’insieme di bancarelle da cui fare acquisti, può comprare un certo numero di giocattoli di un dato tipo seguendo diverse strategie: comprando dalla bancarella che esibisce il minor prezzo unitario, o dalle bancarelle che hanno maggior disponibilità del giocattolo, o scegliendo a caso da quali bancarelle comprare.
Vedi anche
Osservate che determinare una strategia “ottima” è in generale molto difficile e non è affatto richiesto per portare a termine il progetto, gli studenti curiosi possono farsi una idea della questione sfogliando ad esempio l’articolo Allocating procurement to capacitated suppliers with concave quantity discounts o li rapporto tecnico An exact method for the Capacitated Total Quantity Discount Problem.
Potrebbe aver senso raccogliere alcune competenze comuni a tutti i compratori in una classe astratta fornendo poi delle implementazioni concrete che realizzino in modo diverso le varie strategie d’acquisto.
In ogni modo, la classe concreta dovrà avere almeno un costruttore che riceva un
parametro di tipo Set<Bancarella>
e un metodo di segnatura
public Acquisto compra(final int num, final Giocattolo giocattolo)
che sarà usata per effettuare l’acquisto.
La classe di test#
Per ottenere la classe di test di questo esercizio, partite dalla bozza di
sorgente della funzione main
così definita
public static void main(final String[] args) {
/* Lettura dei parametri dalla linea di comando */
final int numDaComprare = Integer.parseInt(args[0]);
final Giocattolo giocattoloDaComprare = new Giocattolo(args[1], args[2]);
/* Lettura del flusso di ingresso */
final Scanner s = new Scanner(System.in);
final int numBancarelle = s.nextInt();
final Set<Bancarella> bancarelle = new HashSet<>(numBancarelle);
final Map<Giocattolo, Integer> giocattolo2prezzo = new HashMap<>();
final Inventario inventario = new Inventario();
for (int b = 0; b < numBancarelle; b++) {
/* Lettura di una bancarella */
final String proprietario = s.next();
final int numGiochi = s.nextInt();
for (int g = 0; g < numGiochi; g++) {
/* Lettura dei giochi della bancarella */
final int num = s.nextInt();
final String nome = s.next();
final String materiale = s.next();
final int prezzo = s.nextInt();
final Giocattolo giocattolo = new Giocattolo(nome, materiale);
inventario.aggiungi(num, giocattolo);
giocattolo2prezzo.put(giocattolo, prezzo);
}
/*
MODIFICARE: aggiungere l'istanziazione del listino, es:
final Listino listino = new ListinoConcreto(giocattolo2prezzo);
*/
final Bancarella bancarella = new Bancarella(proprietario, inventario, listino);
bancarelle.add(bancarella);
}
s.close();
/*
MODIFICARE: aggiungere l'istanziazione del compratore, es:
final Compratore compratore = new Compratore(bancarelle);
*/
final Acquisto ordine = compratore.compra(numDaComprare, giocattoloDaComprare);
System.out.println(ordine);
}
L’input a tale classe è fornito sia dai parametri sulla linea di comando (che indicano quale giocattolo comprare e in che quantità), che dal flusso di ingresso (che contiene una descrizione delle bancarelle).
I tre parametri sulla linea di comando indicano rispettivamente il numero di giocattoli da comprare, il nome e il materiale del giocattolo. Il flusso di ingresso contiene i seguenti elementi (separati da white-space):
un intero positivo corrispondente al numero di bancarelle;
per ciascuna bancarella:
una stringa corrispondente al nome del proprietario,
il numero di giocattoli della bancarella,
una quaterna di valori per ciascun giocattolo corrispondenti al:
numero,
nome,
materiale e
prezzo unitario.
Una volta costruito il compratore, esso dovrà effettuare l’acquisto specificato ed emetterlo nel flusso d’uscita (secondo il formato dato dall’esempio).
Esempio#
Eseguendo soluzione 11 soldatino stagno
e avendo
3
Massimo 4
3 cane pezza 10
2 bilia vetro 2
10 elastico gomma 1
1 soldatino stagno 3
Carlotta 2
10 braccialetti perline 3
4 bilia vetro 1
Federico 3
10 soldatino stagno 2
10 bilia vetro 3
1 cane pezza 5
nel flusso d’ingresso, il programma emette
Acquisto di: soldatino di stagno, numero: 11, per un costo di: 23
Federico 10
Massimo 1
nel flusso d’uscita.
Soluzione#
Il giocattolo#
La classe più elementare è il giocattolo, potrebbe essere anche un record
, ma
seguendo un approccio più convenzionale è sufficiente una classe immutabile con
due attributi il cui invariante (del tutto ovvio) può essere controllato in
costruzione:
public final String nome, materiale; public Giocattolo(final String nome, final String materiale) { this.nome = Objects.requireNonNull(nome, "Il nome non può essere null."); this.materiale = Objects.requireNonNull(materiale, "Il materiale non può essere null"); if (nome.isEmpty() || materiale.isEmpty()) throw new IllegalArgumentException("Nome e materiale non devono essere vuoti."); }
Unica cosa degna di nota è la sovrascrittura dei metodi di Object
:
@Override public int hashCode() { return Objects.hash(nome, materiale); } @Override public boolean equals(Object obj) { if (!(obj instanceof Giocattolo)) return false; final Giocattolo tmp = (Giocattolo) obj; return tmp.nome.equals(nome) && tmp.materiale.equals(materiale); }
In particolare il codice evidenziato mostra l’uso del metodo hash
di Objects
che consente di calcolare l’hashcode a partire da un elenco di attributi.
L’inventario#
Poco più complicato è l’inventario, per tener traccia del numero di giocattoli
è sufficiente una Map<Giocattolo, Integer>
il cui invariante è che non
contenga chiavi o valori nulli e contenga solo numeri positivi:
private final Map<Giocattolo, Integer> inventario = new HashMap<>();
Tra i costruttori, si osservi il costruttore copia
public Inventario(final Inventario originale) { this(originale.inventario); }
che potrà rivelarsi utile, dal momento che l’inventario è mutabile.
I metodi mutazionali devono prestare attenzione a mantenere l’invariante. Iniziamo con l’aggiunta di giocattoli: per prima cosa è necessario accertarsi che non ce ne siano già del tipo indicato (in quel caso, il numero specificato deve aggiungersi a quello già presente nella mappa), viceversa è sufficiente aggiungere una nuova entry:
public int aggiungi(final int num, final Giocattolo giocattolo) { Objects.requireNonNull(giocattolo, "Il giocattolo non può essere null."); if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); int totale = num; if (inventario.containsKey(giocattolo)) totale += inventario.get(giocattolo); inventario.put(giocattolo, totale); return totale; } public int aggiungi(final Giocattolo giocattolo) { return aggiungi(1, giocattolo); }
Si osservi che il metodo per aggiungere un singolo giocattolo delega al metodo più generale, fare il contrario sarebbe stato poco conveniente perché nel metodo generale sarebbe stato necessario effettuare un ciclo per aggiungere un giocattolo alla volta (usando il metodo di aggiunta singola).
In modo del tutto simmetrico all’aggiunta, il metodo per rimuovere un certo numero di giocattoli deve verificare che il numero rimanente di giocattoli non sia negativo (caso in cui viene sollevata una eccezione, ma lasciato immutato lo stato dell’inventario) e, nel caso sia nullo, eliminare la chiave dalla mappa (come evidenziato nel codice):
public int rimuovi(final int num, final Giocattolo giocattolo) { Objects.requireNonNull(giocattolo, "Il giocattolo non può essere null."); if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); if (!inventario.containsKey(giocattolo)) throw new NoSuchElementException("Giocattolo non presente: " + giocattolo); final int totale = inventario.get(giocattolo) - num; if (totale < 0) throw new IllegalArgumentException("Non ci sono abbastanza giocattoli: " + giocattolo); if (totale == 0) inventario.remove(giocattolo); else inventario.put(giocattolo, totale); return totale; }
I metodi osservazionali sono più elementari. Conoscere la quantità di giocattoli di un certo tipo è banale:
public int quantità(final Giocattolo giocattolo) { Objects.requireNonNull(giocattolo, "Il giocattolo non può essere null."); if (!inventario.containsKey(giocattolo)) return 0; return inventario.get(giocattolo); }
Unica cosa degna di nota è la decisione di restituire 0 nel caso l’inventario non comprenda il giocattolo (senza sollevare eccezione); questa scelta permette di evitare l’aggiunta di un metodo per sapere esplicitamente se un giocattolo sia presente o meno nell’inventario.
Per finire, appare comodo rendere la classe un iterabile sui giocattoli, in modo che (con l’ausilio del metodo precedente) si possa conoscerne completamente lo stato.
@Override public Iterator<Giocattolo> iterator() { final List<Giocattolo> giocattoli = new ArrayList<>(inventario.keySet()); Collections.sort( giocattoli, new Comparator<Giocattolo>() { @Override public int compare(Giocattolo o1, Giocattolo o2) { return o1.toString().compareTo(o2.toString()); } }); return giocattoli.iterator(); }
Si noti l’uso della classe anonima che implementa l’interfaccia
Comparator<Giocattolo>
utile a consentire una iterazione in ordine
lessicografico (della rappresentazione testuale dei giocattoli).
I listini#
Stando alla traccia, il listino è una delle entità a cui è necessario provvedere la maggior variabilità possibile di comportamenti. Per questa ragione, è utile definire una interfaccia:
public interface Listino { public boolean conosce(final Giocattolo giocattolo); public int prezzo(final int num, final Giocattolo giocattolo); }
Il primo metodo è necessario perché il secondo solleverà eccezione se interrogato su giocattoli di cui non conosce il prezzo: in un inventario può aver senso segnalare l’assenza di un giocattolo restituendo 0 alla domanda sulla sua numerosità, viceversa non è sensato dire che una cosa ha prezzo 0 se non è compresa nel listino!
A questo punto, i due comportamenti descritti nella traccia dipendono entrambi dalla conoscenza del prezzo unitario, ragion per cui appare sensato interporre una classe astratta che offra questa competenza alle due classi concrete che realizzino i comportamenti descritti.
La classe astratta deve solo tener traccia (in una mappa tra giocattoli e
interi) del prezzo unitario, l’invariante di tale rappresentazione (oltre
all’ovvia richiesta che l’attributo non sia e non contenga null
) è che i
prezzi siano tutti positivi; la classe può essere immutabile e quindi
l’invariante sarà controllato solo in costruzione (codice evidenziato):
private final Map<Giocattolo, Integer> prezzoUnitario; public AbstracListinoUnitario(final Map<Giocattolo, Integer> prezzoUnitario) { this.prezzoUnitario = new HashMap<>(); for (Map.Entry<Giocattolo, Integer> e : prezzoUnitario.entrySet()) { final Giocattolo giocattolo = Objects.requireNonNull(e.getKey(), "La mappa non può contenere chiavi null."); final Integer num = Objects.requireNonNull(e.getValue(), "La mappa non può contenere valori null."); if (num <= 0) throw new IllegalArgumentException("Il prezzp di " + giocattolo + " deve essere positivo"); this.prezzoUnitario.put(giocattolo, num); } }
La rappresentazione scelta consente di sovrascrivere (in modo da renderlo
concreto) il metodo conosce
prescritto dall’interfaccia in modo molto
elementare:
@Override public boolean conosce(final Giocattolo giocattolo) { return prezzoUnitario.containsKey(Objects.requireNonNull(giocattolo)); }
Similmente, il suo unico metodo osservazionale consente di tenere la sua rappresentazione completamente isolata da quella delle sottoclassi:
public int prezzoUnitario(final Giocattolo giocattolo) { Objects.requireNonNull(giocattolo, "Il giocattolo non può essere null."); final Integer prezzo = prezzoUnitario.get(giocattolo); if (prezzo == null) throw new NoSuchElementException("Giocattolo non trovato: " + giocattolo); return prezzo; }
Alle sottoclassi concrete, a questo punto, resta solo l’onere di rendere
concreto il metodo prezzo
(che è l’unico metodo che resta astratto, nella
classe astratta); nel caso del prezzo lineare, questo può essere fatto senza
memorizzare ulteriore stato:
@Override public int prezzo(final int num, final Giocattolo giocattolo) { if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); return prezzoUnitario(giocattolo) * num; }
Nel caso dello sconto, è necessario tener traccia di soglia e percentuale (lo stato della classe) che è immutabile e il cui banale invariante può essere controllato solo in costruzione:
private final int soglia, sconto; public ListinoScontato( final Map<Giocattolo, Integer> prezzoUnitario, final int soglia, final int sconto) { super(prezzoUnitario); if (soglia <= 0) throw new IllegalArgumentException("La soglia deve essere positiva."); this.soglia = soglia; if (sconto < 1 || sconto > 100) throw new IllegalArgumentException("Lo sconto dev'essere compreso tra 1 e 100."); this.sconto = sconto; }
Date le due informazioni di cui sopra, l’implementazione del prezzo è semplice (la parte evidenziata nel codice segue dalla definizione nella traccia):
@Override public int prezzo(int num, Giocattolo giocattolo) { if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); final int prezzoUnitario = prezzoUnitario(giocattolo); return num < soglia ? prezzoUnitario * num : soglia * prezzoUnitario + (int) (((num - soglia) * prezzoUnitario * (100 - sconto)) / 100.0); }
La bancarella#
La bancarella può essere facilmente ottenuta per composizione delle classi sviluppate sin qui; essa conterrà un inventario ed un listino (a cui delegherà il compito di rispondere a domande sui giochi disponibili e sul loro prezzo).
Oltre agli invarianti banali (concernenti la nullità, per così dire), l’unica accortezza è verificare che il listino contenga i prezzi di tutti i giocattoli nell’inventario; dal momento che durante la vita della bancarella l’inventario può solo essere ridotto e che il listino è immutabile, tale controllo può essere fatto in costruzione (codice evidenziato):
public final String proprietario; private final Listino listino; private final Inventario inventario; public Bancarella(final String proprietario, final Inventario inventario, final Listino listino) { this.proprietario = Objects.requireNonNull(proprietario, "Il proprietario non può essere null."); if (proprietario.isEmpty()) throw new IllegalArgumentException("Il proprietario non deve essere vuoto."); this.listino = Objects.requireNonNull(listino, "Il listino non può essere null."); this.inventario = new Inventario(Objects.requireNonNull(inventario, "L'inventario non può essere null.")); for (final Giocattolo g : inventario) if (!listino.conosce(g)) throw new IllegalArgumentException("Il listino manca del prezzo per: " + g); }
Si osservi l’uso del costruttore copia invocato sull’inventario, che ha lo scopo di evitare che chi ha costruito l’inventario prima dell’invocazione del costruttore non possa successivamente modificarlo (magari invalidando l’invariante di questa classe aggiungendo giocattoli non nel listino).
L’unico metodo mutazionale è quello che esegue una vendita (che di fatto comporta solo la riduzione del numero di beni in inventario):
public int vende(final int num, final Giocattolo giocattolo) { return inventario.rimuovi(num, giocattolo); }
I metodi osservazionali quantità
e prezzo
sono di banale implementazione (in
quanto delegati all’inventario e al listino):
public int quantità(final Giocattolo giocattolo) { return inventario.quantità(giocattolo); } public int prezzo(final int num, final Giocattolo giocattolo) { return listino.prezzo(num, giocattolo); } @Override public Iterator<Giocattolo> iterator() { return inventario.iterator(); }
In aggiunta, la classe è resa un Iterable<Giocattolo>
per consentire una
ispezione completa del suo stato (anche in questo caso, attraverso una delega
all’inventario).
Per concludere, siccome sarà comodo usare le bancarelle come chiavi delle mappe
o come membri degli insiemi (nelle collections), sono stati sovrascritti i
metodi equals
e hashCode
considerando uguali bancarelle col medesimo
proprietario (indipendentemente dall’inventario e dal listino).
@Override public boolean equals(Object other) { if (!(other instanceof Bancarella)) return false; return ((Bancarella) other).proprietario.equals(proprietario); } @Override public int hashCode() { return proprietario.hashCode(); }
L’acquisto#
La descrizione dell’acquisto di una determinata quantità di un giocattolo è costituita dall’informazione di quanti giocattoli di quel tipo sono stati acquistati per ciascuna bancarella e del prezzo totale pagato.
public final Giocattolo giocattolo; private final Map<Bancarella, Integer> descrizione; private int prezzo = 0, quantità = 0;
La rappresentazione di tale informazioni può essere contenuta in una mappa dalle bancarelle agli interi e da due valori che memorizzino il prezzo e la quantità totali; l’invariante prescrive che tali valori siano tenuti aggiornati in dipendenza del contenuto della mappa. Poiché la classe ha un costruttore nullo, l’invariante può essere controllato nell’unico metodo mutazionale (codice evidenziato):
public void aggiungi(final int num, final Bancarella bancarella) { if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); Objects.requireNonNull(bancarella, "La bancarella non può essere null."); if (descrizione.containsKey(bancarella)) throw new IllegalArgumentException("La bancarella è già elencata nell'acquisto."); prezzo += bancarella.prezzo(num, giocattolo); quantità += num; descrizione.put(bancarella, num); }
In particolare, si osservi che per semplicità ogni bancarella può essere
aggiunta una sola volta (altrimenti la mappa andrebbe aggiornata, come avviene
ad esempio nel metodo aggiungi
di Inventario
).
I metodi osservazionali sono in parte banali:
public int prezzo() { return prezzo; } public int quantità() { return quantità; }
Inoltre, per consentire una piena conoscenza dello stato dell’acquisto, esso è
reso un Iterable<Bancarella>
e il metodo quantità
consente di sapere, per
ciascuna delle bancarelle restituite dall’iteratore, quanti giocattoli siano
acquistati da essa:
public int quantità(final Bancarella bancarella) { Objects.requireNonNull(bancarella, "La bancarella non può essere null."); if (!descrizione.containsKey(bancarella)) throw new NoSuchElementException("L'acquisto non riguarda la bancarella specificata."); return descrizione.get(bancarella); } @Override public Iterator<Bancarella> iterator() { Set<Bancarella> bancarelle = Collections.unmodifiableSet(descrizione.keySet()); return bancarelle.iterator(); }
Si osservi che l’iteratore “protegge” con Collections.unmodifableSet
le chiavi
della mappa prima di restituirne un iteratore (che potrebbe mutare la mappa
grazie al metodo remove
).
I compratori#
La situazione del compratore richiede una variabilità di comportamenti (rispetto al modo di effettuare l’acquisto) comparabile a quella del listino. Tutti i compratori però non possono prescindere dalla conoscenza di un insieme di bancarelle da cui acquistare; dovendo condividere dello stato, appare più ragionevole mettere a capo della loro sotto-gerarchia una classe astratta.
protected final Set<Bancarella> bancarelle; public AbstractCompratore(final Set<Bancarella> bancarelle) { Objects.requireNonNull(bancarelle, "L'insime di bancarelle non può essere null."); if (bancarelle.isEmpty()) throw new IllegalArgumentException("Il mercatino deve contenere almeno una bancarella"); this.bancarelle = Set.copyOf(bancarelle); }
Riguardo alla rappresentazione essa è protected
in modo che le sottoclassi
possano accedervi, ma è dichiarata final
e l’insieme è immutabile (grazie
all’uso di Set.copyOf
, vedi codice evidenziato). Pertanto l’invariante di
questa rappresentazione è controllato in costruzione e non potrà mai essere
alterato dalle sottoclassi.
Resterà astratto il metodo che descrive la strategia di acquisto:
public abstract Acquisto compra(final int num, final Giocattolo giocattolo);
mentre può essere comodo implementare un metodo che, dato l’insieme di bancarelle, calcoli la quantità totale di giocattoli di un dato tipo presenti nei loro inventari:
public int quantità(final Giocattolo giocattolo) { int quantità = 0; for (final Bancarella b : bancarelle) quantità += b.quantità(giocattolo); return quantità; }
Supponendo quindi di limitarci ai casi descritti nella traccia, potremmo implementare la seguente gerarchia:
Alle sottoclassi concrete non resta che implementare il metodo compra
sulla
scorta dello stato condiviso con la superclasse (l’insieme delle bancarelle).
Il caso più elementare è l’acquisto “a caso”, ottenuto disponendo le bancarelle in ordine casuale (codice evidenziato) e quindi procedendo a comprare quanto più possibile da ciascuna bancarella:
@Override public Acquisto compra(int num, Giocattolo giocattolo) { Objects.requireNonNull(giocattolo, "Il giocattolo non può essere null."); if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); if (quantità(giocattolo) < num) throw new IllegalArgumentException("Non ci sono abbastanza: " + giocattolo); final Acquisto acquisto = new Acquisto(giocattolo); int rimanenti = num; final List<Bancarella> aCaso = new ArrayList<>(bancarelle); Collections.shuffle(aCaso, rng); for (final Bancarella b : aCaso) { if (rimanenti == 0) break; final int daComprare = Math.min(b.quantità(giocattolo), rimanenti); if (daComprare == 0) continue; b.vende(daComprare, giocattolo); acquisto.aggiungi(daComprare, b); rimanenti -= daComprare; } return acquisto; }
Un po’ più complessa l’altra strategia. Finché restano giocattoli da comprare, si sceglie di volta in volta la bancarella che ha il minor prezzo unitario (comprando da essa tutti i giocattoli possibili):
@Override public Acquisto compra(int num, Giocattolo giocattolo) { Objects.requireNonNull(giocattolo, "Il giocattolo non può essere null."); if (num <= 0) throw new IllegalArgumentException("Il numero deve essere positivo"); if (quantità(giocattolo) < num) throw new IllegalArgumentException("Non ci sono abbastanza: " + giocattolo); final Acquisto acquisto = new Acquisto(giocattolo); int rimanenti = num; while (rimanenti > 0) { int daComprare, minUnitario = Integer.MAX_VALUE; Bancarella min = null; for (final Bancarella b : bancarelle) { daComprare = Math.min(b.quantità(giocattolo), rimanenti); if (daComprare == 0) continue; int unitario = b.prezzo(daComprare, giocattolo) / daComprare; if (unitario < minUnitario) { min = b; minUnitario = unitario; } } daComprare = Math.min(min.quantità(giocattolo), rimanenti); min.vende(daComprare, giocattolo); acquisto.aggiungi(daComprare, min); rimanenti -= daComprare; } return acquisto; }
Ad ogni passo (codice evidenziato in giallo) si effettua di fatto una ricerca di minimo tra tutte le bancarelle che hanno il giocattolo a disposizione.
La classe di test#
La classe di test è sostanzialmente provvista dalla traccia, dato il codice sviluppato sin qui è pertanto banale ottenerla. Le uniche modifiche riguardano l’istanziazione del listino:
final Listino listino = new ListinoLineare(giocattolo2prezzo);
E l’istanziazione del compratore:
final CompratoreMinimoUnitario compratore = new CompratoreMinimoUnitario(bancarelle);