Interfaccia separata per i metodi di mutazione

11

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

Ho un'interfaccia Foo definita 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 MutableFoo che fornisce i 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);
}

Ci sono un paio di implementazioni di MutableFoo che potrebbero esistere (non le ho ancora implementate). Uno di questi è

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é è altrettanto probabile che ognuno venga usato. Significa, è altrettanto probabile che qualcuno che usa queste classi vorrà un'istanza immutabile, poiché ne vorranno uno mutabile.

Il caso d'uso principale che ho è un'interfaccia chiamata StatSet che rappresenta alcuni dettagli di combattimento per un gioco (punti ferita, attacco, difesa). Tuttavia, le statistiche "effettive", o le statistiche effettive, sono il risultato delle statistiche di base, che non possono mai essere cambiate, e le statistiche addestrate, che possono essere aumentate. Questi due sono correlati 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);
}

gliStats 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 aumentano subito dopo la battaglia. Utilizzando determinati oggetti, utilizzando determinate tattiche, l'uso intelligente del campo di battaglia può garantire un'esperienza diversa.

Ora per le mie domande:

  1. Esiste un nome per suddividere le interfacce di accessori e mutatori in interfacce separate?
  2. È diviso in questo modo l'approccio "giusto" se è altrettanto probabile che venga utilizzato, oppure esiste un modello diverso e più accettato che dovrei usare al suo posto (ad esempio Foo foo = FooFactory.createImmutableFoo(); che potrebbe restituire DefaultFoo o DefaultMutableFoo ma è nascosto perché createImmutableFoo restituisce Foo )?
  3. Ci sono degli aspetti immediatamente prevedibili nell'usare 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 efficaci possono essere modificate indipendentemente dalle sue parti.

Rendere una nuova classe per EffectiveStatSet non ha molto senso perché non stiamo estendendo le funzionalità in alcun modo. Potremmo modificare l'implementazione e creare un EffectiveStatSet di un composito 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);
    }
}
    
posta Zymus 23.06.2015 - 07:46
fonte

5 risposte

6

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

Is there a name for splitting interfaces by accessors and mutators into separate interfaces?

Questo potrebbe essere un po 'provocatorio, ma in realtà lo chiamerei "sovradimensionamento delle cose" o "sovracompensazione delle cose". Offrendo una variante mutabile e immutabile della stessa classe, offrite due soluzioni funzionalmente equivalenti per lo stesso problema, che si differenziano solo per aspetti non funzionali come il comportamento delle prestazioni, l'API e la sicurezza contro gli effetti collaterali. Immagino che sia perché hai paura di prendere una decisione da preferire o perché cerchi di implementare la funzione "const" di C ++ in C #. Credo che nel 99% dei casi non farà una grande differenza se l'utente sceglie la variante mutabile o immutabile, può risolvere i suoi problemi con l'uno o l'altro. Quindi "la probabilità di una classe da utilizzare" è probabilmente il risultato di ciò che offri nel tuo grimorio: se offri due soluzioni quasi simili allo stesso problema, aspettati il 50% di possibilità che gli utenti scelgano la variante A o B. Se offri solo una soluzione, la probabilità è alta, useranno questa soluzione e ne saranno felici.

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

Is splitting them in this way the 'right' approach if they are equally likely to be used or is there a different, more accepted pattern

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

Are there any immediately foreseeable downsides to using this pattern, save for complicating the interface hierarchy?

Disegni complicati rendono il software più difficile da testare, difficile da mantenere, più difficile da far evolvere.

    
risposta data 02.08.2015 - 09:11
fonte
2

Is there a name for splitting interfaces by accessors and mutators into separate interfaces?

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

Puoi dirci qualsiasi caso di utilizzo aziendale in cui le due interfacce separate ci danno vantaggio o questa domanda è un problema accademico (YAGNI)?

Posso pensare di acquistare un carrello mutevole (puoi inserire più articoli) che può diventare un ordine in cui gli articoli non possono più essere modificati dal cliente. Lo stato dell'ordine è ancora modificabile.

Le implementazioni che ho visto non separano le interfacce

  • non c'è un'interfaccia ReadOnlyList in java

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

    
risposta data 23.06.2015 - 14:13
fonte
1

La separazione di un'interfaccia di raccolta mutabile e un'interfaccia di raccolta del contratto di sola lettura è un esempio del principio di separazione delle interfacce. Non penso che ci siano nomi speciali dati a ciascuna applicazione di principio.

Notate alcune parole qui: "read-only-contract" e "collection".

Un contratto di sola lettura significa che la classe A fornisce alla classe B un 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 dice solo che il destinatario non è autorizzato a cambiarlo; qualcun altro (in particolare, la classe A ) può cambiarlo.

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

Il pattern è molto probabilmente osservato su oggetti che rappresentano una raccolta di dati: elenchi, sequenze, file (flussi), ecc.

La parola "immutabile" diventa 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 valori.
    • Questo DTO conterrà i tre campi: hitpoints , attack , defense .
    • Solo campi: pubblico, chiunque può scrivere, nessuna protezione.
    • Tuttavia, un DTO deve essere un oggetto throwaway: se la classe A deve passare un DTO alla classe B , ne fa una copia e passa la copia. Quindi, B può usare il DTO come preferisce (scrivendo su di esso), senza influenzare il DTO a cui A tiene.
  2. La funzione grantExperience da suddividere in due:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats prende gli input da due DTO, uno che rappresenta le statistiche del giocatore e un altro che rappresenta le statistiche nemiche ed esegue i calcoli.
    • Per l'input, il chiamante dovrebbe scegliere tra le statistiche di base, allenate o efficaci, in base alle tue esigenze.
    • Il risultato sarà un nuovo DTO in cui ogni campo ( hitpoints , attack , defense ) memorizza l'importo da incrementare per quell'abilità.
    • Il nuovo DTO "quantità da incrementare" non riguarda il limite superiore (limite massimo imposto) per quei valori.
  4. increaseStats è un metodo sul giocatore (non sul DTO) che prende un DTO "importo da incrementare" e applica tale incremento sul DTO che è di proprietà del giocatore e rappresenta il DTO addestrabile del giocatore.
    • Se ci sono valori massimi applicabili per queste statistiche, vengono applicate qui.

Nel caso in cui calculateNewStats non dipenda da nessun altro giocatore o informazione nemica (oltre i valori nei due DTO di input), questo metodo può potenzialmente trovarsi in qualsiasi punto del progetto.

Se si trova che calculateNewStats ha una dipendenza completa dal giocatore e dagli oggetti nemici (cioè, il giocatore futuro e gli oggetti nemici possono avere nuove proprietà, e calculateNewStats deve essere aggiornato per consumare la maggior parte delle loro nuove proprietà come possibile), quindi calculateNewStats deve accettare questi due oggetti, non solo il DTO. Tuttavia, il risultato del calcolo sarà comunque il DTO incrementale o qualsiasi oggetto di trasferimento dati semplice che trasporta le informazioni utilizzate per eseguire un incremento / upgrade.

    
risposta data 02.08.2015 - 08:50
fonte
1

Qui c'è un grosso problema: solo perché una struttura dati è immutabile non significa che non abbiamo bisogno di versioni modificate di esso . La versione immutabile reale di una struttura dati fornisce metodi setX , setY e setZ - restituiscono semplicemente una struttura nuova invece di modificare l'oggetto li ha richiamati.

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

Quindi, come si fa a dare a una parte del sistema la possibilità di cambiarla limitando le altre parti? Con un oggetto mutevole che contiene un riferimento a un'istanza della struttura immutabile. Fondamentalmente, invece che la tua classe di giocatori è in grado di mutare il suo oggetto statistico dando a 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 statistiche correnti del giocatore sono un riferimento mutabile all'oggetto immutabile. Non c'è bisogno di creare due interfacce - semplicemente rendere la struttura dei dati completamente immutabile e gestirne un riferimento mutevole dove lo si sta usando per memorizzare lo stato.

Questo significa che altri oggetti nel tuo sistema non possono contare su un riferimento all'oggetto stats - l'oggetto statistico che hanno non sarà lo stesso oggetto statistico che il giocatore ha dopo che il giocatore ha aggiornato le sue statistiche.

Questo ha più senso comunque, dal momento che concettualmente non sono proprio le stats che stanno cambiando, sono le attuali statistiche del giocatore . Non l'oggetto stats. Il riferimento all'oggetto statistico che l'oggetto giocatore ha. Quindi, altre parti del tuo sistema che dipendono da quel riferimento dovrebbero fare esplicitamente riferimento a player.currentStats() o ad alcune di esse invece di ottenere l'oggetto statistico sottostante del giocatore, memorizzarlo da qualche parte e affidarsi all'aggiornamento tramite le mutazioni.

    
risposta data 06.08.2015 - 10:48
fonte
1

Wow ... questo mi riporta davvero indietro. Ho provato questa stessa idea un paio di volte. Ero troppo testardo per rinunciarci perché pensavo che ci sarebbe stato qualcosa di buono da imparare. Ho visto altri provare questo anche nel codice. La maggior parte delle implementazioni che ho visto hanno chiamato le interfacce di sola lettura come la tua FooViewer o ReadOnlyFoo e l'interfaccia di sola scrittura FooEditor o WriteableFoo o in Java penso di aver visto FooMutator una volta. Non sono sicuro che ci sia un vocabolario ufficiale o addirittura comune per fare le cose in questo modo.

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

Un possibile inconveniente: la ripetizione. Dovrai creare queste interfacce per molte classi potenzialmente e anche per una sola classe dovrai 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 dà più fastidio. Alla fine finisco per dimenticare di fare i cambiamenti 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, devi rifattorizzare > rinominare tre volte a meno che tu non abbia un IDE davvero sorprendentemente intelligente. Ho anche trovato questo quando ho avuto 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 passare direttamente all'implementazione only , devo passare all'interfaccia e quindi all'implementazione dell'interfaccia o almeno premere qualche extra le chiavi per arrivarci. Queste cose si sommano.

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

Vorrei davvero dare più pensiero alla composizione che hai menzionato. Sono curioso perché pensi che sarebbe una cattiva idea. Mi sarei aspettato di avere EffectiveStats comporre e immutable Stats e qualcosa come StatModifiers che comporrebbe ulteriormente un insieme di StatModifier s che rappresenta tutto ciò che potrebbe modificare le statistiche (effetti temporanei, elementi, posizione in qualche area di miglioramento, affaticamento) ma il tuo EffectiveStats non avrebbe bisogno di capire perché StatModifiers avrebbe gestito cosa fossero quelle cose e quanti effetti e quale tipo avrebbero avuto su quali statistiche. Il StatModifier sarebbe un'interfaccia per le diverse cose e potrebbe sapere cose come "sono nella zona", "quando la medicina si esaurisce", ecc ... ma non dovrebbe nemmeno lasciare che altri oggetti sappiano come cose. Dovrebbe solo dire quale statistica e come è stata modificata in questo momento. Meglio ancora StatModifier potrebbe semplicemente esporre un metodo per produrre un nuovo Stats immutabile basato su un altro Stats che sarebbe diverso perché era stato alterato in modo appropriato. Potresti quindi fare qualcosa come currentStats = statModifier2.modify(statModifier1.modify(baseStats)) e tutto Stats può essere immutabile. Non lo scriverei direttamente, probabilmente eseguirò un ciclo di tutti i modificatori e applicherò ciascuno al risultato dei precedenti modificatori che iniziano con baseStats .

    
risposta data 07.08.2015 - 23:20
fonte

Leggi altre domande sui tag