Referencing extendable ordinal numbers

I have a class that is basically a container (or composite) of 4 other objects. I’m trying to figure out the “best” way of referring to these objects, while still allowing it to be robust enough, so that future developer can increse it to 5 or 6, or 20.

This may be a case of YAGNI, but I’d like some some more input before I lock it down to 4.

Currently I have something like

public interface MoveSet {

    Move getFirstMove();

    Move getSecondMove();

    Move getThirdMove();

    Move getFourthMove();
}

A Move is an ability that can be used in turn-based combat, not like a sequence of moves in a Chess match.

Because I would like to keep this extensible, a possible subtype could be

public interface SixMoveSet extends MoveSet {

    Move getFifthMove();

    Move getSixthMove();
}

But then this could get messy as more and more subtypes are created, this gets out of hand. The other option I considered was using an indexed method, such as

public interface MoveSet {

    public Move getMove(int moveIndex);
}

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).

The vanilla way of doing this is to limit the number of available moves to 4, in all cases. At the same time however, if someone wants to have a max of 5 moves instead, I want to let them, while still keeping to the SOLID O as much as possible.

1

public interface MoveSet {

    public Move getMove(int moveIndex);
}

I can’t very well guarantee implementations will follow

Notice that in this case, no subclassing of MoveSet will be needed for extensions because new moves are stored as data instead of code. You would provide (the) one implementation of MoveSet and future code can use it to manage its moves.

Adding some constants like

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

could keep stuff a little safer to use and readable.

Of cource, the key for a move does not have to be an int. It could be some object implementing e.g. MoveId to get a little type safety.

Then a ReusableMoveSet could also look like

public interface ReusableMoveSet {

  public MoveId registerMove( Move m );

  public Move getMove( MoveId id );

}

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).

Your main concern when writing an interface is how that interface will be used from the outside. If you need additional validation on top of your interface, then as you say, you cannot rely on the implementation for validation, also, forcing it to include validation would violate SRP.

It would weaken the contract of the interface too, because the user of the interface has no sure way of knowing whether failed validation might result in a null or an exception, or maybe even ignored altogether. The user of the interface really needs to know these things so it can cope with them.

However, you can instead pass arguments which encapsulate that validation elsewhere instead of primitives, and keep the validation isolated as its own concern.

For example, you could build a series of separate validator classes which return a validated argument (depending on the number of permitted moves) instead of an 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)
    {
    }
}

then your MoveSet implementation would not need to worry about validation, it would just need to accept a MoveIndex instead; furthermore, the user of the MoveIndex interface will know what to do when validation fails:

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.
        }
    }
}

There are many variations on this, but the overall theme when mixing interfaces with validation (or any other constraint) is to provide wrapper classes for the data you want to pass as parameters – then those constraints become an equal part of the interface.

In the above case, the user of the interface can be sure that the index will be validated, and that failed validation will throw an OutOfRangeException.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *