Troppi parametri nei metodi Java, parte 6: restituzioni dei metodi

Nell'attuale serie di post che sto scrivendo sulla riduzione del numero di parametri richiesti per chiamare metodi e costruttori Java, mi sono concentrato finora su approcci che influenzano direttamente i parametri stessi (tipi personalizzati, oggetti parametri, pattern builder, sovraccarico di metodi e denominazione del metodo). Detto questo, potrebbe sembrare sorprendente per me dedicare un post di questa serie a come i metodi Java forniscono i valori di ritorno. Tuttavia, i valori di ritorno dei metodi possono influire sui parametri accettati dai metodi quando gli sviluppatori scelgono di fornire valori di "ritorno" impostando o modificando i parametri forniti piuttosto che o in aggiunta ai meccanismi di restituzione del metodo più tradizionali.

I "modi tradizionali" in cui un metodo non costruttore restituisce un valore possono essere specificati entrambi nella firma del metodo. L'approccio più comunemente riconosciuto per la restituzione di un valore da un metodo Java è tramite il suo tipo di ritorno dichiarato. Questo spesso funziona bene, ma una delle frustrazioni che si verificano più comunemente è la possibilità di restituire un solo valore da un metodo Java.

Il meccanismo di gestione delle eccezioni di Java è anche un altro approccio per mantenere un "risultato" di un metodo per i chiamanti. Le eccezioni verificate, in particolare, vengono annunciate al chiamante tramite la clausola throws. In effetti, Jim Waldo, nel suo libro Java: The Good Parts, afferma che è più facile capire le eccezioni Java quando si pensa alle eccezioni Java come un altro tipo di ritorno del metodo limitato ad essere un tipo Throwable.

Sebbene il tipo di ritorno del metodo e le eccezioni generate siano gli approcci principali per i metodi per restituire informazioni ai chiamanti, a volte si è tentati di restituire dati o stati tramite i parametri passati nel metodo. Quando un metodo deve restituire più di un'informazione, i risultati a valore singolo dei metodi Java possono sembrare limitanti. Sebbene le eccezioni forniscano un altro modo per comunicare con il chiamante, sembra quasi universalmente accettato che le eccezioni debbano essere utilizzate solo per segnalare situazioni eccezionali e non per segnalare dati "normali" o utilizzate nel flusso di controllo. Dato che un solo oggetto o primitiva può essere restituito da un metodo e che le eccezioni consentono solo la restituzione di un fileThrowable e dovrebbe essere utilizzato solo per segnalare situazioni eccezionali, diventa sempre più interessante per lo sviluppatore Java dirottare i parametri come percorso alternativo per restituire i dati al chiamante.

La tecnica che uno sviluppatore può utilizzare per applicare i parametri del metodo come vettori per i dati di ritorno consiste nell'accettare parametri che sono modificabili e nel mutare lo stato degli oggetti passati. Questi oggetti mutabili possono subire modifiche ai loro contenuti dal metodo e quindi il chiamante può accedere all'oggetto fornito per determinare le sue nuove impostazioni di stato che sono state applicate dal metodo chiamato. Sebbene ciò possa essere fatto con qualsiasi oggetto modificabile, le raccolte sembrano particolarmente attraenti per lo sviluppatore che cerca di restituire valori al chiamante tramite parametri.

Ci sono alcuni svantaggi nel restituire lo stato al chiamato tramite i parametri forniti. Questo approccio spesso viola il principio del minimo stupore poiché la maggior parte degli sviluppatori Java probabilmente si aspetta che i parametri siano INcoming piuttosto che OUTgoing (e Java non fornisce alcun supporto di codice per specificare la differenza). Bob Martin lo mette in questo modo nel suo libro Clean Code, "In generale, gli argomenti di output dovrebbero essere evitati". Un altro svantaggio dell'utilizzo degli argomenti come mezzo per un metodo per fornire lo stato o l'output al chiamante è che questo si aggiunge alla confusione di argomenti passati a un metodo. Con questo in mente, il resto di questo post si concentra sulle alternative alla restituzione di più valori tramite parametri passati.

Sebbene i metodi Java possano restituire solo un singolo oggetto o una primitiva, questa non è davvero una grande limitazione se si considera che un oggetto può essere praticamente qualsiasi cosa vogliamo che sia. Ci sono diversi approcci che ho visto ma che non consiglio. Uno di questi sta restituendo un array o una raccolta di istanze di Object con ciascunoObjectessendo una "cosa" disparata e distinta e spesso non correlata. Ad esempio, il metodo potrebbe restituire tre valori come tre elementi di una matrice o di una raccolta. Una variazione di questo approccio consiste nell'usare una coppia di tupla o una tupla di dimensioni n per restituire più valori associati. Un'altra variante di questo approccio consiste nel restituire una Java Map che mappa chiavi arbitrarie al valore associato. Come con le altre soluzioni, questo approccio impone al client un onere eccessivo di sapere quali sono quelle chiavi e di accedere ai valori della mappa tramite tali chiavi.

L'elenco di codice successivo contiene molti di questi approcci meno interessanti per la restituzione di più valori senza dirottare i parametri del metodo per restituire più valori.

Restituzione di più valori tramite strutture dati generiche

 // =============================================================== // NOTE: These examples are intended solely to illustrate a point // and are NOT recommended for production code. // =============================================================== /** * Provide movie information. * * @return Movie information in form of an array where details are mapped to * elements with the following indexes in the array: * 0 : Movie Title * 1 : Year Released * 2 : Director * 3 : Rating */ public Object[] getMovieInformation() { final Object[] movieDetails = {"World War Z", 2013, "Marc Forster", "PG-13"}; return movieDetails; } /** * Provide movie information. * * @return Movie information in form of a List where details are provided * in this order: Movie Title, Year Released, Director, Rating. */ public List getMovieDetails() { return Arrays.asList("Ender's Game", 2013, "Gavin Hood", "PG-13"); } /** * Provide movie information. * * @return Movie information in Map form. Characteristics of the movie can * be acquired by looking in the map for these key elements: "Title", "Year", * "Director", and "Rating"./ */ public Map getMovieDetailsMap() { final HashMap map = new HashMap(); map.put("Title", "Despicable Me 2"); map.put("Year", 2013); map.put("Director", "Pierre Coffin and Chris Renaud"); map.put("Rating", "PG"); return map; } 

Gli approcci mostrati sopra soddisfano l'intento di non ritrasmettere i dati al chiamante tramite i parametri dei metodi invocati, ma il chiamante deve ancora conoscere i dettagli intimi della struttura dei dati restituiti. È bello ridurre il numero di parametri al metodo e non violare il principio della minima sorpresa, ma non è così bello richiedere al cliente di conoscere le complessità di una complessa struttura di dati.

Preferisco scrivere oggetti personalizzati per i miei resi quando ho bisogno di restituire più di un valore. È un po 'più di lavoro rispetto all'utilizzo di una struttura di array, raccolta o tupla, ma la quantità molto ridotta di lavoro extra (in genere pochi minuti con i moderni IDE Java) ripaga con leggibilità e fluidità che non è disponibile con questi approcci più generici. Invece di dover spiegare con Javadoc o richiedere agli utenti del mio codice di leggere attentamente il mio codice per sapere quali parametri sono forniti in quale ordine nella matrice o nella raccolta o quale valore è quale nella tupla, i miei oggetti di ritorno personalizzati possono avere metodi definiti su quelli che dicono al cliente esattamente cosa stanno fornendo.

I frammenti di codice che seguono illustrano una semplice Movieclasse generata in gran parte da NetBeans che può essere utilizzata come tipo restituito insieme al codice che potrebbe restituire un'istanza di quella classe piuttosto che una struttura dati più generica e meno leggibile.

Movie.java

package dustin.examples; import java.util.Objects; /** * Simple Movie class to demonstrate how easy it is to provide multiple values * in a single Java method return and provide readability to the client. * * @author Dustin */ public class Movie { private final String movieTitle; private final int yearReleased; private final String movieDirectorName; private final String movieRating; public Movie(String movieTitle, int yearReleased, String movieDirectorName, String movieRating) { this.movieTitle = movieTitle; this.yearReleased = yearReleased; this.movieDirectorName = movieDirectorName; this.movieRating = movieRating; } public String getMovieTitle() { return movieTitle; } public int getYearReleased() { return yearReleased; } public String getMovieDirectorName() { return movieDirectorName; } public String getMovieRating() { return movieRating; } @Override public int hashCode() { int hash = 3; hash = 89 * hash + Objects.hashCode(this.movieTitle); hash = 89 * hash + this.yearReleased; hash = 89 * hash + Objects.hashCode(this.movieDirectorName); hash = 89 * hash + Objects.hashCode(this.movieRating); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Movie other = (Movie) obj; if (!Objects.equals(this.movieTitle, other.movieTitle)) { return false; } if (this.yearReleased != other.yearReleased) { return false; } if (!Objects.equals(this.movieDirectorName, other.movieDirectorName)) { return false; } if (!Objects.equals(this.movieRating, other.movieRating)) { return false; } return true; } @Override public String toString() { return "Movie{" + "movieTitle=" + movieTitle + ", yearReleased=" + yearReleased + ", movieDirectorName=" + movieDirectorName + ", movieRating=" + movieRating + '}'; } } 

Restituzione di più dettagli in un singolo oggetto

 /** * Provide movie information. * * @return Movie information. */ public Movie getMovieInfo() { return new Movie("Oblivion", 2013, "Joseph Kosinski", "PG-13"); } 

La semplice scrittura del file Moviela lezione mi ha richiesto circa 5 minuti. Ho utilizzato la procedura guidata di creazione della classe NetBeans per selezionare il nome della classe e il pacchetto, quindi ho digitato i quattro attributi della classe. Da lì, ho semplicemente utilizzato il meccanismo "Inserisci codice" di NetBeans per inserire metodi di accesso "get" insieme ai metodi sovrascritti toString (), hashCode () ed equals (Object). Se non pensassi di averne bisogno, potrei mantenere la classe più semplice, ma è davvero facile crearla così com'è. Ora, ho un tipo di ritorno molto più utilizzabile e questo si riflette nel codice che usa la classe. Non ha bisogno di quasi tanti commenti Javadoc sul tipo restituito perché quel tipo parla da solo e pubblicizza il suo contenuto con i suoi metodi "get".Ritengo che il piccolo sforzo aggiuntivo per creare queste classi semplici per restituire più valori ripaghi con enormi dividendi rispetto ad alternative come la restituzione dello stato tramite parametri di metodo o l'utilizzo di strutture di dati di ritorno più generiche e più difficili da usare.

Non sorprende che un tipo personalizzato per contenere più valori da restituire a un chiamante sia una soluzione interessante. Dopotutto, questo è concettualmente molto simile ai concetti di cui ho scritto nel blog in precedenza relativi all'uso di tipi e oggetti parametri personalizzati per il passaggio di più parametri correlati piuttosto che trasmetterli tutti individualmente. Java è un linguaggio orientato agli oggetti e quindi mi sorprende quando non vedo oggetti usati più spesso nel codice Java per organizzare i parametri E restituire valori in un bel pacchetto.

Benefici e vantaggi

I vantaggi dell'utilizzo di oggetti parametro personalizzati per rappresentare e incapsulare più valori restituiti sono evidenti. I parametri del metodo possono rimanere parametri di "input" poiché tutte le informazioni di output (ad eccezione delle informazioni di errore comunicate tramite il meccanismo di eccezione) possono essere fornite nell'oggetto personalizzato restituito dal metodo. Si tratta di un approccio più pulito rispetto all'utilizzo di array generici, raccolte, mappe, tuple o altre strutture di dati generiche perché tutti questi approcci alternativi spostano lo sforzo di sviluppo su tutti i potenziali clienti.

Costi e svantaggi

Vedo pochissimi svantaggi nello scrivere tipi personalizzati con più valori da utilizzare come tipi restituiti dai metodi Java. Forse il costo dichiarato più spesso è il prezzo per scrivere e testare queste classi, ma tale costo è piuttosto basso perché queste classi tendono ad essere semplici e perché gli IDE moderni fanno la maggior parte del lavoro per noi. Poiché gli IDE lo fanno automaticamente, il codice è generalmente corretto. Le classi sono così semplici da essere facilmente leggibili dai revisori del codice e facili da testare.