Riferimento ai numeri ordinali estendibili

4

Ho una classe che è fondamentalmente un contenitore (o composto) di altri 4 oggetti. Sto cercando di capire il modo "migliore" di riferirsi a questi oggetti, pur continuando a consentirgli di essere abbastanza robusto, in modo che lo sviluppatore futuro possa aumentarlo a 5 o 6 o 20.

Potrebbe trattarsi di YAGNI, ma mi piacerebbe qualche altro input prima di bloccarlo a 4.

Attualmente ho qualcosa di simile

public interface MoveSet {

    Move getFirstMove();

    Move getSecondMove();

    Move getThirdMove();

    Move getFourthMove();
}

Una Move è un'abilità che può essere usata nel combattimento a turni, non come una sequenza di mosse in una partita di scacchi.

Perché vorrei mantenere questo estensibile, un possibile sottotipo potrebbe essere

public interface SixMoveSet extends MoveSet {

    Move getFifthMove();

    Move getSixthMove();
}

Ma questo potrebbe diventare disordinato via via che sempre più sottotipi vengono creati, questo sfugge di mano. L'altra opzione che ho considerato stava usando un metodo indicizzato, come

public interface MoveSet {

    public Move getMove(int moveIndex);
}

A quel punto, però, ho bisogno di eseguire ulteriori validazioni sull'input, che non posso garantire che le implementazioni seguiranno (che potrebbero essere sventolate a mano come comportamento non definito).

Il modo vanilla di fare ciò è di limitare il numero di mosse disponibili a 4, in tutti i casi. Allo stesso tempo, tuttavia, se qualcuno vuole avere un massimo di 5 mosse, voglio lasciarle, pur mantenendo il SOLIDO il più possibile.

    
posta Zymus 14.03.2016 - 10:10
fonte

2 risposte

1
public interface MoveSet {

    public Move getMove(int moveIndex);
}

I can't very well guarantee implementations will follow

Si noti che in questo caso non sarà necessaria alcuna sottoclasse di MoveSet per le estensioni poiché le nuove mosse sono archiviate come dati anziché come codice. Fornirai (a) un'implementazione di MoveSet e il futuro codice può usarlo per gestire i suoi spostamenti.

Aggiunta di alcune costanti come

public static final int ATTACK_MOVE = 0;
public static final int DEFEND_MOVE = 1;
...

potrebbe mantenere le cose un po 'più sicure da usare e leggere.

Di cource, la chiave per una mossa non deve essere un int . Potrebbe essere un oggetto che implementa, ad es. MoveId per ottenere un piccolo tipo di sicurezza.

Quindi anche ReusableMoveSet potrebbe apparire come

public interface ReusableMoveSet {

  public MoveId registerMove( Move m );

  public Move getMove( MoveId id );

}
    
risposta data 14.03.2016 - 11:46
fonte
0

At that point though, I need to perform additional validation on the input, which I can't very well guarantee implementations will follow (which could be hand-waved away as undefined behavior).

La tua principale preoccupazione quando scrivi un'interfaccia è come l'interfaccia verrà utilizzata dall'esterno. Se hai bisogno di ulteriore convalida sulla tua interfaccia, allora come dici tu, non puoi fare affidamento sull'implementazione per la validazione, inoltre, forzarla a includere la convalida violerebbe SRP.

Potrebbe indebolire anche il contratto dell'interfaccia, perché l'utente dell'interfaccia non ha un modo sicuro per sapere se la convalida non riuscita potrebbe dare come risultato un null o un'eccezione, o forse addirittura ignorarlo del tutto. L'utente dell'interfaccia ha davvero bisogno di sapere queste cose in modo che possa far fronte a loro.

Tuttavia, puoi invece passare argomenti che racchiudono quella convalida in un altro posto invece che in primitivi e mantengono isolata la validazione come sua propria preoccupazione.

Ad esempio, potresti creare una serie di classi di convalida separate che restituiscono un argomento convalidato (in base al numero di mosse consentite) invece di int :

public abstract class MoveIndex
{
     public int Index { get; }
}

public abstract class MoveIndexValidator
{
    private class MoveIndexImpl : MoveIndex
    {
        public int Index { get; set; }
    }

    private int _max;

    protected MoveIndexValidator(int max)
    {
        _max = max;
    }

    public MoveIndex CreateIndex(int index)
    {
        if (index >= 0 && index < _max)
        {
            return new MoveIndexImpl { Index = index };
        }
        else
        {
            throw new OutOfRangeException("Index out of range");
        }
    }
}

public class FourMoveIndexValidator : MoveIndexValidator
{
    public FourMoveIndexValidator() : base(4) 
    {
    }
}

public class SixMoveIndexValidator : MoveIndexValidator
{
    public SixMoveIndexValidator() : base(6)
    {
    }
}

quindi la tua implementazione MoveSet non avrebbe dovuto preoccuparsi della convalida, avrebbe invece dovuto accettare un MoveIndex ; inoltre, l'utente dell'interfaccia MoveIndex saprà cosa fare quando la convalida fallisce:

public interface MoveSet
{
    Move GetMove(MoveIndex index);
}

public class FourMoveApp
{
    private MoveIndexer _indexer = new FourMoveIndexer();

    public void DoMove(MoveSet moveSet, int rawIndex)
    {
        try 
        {
            var index = _indexer.CreateIndex(rawIndex);
            var move = moveSet.GetMove(index);
            // TODO: Something with 'move'.
        } 
        catch (OutOfRangeException e)
        {
            // etc.
        }
    }
}

Ci sono molte varianti su questo, ma il tema generale quando si mescolano le interfacce con la validazione (o qualsiasi altro vincolo) è quello di fornire classi wrapper per i dati che si desidera passare come parametri - quindi quei vincoli diventano una parte uguale dell'interfaccia .

Nel caso precedente, l'utente dell'interfaccia può essere certo che l'indice verrà convalidato e che la convalida non riuscita genererà un'eccezione OutOfRangeException.

    
risposta data 14.03.2016 - 13:25
fonte

Leggi altre domande sui tag