Interfaccia separata per i metodi di mutazione


11

Ho lavorato sul refactoring del codice e penso di aver fatto il primo passo nella tana del coniglio. Sto scrivendo l'esempio in Java, ma suppongo che potrebbe essere agnostico.

Ho un'interfaccia Foodefinita come

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

E un'implementazione come

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

Ho anche un'interfaccia MutableFooche fornisce mutatori corrispondenti

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

MutableFooPotrebbero esistere un paio di implementazioni (non le ho ancora implementate). Uno di essi è

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

Il motivo per cui li ho divisi è perché è ugualmente probabile che ciascuno venga usato. Ciò significa che è altrettanto probabile che qualcuno che utilizza queste classi desidererà un'istanza immutabile, poiché è desiderabile che ne abbia una mutabile.

Il caso d'uso principale che ho è un'interfaccia chiamata StatSetche rappresenta alcuni dettagli di combattimento per un gioco (punti ferita, attacco, difesa). Tuttavia, le statistiche "efficaci", o le statistiche effettive, sono il risultato delle statistiche di base, che non possono mai essere modificate e delle statistiche allenate, che possono essere aumentate. Questi due sono collegati da

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

gliStat addestrati vengono aumentati dopo ogni battaglia come

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

ma non sono aumentati subito dopo la battaglia. L'uso di determinati oggetti, l'utilizzo di alcune tattiche, l'uso intelligente del campo di battaglia possono garantire un'esperienza diversa.

Ora per le mie domande:

  1. Esiste un nome per suddividere le interfacce tra accessori e mutatori in interfacce separate?
  2. Suddividendoli in questo modo è l'approccio "giusto" se è altrettanto probabile che vengano utilizzati o esiste invece un modello diverso e più accettato che dovrei usare (ad es. Foo foo = FooFactory.createImmutableFoo();Che potrebbe restituire DefaultFooo DefaultMutableFooma è nascosto perché createImmutableFooritorna Foo)?
  3. Ci sono aspetti negativi immediatamente prevedibili nell'uso di questo modello, salvo complicare la gerarchia dell'interfaccia?

Il motivo per cui ho iniziato a progettarlo in questo modo è perché sono dell'idea che tutti gli implementatori di un'interfaccia dovrebbero aderire all'interfaccia più semplice possibile e non fornire nulla di più. Aggiungendo setter all'interfaccia, ora le statistiche effettive possono essere modificate indipendentemente dalle sue parti.

Fare una nuova classe per il EffectiveStatSetnon ha molto senso in quanto non stiamo estendendo la funzionalità in alcun modo. Potremmo cambiare l'implementazione e creare un EffectiveStatSetcomposto di due diversi StatSets, ma ritengo che non sia la soluzione giusta;

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}


1
Penso che il problema sia che il tuo oggetto è mutevole anche se stai accedendo alla mia interfaccia "immutabile". Meglio avere un oggetto mutevole con Player.TrainStats ()
Ewan il

@gnat hai qualche riferimento a dove posso saperne di più? Sulla base della domanda modificata, non sono sicuro di come o dove potrei applicarlo.
Zimo

@gnat: i tuoi collegamenti (e i suoi collegamenti di secondo e terzo grado) sono molto utili, ma frasi accattivanti di saggezza sono di scarso aiuto. Attrae solo incomprensioni e disprezzo.
rwong,

Risposte:


6

A me sembra che tu abbia una soluzione alla ricerca di un problema.

Esiste un nome per suddividere le interfacce tra accessori e mutatori in interfacce separate?

Questo potrebbe essere un po 'stimolante, ma in realtà lo definirei "sovrastimando le cose" o "cose ​​eccessivamente complicate". Offrendo una variante mutevole e immutabile della stessa classe, offri due soluzioni funzionalmente equivalenti per lo stesso problema, che differiscono solo per aspetti non funzionali come comportamento prestazionale, API e sicurezza rispetto agli effetti collaterali. Immagino che sia perché temi di prendere una decisione quale preferire o perché cerchi di implementare la funzionalità "const" di C ++ in C #. Immagino che nel 99% dei casi non farà alcuna differenza se l'utente sceglie la variante mutabile o immutabile, può risolvere i suoi problemi con l'uno o l'altro. Quindi "la verosimiglianza di una classe da usare"

L'eccezione è quando si progetta un nuovo linguaggio di programmazione o un framework multiuso che verrà utilizzato da una decina di migliaia di programmatori o più. Quindi può davvero ridimensionarsi quando offri varianti immutabili e mutabili di tipi di dati generici. Ma questa è una situazione in cui avrai migliaia di diversi scenari di utilizzo - che probabilmente non è il problema che affronti, immagino?

Li sta dividendo in questo modo l'approccio "giusto" se è altrettanto probabile che vengano utilizzati o esiste un modello diverso e più accettato

Il "modello più accettato" si chiama KISS: mantienilo semplice e stupido. Prendi una decisione a favore o contro la mutabilità per la specifica classe / interfaccia nella tua libreria. Ad esempio, se il tuo "StatSet" ha una dozzina di attributi o più, e sono per lo più modificati singolarmente, preferirei la variante mutabile e non modificherei le statistiche di base dove non dovrebbero essere modificati. Per qualcosa come una Fooclasse con attributi X, Y, Z (un vettore tridimensionale), probabilmente preferirei la variante immutabile.

Ci sono aspetti negativi immediatamente prevedibili nell'uso di questo modello, salvo complicare la gerarchia dell'interfaccia?

I progetti complicati rendono il software più difficile da testare, più difficile da mantenere, più difficile da evolvere.


1
Penso che intendi "KISS - Keep It Simple, Stupid"
Epicblood

2

Esiste un nome per suddividere le interfacce tra accessori e mutatori in interfacce separate?

Potrebbe esserci un nome per questo se questa separazione è utile e fornisce un vantaggio che non vedo . Se i separatoni non forniscono un vantaggio, le altre due domande non hanno senso.

Puoi dirci un caso d'uso aziendale in cui le due interfacce separate ci forniscono vantaggi o questa domanda è un problema accademico (YAGNI)?

Mi viene in mente di fare acquisti con un carrello mutevole (puoi inserire più articoli) che può diventare un ordine in cui gli articoli non possono più essere cambiati dal cliente. Lo stato dell'ordine è ancora modificabile.

Le implementazioni che ho visto non separano le interfacce

  • non esiste un'interfaccia ReadOnlyList in java

La versione ReadOnly utilizza "WritableInterface" e genera un'eccezione se viene utilizzato un metodo di scrittura


Ho modificato l'OP per spiegare il caso d'uso principale.
Zimo

2
"non esiste un'interfaccia ReadOnlyList in java" - francamente, ci dovrebbe essere. Se hai un pezzo di codice che accetta un Elenco <T> come parametro, non puoi facilmente dire se funzionerà con un elenco non modificabile. Codice che restituisce elenchi, non c'è modo di sapere se è possibile modificarli in sicurezza. L'unica opzione è fare affidamento su documentazione potenzialmente incompleta o inesatta, o fare una copia difensiva per ogni evenienza. Un tipo di raccolta di sola lettura renderebbe questo molto più semplice.
Jules il

@Jules: infatti, il modo Java sembra essere tutto sopra: per assicurarsi che la documentazione sia completa e accurata, oltre a fare una copia difensiva per ogni evenienza. Sicuramente si adatta a progetti di impresa molto grandi.
rwong

1

La separazione tra un'interfaccia di raccolta mutabile e un'interfaccia di raccolta di sola lettura è un esempio del principio di segregazione dell'interfaccia. Non credo che ci siano nomi speciali dati ad ogni applicazione del principio.

Nota qui alcune parole: "contratto di sola lettura" e "raccolta".

Un contratto di sola lettura indica che la classe Aoffre alla classe Bun accesso di sola lettura, ma non implica che l'oggetto di raccolta sottostante sia effettivamente immutabile. Immutabile significa che non cambierà mai in futuro, indipendentemente da qualsiasi agente. Un contratto di sola lettura indica solo che al destinatario non è consentito modificarlo; qualcun altro (in particolare la classe A) è autorizzato a cambiarlo.

Per rendere un oggetto immutabile, deve essere veramente immutabile - deve negare i tentativi di modifica dei suoi dati indipendentemente dall'agente che lo richiede.

Molto probabilmente il modello viene osservato su oggetti che rappresentano una raccolta di dati: elenchi, sequenze, file (flussi), ecc.


La parola "immutabile" diventa di moda, ma il concetto non è nuovo. E ci sono molti modi per usare l'immutabilità, così come molti modi per ottenere un design migliore usando qualcos'altro (cioè i suoi concorrenti).


Ecco il mio approccio (non basato sull'immutabilità).

  1. Definire un DTO (oggetto di trasferimento dati), noto anche come tupla di valore.
    • Questo DTO conterrà tre campi: hitpoints, attack, defense.
    • Solo campi: pubblico, chiunque può scrivere, nessuna protezione.
    • Tuttavia, un DTO deve essere un oggetto usa e getta: se la classe Adeve passare un DTO alla classe B, ne fa una copia e passa invece la copia. Quindi, è Bpossibile utilizzare il DTO come piace a lui (scriverlo), senza influire sul DTO che si Amantiene.
  2. La grantExperiencefunzione da suddividere in due:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats prenderà gli input da due DTO, uno che rappresenta le statistiche del giocatore e un altro che rappresenta le statistiche nemiche, ed eseguirà i calcoli.
    • Per l'input, il chiamante dovrebbe scegliere tra statistiche di base, addestrate o efficaci, in base alle proprie esigenze.
    • Il risultato sarà un nuovo DTO dove ogni campo ( hitpoints, attack, defense) memorizza l'importo da incrementare per tale capacità.
    • Il nuovo DTO "importo da incrementare" non riguarda il limite superiore (limite massimo imposto) per tali valori.
  4. increaseStats è un metodo sul giocatore (non sul DTO) che richiede un DTO "importo da incrementare" e applica tale incremento al DTO di proprietà del giocatore e rappresenta il DTO addestrabile del giocatore.
    • Se esistono valori massimi applicabili per queste statistiche, vengono applicati qui.

Nel caso in cui calculateNewStatssi riscontri che non dipende da nessun altro giocatore o informazione nemica (oltre i valori nei due DTO di input), questo metodo può potenzialmente essere localizzato in qualsiasi parte del progetto.

Se calculateNewStatssi scopre che ha una dipendenza completa dal giocatore e dagli oggetti nemici (vale a dire, i giocatori futuri e gli oggetti nemici possono avere nuove proprietà e calculateNewStatsdevono essere aggiornati per consumare il maggior numero possibile delle loro nuove proprietà), quindi calculateNewStatsdevono accettare quei due oggetti, non semplicemente il DTO. Tuttavia, il suo risultato di calcolo sarà comunque l'incremento DTO, o qualsiasi altro semplice oggetto di trasferimento dati che trasporta le informazioni utilizzate per eseguire un incremento / aggiornamento.


1

C'è un grosso problema qui: solo perché una struttura di dati è immutabile non significa che non abbiamo bisogno di versioni modificate di essa . La vera versione immutabile di una struttura di dati non fornisce setX, setYe setZmetodi - hanno appena restituire una nuova struttura invece di modificare l'oggetto che li ha chiamato via.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

Quindi, come si può dare a una parte del sistema la possibilità di cambiarla limitando altre parti? Con un oggetto mutabile contenente un riferimento a un'istanza della struttura immutabile. Fondamentalmente, invece che la tua classe di giocatore sia in grado di mutare il suo oggetto stats mentre offre ad ogni altra classe una visione immutabile delle sue statistiche, le sue statistiche sono immutabili e il giocatore è mutabile. Invece di:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

Avresti:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

Vedi la differenza? L'oggetto stats è completamente immutabile, ma le attuali statistiche del giocatore sono un riferimento mutevole all'oggetto immutabile. Non è necessario creare due interfacce: è sufficiente rendere la struttura dei dati completamente immutabile e gestire un riferimento mutevole ad esso dove vi capita di utilizzarlo per memorizzare lo stato.

Ciò significa che altri oggetti nel tuo sistema non possono fare affidamento su un riferimento all'oggetto stats: l'oggetto stats che hanno non sarà lo stesso oggetto stat che ha il giocatore dopo che il giocatore ha aggiornato le proprie statistiche.

Questo ha comunque più senso, dal momento che concettualmente non stanno cambiando le statistiche, sono le statistiche attuali del giocatore . Non l'oggetto stats. Il riferimento all'oggetto stats dell'oggetto giocatore. Quindi, altre parti del sistema che dipendono da quel riferimento dovrebbero essere tutte esplicitamente referenziate player.currentStats()o alcune invece di ottenere l'oggetto delle statistiche sottostanti del giocatore, memorizzarlo da qualche parte e fare affidamento sull'aggiornamento attraverso mutazioni.


1

Wow ... questo mi riporta davvero indietro. Ho provato questa stessa idea alcune volte. Ero troppo testardo per arrendermi perché pensavo che ci sarebbe stato qualcosa di buono da imparare. Ho visto altri provare anche questo nel codice. La maggior parte delle implementazioni Ho visto chiamato le interfacce di sola lettura come la tua FooViewero ReadOnlyFooe l'interfaccia sola scrittura la FooEditoro WriteableFooo in Java penso di aver visto FooMutatoruna sola volta. Non sono sicuro che esista un vocabolario ufficiale o comune per fare le cose in questo modo.

Questo non ha mai fatto nulla di utile per me nei luoghi in cui l'ho provato. Lo eviterei del tutto. Vorrei fare come suggeriscono gli altri e fare un passo indietro e valutare se hai davvero bisogno di questa nozione nella tua idea più grande. Non sono sicuro che esista un modo giusto per farlo poiché non ho mai mantenuto alcun codice prodotto durante il tentativo. Ogni volta che mi sono ritirato dopo un notevole sforzo e semplificato le cose. E con questo intendo qualcosa sulla falsariga di ciò che gli altri hanno detto su YAGNI, KISS e DRY.

Un possibile aspetto negativo: la ripetizione. Dovrai creare potenzialmente queste interfacce per molte classi e anche per una sola classe devi nominare ciascun metodo e descrivere ogni firma almeno due volte. Di tutte le cose che mi bruciano nella codifica, dover cambiare più file in questo modo mi mette nei guai. Alla fine mi dimentico di apportare le modifiche in un posto o nell'altro. Una volta che hai una classe chiamata Foo e interfacce chiamate FooViewer e FooEditor, se decidi che sarebbe meglio se la chiamassi Bar invece devi rifattorizzare-> rinominare tre volte a meno che tu non abbia un IDE davvero incredibilmente intelligente. Ho anche scoperto che questo è il caso quando avevo una notevole quantità di codice proveniente da un generatore di codice.

Personalmente, non mi piace creare interfacce per cose che hanno anche una sola implementazione. Significa che quando viaggio nel codice non posso semplicemente passare direttamente alla sola implementazione, devo saltare all'interfaccia e quindi all'implementazione dell'interfaccia o almeno premere alcuni tasti extra per arrivarci. Quelle cose si sommano.

E poi c'è la complicazione che hai citato. Dubito che andrò mai più in questo modo con il mio codice. Anche per il posto che si adatta meglio al mio piano generale per il codice (un ORM che ho costruito) l'ho estratto e sostituito con qualcosa di più facile da codificare.

Darei davvero più pensiero alla composizione che hai citato. Sono curioso di sapere perché pensi che sarebbe una cattiva idea. Mi sarei aspettato di avere EffectiveStatscomporre e immutabile Statse qualcosa come StatModifiersche compongono ulteriormente un insieme di StatModifiers che rappresentano ciò che potrebbe modificare le statistiche (effetti temporanei, articoli, posizione in una certa zona valorizzazione, la fatica), ma che il vostro EffectiveStatsnon avrebbero bisogno di capire perché StatModifiersWould gestire quali erano quelle cose e quanto effetto e che tipo avrebbero avuto su quali statistiche. IlStatModifiersarebbe un'interfaccia per le diverse cose e potrebbe sapere cose come "sono nella zona", "quando svanisce la medicina", ecc ... ma non dovrebbe nemmeno far sapere ad altri oggetti tali cose. Dovrebbe solo dire quale stat e come è stato modificato in questo momento. Meglio ancora StatModifierpotrebbe semplicemente esporre un metodo per produrre un nuovo immutabile Statsbasato su un altro Statsche sarebbe diverso perché era stato modificato in modo appropriato. Potresti quindi fare qualcosa del genere currentStats = statModifier2.modify(statModifier1.modify(baseStats))e tutto ciò che Statspuò essere immutabile. Non lo scriverei nemmeno direttamente, probabilmente passerei in rassegna tutti i modificatori e li applicherei al risultato dei modificatori precedenti a partire da baseStats.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.