Opzioni per la costruzione di un oggetto che non è inizializzato logicamente fino a quando non vengono impostati tutti i campi

2

Al momento sto lavorando a un gioco di Pokemon e sto incontrando alcuni problemi di progettazione. L'esempio più semplice è il seguente:

Ogni Species di Pokemon ha diversi tratti che sono richiesti prima che sia inizializzato logicamente. Questi includono, ma non sono limitati a:

  • id : una rappresentazione inglese di name (in genere solo il nome in lettere minuscole). Esempio: charizard
  • name : il nome localizzato di Species . Esempio: Charizard in inglese, Rizardon in giapponese e Dracaufeu in francese.
  • primaryType : l'elemento principale di questo Species . Esempio: il tipo principale di Charizard è Fire perché è un drago sputafuoco.
  • secondaryType : (facoltativo) ogni Species può anche avere un tipo secondario. Esempio: il tipo secondario di Charizard è Flying , ma la pre-evoluzione non ha ali, quindi non ha secondaryType . null va bene qui.
  • baseStats : le abilità di combattimento di base di tutti i membri di un Species
  • evPointYield : il tipo di esperienza acquisita dopo aver sconfitto un membro di Species

Per brevità, mi fermo qui. Ci sono almeno altri 4 campi che sono richiesti, ma non sono importanti per l'esempio.

In questo esempio, poiché tutti i campi sono obbligatori prima che venga creato un Species (leggi: nessun Species può esistere senza tutti questi campi), un costruttore avrà un aspetto simile a

public Species(final String id, final String name, final Type primaryType,
    final Type secondaryType, final StatSet baseStats, final StatSet evPointYield) {
    // argument checking omitted for brevity
    this.id = id;
    this.name = name;
    this.primaryType = primaryType;
    this.secondaryType = secondaryType;
    this.baseStats = baseStats;
    this.evPointYield = evPointYield;
}

Non c'è modo di raggrupparli logicamente in oggetti diversi per ridurre la quantità di parametri passati nel costruttore.

Il mio primo tentativo di soluzione è stato creare un Builder

public class Species {
    public static final class Builder {

        // setters for all 6 fields

        public Pokemon build() {
            return new Pokemon(this);
        }
    }

    public Species(final Species.Builder builder) {
        // ensure builder is not null
        // set fields from builder
    }
}

Ma questo dovrebbe essere fatto da tutti gli utenti, mentre non chiama nessun metodo come

Species species = new Species.Builder().build();

Quindi, ho modificato il metodo di build per includere un metodo di convalida

public Species build() {
    if (!stateIsValid()) {
        // actual message is more descriptive, including which specific fields
        // were missed
        throw new IllegalStateException("not all fields were initialized before building");
    }
    return new Species(this);
}

private boolean stateIsValid() {
    return
        idIsValid() &&
        nameIsValid() &&
        primaryTypeIsValid() &&
        secondaryTypeIsValid() &&
        baseStatsAreValid() &&
        evPointYieldIsValid();
}

private boolean idIsValid() {
    // return it was set to a valid value
}

// other state checkers

Dovrei anche notare che sto usando Species come più di un tipo di dati. Significa che i campi saranno caricati da da qualche parte e ci sono pochissime operazioni logiche che sarebbero necessarie in questa classe. Questa classe (sh / w) non può essere utilizzata per uno specifico Species come:

public class Charizard extends Species {

}

ma piuttosto per aggiungere funzionalità aggiuntive a tutti Species

public class ComparableSpecies extends Species implements Comparable {
    // ...
}

Questo accade altre volte nel codice, dove un oggetto non è costruito logicamente finché tutti i campi non sono impostati. Se ricordo male, un costruttore dovrebbe contenere tutte le informazioni necessarie per completare logicamente la costruzione di un oggetto, ma cosa succede se questo comporta un sacco di dati (come in questo esempio)?

Quindi alla domanda: un Builder sarebbe l'approccio giusto per questo problema? C'è un altro modello che sarebbe più adatto per questo tipo di costruzione? O ho appena finito il Builder tana del coniglio così lontano che non riesco a vedere cosa c'è di giusto in faccia?

(Nota: non preoccuparti / concentrati su / su come le modifiche alle classi avranno un impatto su tutti i consumatori a valle della mia API. Non ci sono ancora consumatori, dato che tutto il codice è ancora privato, e nemmeno pronto per alpha test ancora)

Qualsiasi feedback sarebbe apprezzato.

    
posta Zymus 15.01.2015 - 08:53
fonte

4 risposte

6

Il pattern Builder è molto utile quando il prodotto che stai costruendo consiste di più parti discrete, dove le informazioni esterne specificano quali parti, o se il prodotto ha molte proprietà opzionali .

Nel tuo caso, hai una lunga lista di proprietà richieste . Qui il modello Builder ti dà solo l'aspetto di un costruttore meno complesso a costo di non essere in grado di rilevare in fase di compilazione che una proprietà è stata persa.
Stai essenzialmente scambiando codice come questo

type1 property1 = ...
type2 property2 = ...
type3 property3 = ...
.
.
.
typeN propertyN = ...
Species x = new Species(property1, property2, property3, ..., propertyN);

per

Builder builder;
builder.setProperty1(...);
builder.setProperty3(...);
.
.
.
builder.setPropertyN(...);
Species x = builder.build();

La costruzione delle specie sembra deliziosamente semplice, ma quanto ci vorrà per capire perché questo codice non funziona?

So che ci sono linee guida che i metodi non dovrebbero prendere più di 3 (o 4) argomenti. Tuttavia, c'è un motivo per cui tutte le lingue nell'uso pratico non lo impongono come un limite difficile: a volte hai solo bisogno di più argomenti. Se anche il più aggressivo raggruppamento di argomenti in oggetti parametro non può farti abbassare di 6 o 7 parametri, ne hai solo bisogno.

    
risposta data 15.01.2015 - 12:24
fonte
3

There is no way to logically group these into different objects to reduce the amount of parameters passed into the constructor.

Penso che questa sia l'essenza del tuo problema.

Innanzitutto, il modello Builder viene normalmente utilizzato in questa situazione come best practice. Tuttavia, per me personalmente, non mi piace e addirittura lo considero un modello anti. Anche tu in qualche modo, altrimenti non lo chiederesti qui, giusto? ;)

Quindi, mi sento come se alcune proprietà fossero ancora raggruppate. Tuttavia, un buon raggruppamento logico è una delle decisioni di progettazione più difficili. Quindi partiamo dalle informazioni fornite (ho rimosso la finale per facilitare la lettura):

String id,
String name,
Type  primaryType,
Type secondaryType,
StatSet baseStats,
StatSet evPointYield

Ecco che immediatamente mi vengono in mente le coppie di caratteri. Mentre id e name sembrano essere raggruppati, servono a scopi diversi (identificativo e visualizzazione). Quindi non raggruppare qui, hai ragione.

I Type s e StatSet s hanno comunque lo stesso scopo: essi e solo loro influenzano il modo in cui le specie si comportano nelle interazioni di gioco. Raggrupperei quindi tutti questi elementi in un oggetto Behaviour generale. Le sue proprietà possono essere ulteriormente raggruppate. Type s potrebbe essere raggruppato in un oggetto TypeStats . Il StatSet s sembra essere usato solo in combattimento, quindi sarebbe il combatStats . I nomi non sono scelti al meglio qui, lo ammetto.

Questo ha alcuni vantaggi. Ora puoi condividere un comportamento comune tra specie diverse se lo desideri. Puoi assicurarti che quando crei TypeStats non ce ne sia nessuno in cui sia il primo che il secondo tipo sono uguali. Questa dovrebbe essere una preoccupazione del TypeStats non del Species secondo me. È anche più facile spiegare la classe:

Species contain an Id by which they are identified unique. When playing, they are displayed to the user by their names, depending on the country. Each Species also has their own behaviour in this game. This behaviour consists of how it behaves in combats and what you can do with it when not in combat.

Tuttavia, la mia risposta potrebbe essere incompleta o anche parzialmente errata, a seconda del significato delle proprietà che hai menzionato e di quelle che non hai menzionato.

Un ultimo punto: non rendere il tuo secondo tipo null . Questo non è davvero buono. Se usi Java 8, vai alla classe Optional . Se usi Java al di sotto di 7, dai un'occhiata alla libreria googles guava che ti fornisce una classe di questo tipo.

    
risposta data 15.01.2015 - 09:26
fonte
3

In momenti come questo cerco di fare un passo indietro, dimenticare l'implementazione e concentrarmi sui requisiti.

In sostanza, si desidera:
- Definire entità discrete utilizzando elementi di dati primitivi
- Convalidare le entità utilizzando regole specifiche del dominio
- Costruisci un modello di dominio da entità valide
- Esegui calcoli utili e interessanti con il tuo modello di dominio

Se sei in pre-alpha, il pattern Builder funzionerà perfettamente per iniziare. Probabilmente puoi saltare del tutto Builder e basta usare un'interfaccia per nascondere i tuoi setter. In ogni caso, completa le funzionalità più preziose prima di preoccuparti dei dettagli di implementazione di basso livello. Le tue funzionalità guideranno i requisiti del tuo modello di dominio.

Una volta individuati i requisiti del tuo modello di dominio, sarai in una posizione migliore per decidere se Builder è una soluzione accettabile (non necessariamente la migliore). Potresti scoprire che i campi che pensavi fossero necessari non sono affatto necessari. Ad esempio, un Pokemon deve possedere la sua resa in punti EV? Potresti invece mappare un Pokemon alla sua resa in punti EV in un modello separato? Con meno campi obbligatori, Builder può sembrare una soluzione migliore. I concetti utili qui includono la Legge di Demetra e principio di separazione delle interfacce .

Se il modello di Builder non soddisfa ancora i tuoi requisiti, prova a guardare l'associazione dei dati con Architettura Java per il binding XML (JAXB ). È uno standard che definisce come creare un'entità utilizzando i dati memorizzati come XML e include anche la convalida. Oracle ha un tutorial di base per iniziare.

Se il tuo XML ha questo aspetto:

<pokemon>
    <id>Charizard</id>
    <!-- Other elements -->
</pokemon>

Il tuo Pokemon potrebbe apparire come questo:

@XmlRootElement(name="pokemon")
public class Pokemon {

    @XmlElement(required=true)
    private String id;
}

Normalmente cerco di evitare XML, e JAXB è decisamente eccessivo per i piccoli progetti. Ma per i grandi insiemi di dati leggibili dall'uomo con regole di validazione ben definite che si integrano con Java, funziona piuttosto bene.

    
risposta data 15.01.2015 - 18:25
fonte
0

Un'opzione consiste nell'utilizzare un oggetto parametro .

Prendi i parametri richiesti da ogni istanza e raggruppali come una classe separata.

Vedi anche questa domanda

    
risposta data 15.01.2015 - 16:45
fonte

Leggi altre domande sui tag