0.4 Java vs. Go
1.1.2 Soluzioni
Il codice sorgente di un programma Java è generalmente composto da molti file: un .java
per ogni classe pubblica, file contenenti dati e altri risorse utili al programma, codice di
test , ecc. Il compilatore Java (javac) e la Java Virtual Machine (java) necessaria
all’e-secuzione del bytecode generato dal compilatore, usano opportune strategie per trovare
i file necessari alla produzione e l’esecuzione, ma è facile trovarsi in difficoltà quando
si tratta di risolvere eventuali eccezioni “Class not found” e similari. Strumenti come
Gradlesemplificano molto la produzione di programmi complessi, permettendo anche di
scaricare al bisogno le librerie da cui il programma dipende. Occorre però organizzare
il codice con una struttura convenzionale e scrivere un file build.gradle che elenca le
dipendenze e altre particolarità del progetto. Gli esempi sono preparati per essere
ri-prodotti con Gradle: perciò il codice Java si trova nella cartella src main java come
previsto dalla convenzione (i test vanno invece in src test java), le risorse ulteriori in
src main resources e un build.gradle adeguato descrive il processo di assemblaggio
del programma. Ciò rende possibile eseguire il programma anche con un semplice
co-mando gradle run, che compila ed esegue il programma finale, a patto che il Software
Development Kit (SDK) di Java e Gradle siano installati correttamente; serve inoltre una
connessione di rete funzionante per scaricare le librerie necessarie (p.es. JavaFX). Gradle
1.1 Campo minato può anche essere usato da IntelliJ IDEA: è sufficiente importare il progetto segnalando la presenza del build.gradle.
Attenzione: IntelliJ IDEA è uno strumento complesso (e in continua evoluzione). Non è necessario conoscerne tutti i particolari, anche se una scorsa alla documentazione di base può aumentare parecchio la propria produttività (qui il
video introduttivodel produttore JetBrains). Utilissima è la funzionalità
Find Action(attivabile anche premendo due volte in sequenza il tasto ) che permette di cercare una funzionalità anche per nome, senza doversi ricordare in quale menù si trova o quale tasto l’attiva. Per esempio: supponiamo di voler eseguire il nostro programma con Gradle, ma non ricordare dove si trovano i comandi del plugin Gradle
1(generalmente in un ‘tab’ verticale a destra). Basterà cercare
Gradle
con
Find Actione cliccare sul risultato principale. A questo punto potremo usare la finestra Gradle per eseguire il programma, cliccando due volte sul task
run.
1. Reperire la documentazione
Durante la programmazione e la lettura del codice è molto importante disporre facilmente della documentazione di Java e delle librerie in uso. Durante la scrittura IDEA cerca di completare i simboli che scriviamo (eventualmente premendo ) e le scelte possibili appaiono in un menù vicino al cursore. Se invece stiamo leggendo codice già scritto, l’azione
Quick Documentation permette di ottenere la documentazione di un simbolo (i commenti che il programmatore ha inserito secondo lo standard
javadoc
). Ulteriori informazioni si trovano con opportune ricerche in rete: è bene fare attenzione, però, ai numeri di versione, per evitare di trovare documentazione non allineata con ciò che si sta usando.L’organizzazione orientata agli oggetti del codice Java facilita molto la ricerca delle funzio-nalità. Per esempio, le procedure che manipolano stringhe saranno per lo più metodi della classe
String
: una ricerca "java 11 string" è sufficiente per trovare ladocumentazione ufficiale online diString
e vedere elencati tutti i metodi predefiniti, oltre a scoprire una delle caratteristiche più importanti dell’implementazione diString
: si tratta di una classe di oggetti immutabili, ogni metodo che manipola una stringa, produce perciò un nuovo oggetto.2. Quanti oggettiCellvengono istanziati?
Se cerchiamo gli usi della classe
Cell
(Find Usages), escludendo i test, troviamo due punti significativi:Nel costruttore di
Minefield
, alla linea 50 di Minefield.java:aCells = new Cell[pRows][pColumns];
Qui viene creato un array bidimensionale con
pRows
righe epColumns
colonne: i valori utilizzati sono i parametri del costruttore della classeMinefield
. Si noti che viene creato l’array, ma non gli oggetti cui gli elementi dell’array si riferiscono: quindi al momento tutti gli elementi sononull.Nel metodo
Minefield.initialize
, alla linea 215 di Minefield.java:1Attenzione perché ci siano va installato il plugin Gradle e questo deve essere attivo: è peraltro lo stato delle installazioni standard di IntelliJ IDEA.
private void initialize() {
for( int row = 0; row < aCells.length; row++) {
for( int column = 0; column < aCells[0].length; column++) {
aCells[row][column] = new Cell();
aAllPositions.add(new Position(row, column));
} } }
Qui vengono effettivamente creati gli oggetti
Cell
(con newCell()
), uno per ogni ele-mento dell’array. In totale sono 8 × 20 = 160, visto che in Minesweeper.java viene creato l’unico oggettoMinesweeper
con questi valori di righe e colonne. In realtà sono 160 ogni volta che si crea un oggettoMinesweeper
: premendo si ricomincia il gioco, creando un nuovo oggettoMinesweeper
e perciò 160 nuovi oggettiCell
.3. Quanti oggetti Position vengono istanziati?
Contare gli oggetti
Position
è un po’ più difficile. Find Usages trova due punti in cui vengono creati oggetti Position.Nel primo (linea 216 di Minefield.java) viene creato un oggetto
Position
per ogniCell
.private void initialize()
{ for( int row = 0; row < aCells.length; row++) {
for( int column = 0; column < aCells[0].length; column++) {
aCells[row][column] = new Cell();
aAllPositions.add(new Position(row, column));
} }
}
Nel secondo (linea 289 di Minefield.java) si crea un oggetto
Position
quando si cercano le posizioni adiacenti a una posizione data.private List<Position> getNeighbours(Position pPosition) { List<Position> neighbours = new ArrayList<>();
for( int row = Math.max(0, pPosition.getRow() -1);
row <= Math.min(getNumberOfRows()-1, pPosition.getRow()+1);
row++)
{ for( int column = Math.max(0, pPosition.getColumn()-1);
column <= Math.min(getNumberOfColumns()-1, pPosition.getColumn()+1);
column++)
{ Position position = new Position(row, column);
if( !position.equals(pPosition)) {
1.1 Campo minato
neighbours.add(position);
} } }
return neighbours;
}
Il numero totale di oggetti
Position
, dunque, è senz’altro ≥ 160, ma dipende dal numero di volte in cui si cercano le posizioni adiacenti, quindi dallo sviluppo del gioco.Per contarli, una strategia può essere quella di aumentare un contatore a ogni creazione (cioè nel costruttore dell’oggetto Position). Il contatore potrebbe essere una variabile glo-bale del programma, ma questa sarebbe una scelta che non garantisce nessuna informa-tion hiding e qualunque porzione di codice potrebbe influenzarne il valore. Molto meglio creare una variabile condivisa solo tra gli oggetti
Position
. Ciò è possibile marcando un membro diPosition
comestatic.public class Position {
private final int aRowIndex;
private final int aColumnIndex;
static int counter = 0;
// ...
Senza alcuna indicazione di accesso (public,privateoprotected) la variabile è visibile e alterabile da tutte le classi dello stesso package (in questo caso
ca.mcgill.cs.sw
⌋ evo.minesweeper come si desume dalla linea 21). Meglio segnarla come private e aggiungere un metodopublicche permetta solo di leggerne il valore, ma non di cambiarlo (operazione riservata agli oggettiPosition
). Questi metodi sono noti col nome di getter e possono essere prodotti anche automaticamente con l’azione Create getter for.public class Position
{ private final int aRowIndex;
private final int aColumnIndex;
private static int counter = 0;
public static int getCounter() {
return counter;
} // ...
Il contatore deve poi essere incrementato nel costruttore di Position. A questo punto possiamo stamparne il valore prima del termine del programma.
public static void main(String[] pArgs) {
launch(pArgs);
System.out.println("# of Position objects: " + Position.getCounter());
}
Se eseguiamo il programma col debugger (azione Debug) possiamo anche vedere il valore della variabile
counter
in qualsiasi momento (bloccando l’esecuzione fissando un break-point). In realtà il debugger di IDEA fornisce anche una funzionalità (nella Memoryview del debugger) che permette direttamente di contare gli oggetti presenti in memoria appartenenti a una determinata classe.
4. Quali Responsabilità e Collaborazioni gestisce Cell?
Un oggetto
Cell
ha la responsabilità di gestire le informazioni riguardo un elemento del piano di gioco (gestito daMinefield
), che può essere minato. L’elemento inoltre può essere nascosto o visibile al giocatore. Quando è nascosto, il giocatore ha la possibilità di aggiungere o togliere una “marcatura”. Collaborazioni: i tipi base e un’enumerazione privataCellInteractionStatus
.Ragionare su quali responsabilità e collaborazioni è un buon modo di comprendere (e progettare) sistemi a oggetti che non si prestano ad analisi (e sintesi) gerarchiche come invece succede nei programmi strettamente procedurali
Da http://c2.com/doc/oopsla89/paper.html, l’articolo che presenta il cosiddetto ap-proccio Class, Responsibility, and Collaboration (CRC) per progettare sistemi a oggetti:
The class name of an object creates a vocabulary for discussing a design.
[. . . ]
Responsibilities identify problems to be solved. The solutions will exist in many versions and refinements. A responsibility serves as a handle for discussing potential solutions. The responsibilities of an object are expressed by a handful of short verb phrases, each containing an active verb. The more that can be expressed by these phrases, the more powerful and concise the design.
[. . . ]
One of the distinguishing features of object design is that no object is an island.
All objects stand in relationship to others, on whom they rely for services and control. The last dimension we use in characterizing object designs is the collaborators of an object. We name as collaborators objects which will send or be sent messages in the course of satisfying responsibilities. Collaboration is not necessarily a symmetric relation.
5. Quali Responsabilità e Collaborazioni gestisce Position?
Un oggetto
Position
ha la responsabilità di gestire una posizione (di un oggettoCell
) in una griglia rettangolare.Collaborazioni: i tipi base. Si tratta di una classe facilmente riutilizzabile in contesti diversi: sostanzialmente ovunque serva una coppia di coordinate positive.
6. Quali Responsabilità e Collaborazioni gestisce Minefield?
Un oggetto
Minefield
ha la responsabilità di gestire il piano di gioco. Per farlo collabora conCell
ePosition
. Il piano di gioco è una matrice di celle (alcune delle quali minate), ciascuna associata a una posizione.Vale la pena di notare che la parte “grafica” dell’applicazione (realizzata sfruttando il framework JavaFX) è separata da quella della logica del gioco (incapsulata nelle tre classi
Cell
,Position
eMinefield
).1.1 Campo minato
7. Premendo il tasto r si ricominciL’interazione grafica tramite la Graphical User Interface (GUI) è responsabilità della classe
Minesweeper
(in collaborazione con le classi della libreria JavaFX). Sarà quindi in questa classe che cercheremo un metodo incaricato di gestire gli eventi di interazione.Tipicamente si tratta di metodi chiamati dai framework grafici come JavaFX: il pro-grammatore del metodo si aspetta che il metodo venga chiamato al momento giusto (cioè quando l’utente preme un tasto o agisce col mouse, ecc.). Per questo motivo questi me-todi hanno generalmente nomi convenzionali o usano altre tecniche (come le annotazioni) per risultare identificabili dal framework chiamante. Si tratta del cosiddetto “Hollywood principle”, nome scherzoso che deriva da uno schema ricorrente nelle audizioni fatte a Hol-lywood, che generalmente terminano con la raccomandazione di non chiamare per sapere come è andata: «Non si preoccupi, la chiameremo noi. . . ».
In effetti, nel metodo
createScene()
troviamo una chiamataroot.setOnKeyPressed
: per chiarire i dettagli è necessario consultare la documentazione di JavaFX. Ma il nome del messaggiosetOnKeyPressed
(inviato a un oggetto di classeBorderPane
) ci suggerisce l’esistenza di un metodo per associare l’esecuzione di codice specifico alla pressione di un tasto.root.setOnKeyPressed(new EventHandler<KeyEvent>() { @Override
public void handle(final KeyEvent pEvent) { if (pEvent.getCode() == KeyCode.ENTER)
{ newGame();
refresh();
}
pEvent.consume();
}); }
Si noti che la chiamata crea “al volo” un oggetto di classe
EventHandler<KeyEvent>
per la quale si fornisce anche (di nuovo “al volo”, cioè senza passare per una definizione indipendente, con un nome e la possibilità di utilizzarla altrove) il metodo
handle
. Tutto ciò è necessario perché in Java ogni procedura deve far parte di una classe. A partire da Java 8, però, è possibile definire procedure anonime, cioè senza nome (quindi non riu-tilizzabili), dette espressioni lambda, secondo una consolidata tradizione informatica2. Con un’espressione lambda diventerebbe (si può ottenere automaticamente l’espressione lambda con IDEA Replace with lambda):root.setOnKeyPressed(pEvent -> {
if (pEvent.getCode() == KeyCode.ENTER) {
newGame();
refresh();
}pEvent.consume();
});
2Il Lambda-calcolo è lo studio di un modello di computabilità in cui le operazioni primitive fondamentali sono l’astrazione funzionale e l’applicazione di un’astrazione a valori concreti.
Questa scrittura è in effetti preferibile in molti casi (ed è usata nella classe
Minesweeper
poco più avanti, nella chiamata dibutton.setOnMouseClicked
), perché permette di concentrarci sul codice dello handler, l’unico metodo dell’oggetto creato precedentemente (si noti che se l’oggetto da creare al volo avesse bisogno di due metodi, l’espressione lambda non andrebbe più bene, perché non avremmo modo di distinguere con nomi diversi; qui in-vece ce n’era uno solohandle
, quindi quello anonimo fornirà una riscrittura (overriding) dihandle
, come prima risultava esplicitamente dall’annotazione@Override). Si noti anche chepEvent
è un nome arbitrario, che risulta legato (bound) solo nel contesto dell’espres-sione lambda (tutti gli altri simboli, comenewGame
, si dicono liberi, perché dipendono da definizioni esterne alla lambda): del tutto equivalentemente potremmo scrivere:root.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
newGame();
refresh();
}event.consume();
});
Per far sì che il gioco ricominci anche alla pressione del tasto r , aggiungiamo una clausola oralla condizione di selezione.
root.setOnKeyPressed(event -> {
if( event.getCode() == KeyCode.ENTER
|| event.getCode() == KeyCode.R) {
newGame();
refresh();
}
event.consume();
});
8. Cliccando con il tasto destro su una cella la si marchi in giallo
Il codice eseguito quando si clicca con il tasto destro (
MouseButton.SECONDARY
) è im-postato dalla chiamata dibutton.setOnMouseClicked
.button.setOnMouseClicked(e -> {
if( e.getButton() == MouseButton.SECONDARY ) { aMinefield.toggleMark(pPosition);
}else
{ aMinefield.reveal(pPosition);
}refresh();
});
L’effetto è quello di cambiare lo stato della marcatura della cella su cui si è cliccato (che si trova in
pPosition
nella matrice del campo minato). Chi definisce come rappresentare le celle marcate? Poco più sopra:1.1 Campo minato
if (aMinefield.isMarked(pPosition)) {
button.setText("!");
}
Questo è il punto in cui aggiungere le istruzioni necessarie ad avere uno sfondo giallo. Lo stile delle celle nascoste è definito all’inizio:
private static final String TILE_STYLE_HIDDEN =
"-fx-background-radius: 0; -fx-pref-width: 22px; -fx-pref-height: 22px;" +
"-fx-focus-color: transparent; -fx-faint-focus-color: transparent;
-fx-font-size: 12; " +
⤶
⤷
"-fx-text-fill: red; -fx-font-weight: bold;";
Possiamo definire un nuovo stile con lo sfondo giallo:
private static final String TILE_STYLE_MARKED =
"-fx-background-radius: 0; -fx-pref-width: 22px; -fx-pref-height: 22px;" +
"-fx-focus-color: transparent; -fx-faint-focus-color: transparent;
-fx-font-size: 12; " +
⤶
⤷
"-fx-text-fill: red; -fx-font-weight: bold; -fx-background-color: yellow;";
E usarlo per le celle marcate:
if( aMinefield.isMarked(pPosition)) { button.setText("!");
button.setStyle(TILE_STYLE_MARKED);
}
Questo funziona, ma abbiamo una brutta duplicazione negli stili. Meglio fattorizzare la parte comune.
private static final String TILE_STYLE_MARKED = TILE_STYLE_HIDDEN +
"-fx-background-color: yellow;";
⤶
⤷
Possiamo anche fattorizzare tutte le parti comuni delle stringhe di stile: se un giorno volessimo cambiare le dimensioni delle celle avremmo un solo punto da cambiare.
private static final String TILE_STYLE_COMMON = "-fx-pref-width: 22px;
-fx-pref-height: 22px;";
⤶
⤷
private static final String TILE_STYLE_HIDDEN = TILE_STYLE_COMMON + "-fx-background-radius: 0;" +
"-fx-focus-color: transparent; -fx-faint-focus-color: transparent;
-fx-font-size: 12; " +
⤶
⤷
"-fx-text-fill: red; -fx-font-weight: bold;";
private static final String TILE_STYLE_MARKED = TILE_STYLE_HIDDEN +
"-fx-background-color: yellow;";
⤶
⤷
private static final String TILE_STYLE_REVEALED =
TILE_STYLE_COMMON + "-fx-border-width: 0; -fx-border-color: black;" +
"-fx-background-color: lightgrey;";
9. Schiacciando s si scoprano tutte le celle
Sappiamo già dove si gestisce la pressione dei tasti. Aggiungeremo quindi la gestione di
s
.root.setOnKeyPressed(event -> {
if( event.getCode() == KeyCode.ENTER
|| event.getCode() == KeyCode.R) {
newGame();
refresh();
}else if (event.getCode() == KeyCode.S) { // TODO
}event.consume();
});
L’operazione da compiere è scoprire tutte le celle. La classe
Minefield
ha un metodorevealAll
che farebbe al caso nostro: sfortunatamente èprivatequindi non è possibi-le accedervi dall’oggettoaMinefield
con cui collaboraMinesweeper
. Rendere public il metodo non sarebbe una buona soluzione: infrangerebbe l’incapsulamento pensato dal progettista diMinefield
, il quale, una volta che il metodo fosse reso pubblico, avreb-be la responsabilità di gestirlo in modo da non alterare il contratto con tutti i “clienti”della classe. Meglio attenersi ai metodi già esplicitamente pubblici, che sono quelli che, dichiaratamente, servono per chiedere agli oggetti
Minefield
di portare a compimento le responsabilità loro affidate. CongetAllPositions
si possono ottenere tutte lePosit
⌋ion
del campo minato e conreveal
si può scoprire la cella in una dataPosition
. Alla fine occorre anche rinfrescare la grafica.else if (event.getCode() == KeyCode.S) {
for (Position p : aMinefield.getAllPositions()) { aMinefield.reveal(p);
}
refresh();
}
La sintassifor (Position p :
aMinefield.getAllPositions())
(“for each”) è re-sa possibile dal fatto che il metodogetAllPositions
ritorna unIterable<Position>
da cui si può ottenere un
Iterator<Position>
che è in grado di rispondere ai messaggihasNext()
enext()
usati automaticamente nelfor.Visto che
refresh
va fatto anche nel caso di Enter/ r , meglio evitare di ripeterlo.root.setOnKeyPressed(event -> {
if( event.getCode() == KeyCode.ENTER
|| event.getCode() == KeyCode.R) {
newGame();
}
else if (event.getCode() == KeyCode.S) {
for (Position p : aMinefield.getAllPositions()) { aMinefield.reveal(p);
} }refresh();
event.consume();
});
1.1 Campo minato
Ci sono solo due casi, ma unoswitchrende il codice più leggibile e più facile da modificare per aggiungere il trattamento di altri tasti (ma attenzione a non dimenticare ibreak!).root.setOnKeyPressed(event -> { switch (event.getCode()) { case ENTER:
case R:
newGame();
break;
case S:
for (Position p : aMinefield.getAllPositions()) { aMinefield.reveal(p);
}break;
}refresh();
event.consume();
});
10. Schiacciando h si ottenga un suggerimento
In questo caso le cose da fare sono un po’ più complicate, meglio usare un metodo apposito per aumentare l’incapsulamento.
root.setOnKeyPressed(event -> { switch (event.getCode()) { case ENTER:
case R:
newGame();
break;
case S:
for (Position p : aMinefield.getAllPositions()) { aMinefield.reveal(p);
}break;
case H:
showHint();
break;
}refresh();
event.consume();
});
// ...
private void showHint() { // TODO
}
Una strategia facile per ottenere un suggerimento (cioè la posizione di una cella certamen-te minata secondo le informazioni disponibili al giocatore), potrebbe essere questa (per suggerimenti più elaborati si vedahttp://www.minesweeper.info/wiki/Strategy):
a) prendere in esame tutte le informazioni disponibili, analizzando tutte le celle scoperte;
b) per ogni cella scoperta, prendere in esame tutte le celle vicine ancora nascoste;
c) se il numero di celle vicine nascoste è uguale al numero di mine segnalate dalla cella scoperta, allora tutte quelle nascoste sono mine sicure.
Per implementare questa strategia sarebbe comodo poter ottenere direttamente tutte le celle scoperte. Aggiungiamo quindi un metodo a
Minefield
.private List<Position> getAllRevealed() { List<Position> revealed = new ArrayList<>();
for (Position position : getAllPositions()) { if (isRevealed(position)){
revealed.add(position);
} }
return revealed;
}
Sarebbe utile anche un metodo per ottenere le vicine nascoste.
private List<Position> getHiddenNeighbours(Position p) { List<Position> hidden = new ArrayList<>();
for (Position neighbour : getNeighbours(p)) { if (!isRevealed(neighbour)){
hidden.add(neighbour);
}
}return hidden;
}
Il numero di mine segnalate da una cella scoperta è già calcolabile con il metodo
getNu
⌋mberOfMinedNeighbours
. Quindi ora possiamo mettere tutto insieme:/*** @return null if no simple hints are available,
* otherwise a Position hiding a mine.
public*/ Position getHint(){
for (Position p : getAllRevealed()) { int mined = getNumberOfMinedNeighbours(p);
if (mined > 0) {
List<Position> hidden = getHiddenNeighbours(p);
if (hidden.size() == mined){
for (Position h : hidden) { if (!isMarked(h)){
return h;
} } } } }
return null;
}
E a questo punto usare
getHint
inMinesweeper
1.1 Campo minato
private void showHint() {
Position hint = aMinefield.getHint();
if (hint != null){
aMinefield.toggleMark(hint);
} }
11. Aggiungere a Minefieldla gestione dello spazio dei suoi stati possibili
Al momento gli stati possibili sono gli elementi dell’enumerazione
MinefieldStatus
. Unaenumnon è altro che una classe (cioè un insieme di oggetti) con un numero di istan-ze/oggetti predefinito, fissato e costante. In altre paroleMinefieldStatus
rappresenta l’insieme dei tre oggetti immutabiliMinefieldStatus.NOT_CLEARED
,MinefieldStat
⌋us.CLEARED
,MinefieldStatus.EXPLODED
. La classe mette a disposizione anche alcuni metodi predefiniti: per esempioMinefieldStatus.NOT_CLEARED.ordinal()
restitui-sce il numero d’ordine in cui appare la costante nell’enumerazione (zero-based, quindi 0 nell’esempio). E naturalmente si possono definire nuovi metodi, se ce ne fosse bisogno.Per attribuire a
Minefield
la responsabilità di gestireMinefieldStatus
(che attual-mente è una classe pubblica e accessibile senza la mediazione diMinefield
) possiamo spostarla nell’ambito dell’incapsulamento diMinefield
, come inner class (come già suc-cede per l’enumerazioneCellInteractionStatus
interna aCell
). L’operazione può anche essere fatta automaticamente da IDEA (con il refactoring Move Class). L’enumdeve rimanerepublic(o package protected) però, perché è utilizzata come valore di ritorno di metodi pubblici. Per usare le costanti fuori daMinefield
occorre usare il nome completo che “attraversa” la classeMinefield
, p.es.Minefield.MinefieldStatus.CLEARED
. 12. Associare la rappresentazione della cella allo stato della cella stessaOgni oggetto in Java ha un metodo
toString
con cui è possibile ottenere una rappresen-tazione dell’oggetto comeString
: è il metodo usato quando per esempio si stampa con unaSystem.out.println(oggetto)
. Possiamo usare questo metodo per ottenere unaOgni oggetto in Java ha un metodo