0

Consider Chess as an example. Say, we have a lot of domain objects that are alike, in this case chess pieces. I have two proposes to implementing the behaviour of chess pieces. Both uses the following interface.

interface IPiece {
    // ... shared behaviour
    bool IsValidMove(int targetX, int targetY);
    // ... more shared behaviour
}

Proposal 1: Functional Interfaces

Implement a standard implementation of a piece and use a functional interface.

interface IMoveValidator {
    bool IsValidMove(int currentX, int currentY, int targetX, int targetY);
}

class Piece : IPiece { private int _x, _y; private IMoveValidator _moveValidator;

public Piece(int x, int y, IMoveValidator moveValidator) {
    _x = x;
    _y = y;
    _moveValidator = moveValidator
}

// implement shared behaviour

public bool IsValidMove(int targetX, int targetY) {
    _moveValidator(_x, _y, targetX, targetY);
}

}

class PawnMoveValidator { bool IsValidMove(int currentX, int currentY, int targetX, int targetY) { // do unique stuff } }

// implement KnightMoveValidator, QueenMoveValidator ...

Proposal 2: Decorator-like Implementations

Implement a base implementation of a piece and use a unique class for each piece.

class BasePiece {
    private int _x, _y;
public BasePiece(int x, int y) {
    _x = x;
    _y = y;
}

// implement shared behaviour

}

class Pawn : IPiece { private BasePiece _base;

public Pawn(int x, int y) {
    _base = new BasePiece(x, y);
}

// delegate shared behaviour to BasePiece

public bool IsValidMove(int targetX, int targetY) {
    // do unique stuff
}

}

// implement Knight, Queen ...

The second proposal could be implemented using shallow inheritance with an abstract base class, but I chose to favour the compositional implementation for the sake of comparison.

Question

Now the above two proposal can be used to implement a lot of different objects that are alike, for example Chess Pieces or Cards in a Trading Card Game. Sometimes there are a few alike objects, sometimes like in Chess there are 6, and sometimes like in Magic: The Gathering there are over 20.000 unique cards.

Should I favour one of the proposals over the other? What are the benefits and liability of each? Are there even better alternative proposals?

1 Answers1

1

There is more than one way to skin a cat.

In software design, there are often different solutions, where none of them is clearly "better" than the others. Your example shows such a case, where both solution will surely work, but it is quite opinionated which one a developer should choose.

If I had to pick one of the designs up-front, I would pick neither of them:

  • in the first suggestion, there will be more services than just the IMoveValidator to be injected into a Piece, like a "DrawOnScreen" service, a "PieceDescription" service and maybe more. One could in theory mix different of these services, like "DrawPawnOnScreen" with a "QueenMoveValidator". This kind of flexibility does not make much sense to me. For chess, it looks overdesigned. However, in a game with 20000 different pieces or cards, such a design could make sense, when pieces (or cards) can be constructed from different building blocks.

    However, for chess, it does not feel obvious to me that the class type of a piece does not reflect the piece type. Sure, there is one exceptional rule in chess where a piece can change it's type during the game, but this can be implemented by creating a new piece object and exchanging the old piece by the new one.

  • in the second case, I would expect that a Pawn inherits from BasePiece (which I would call just Piece), which inherits from IPiece - a Pawn is a Piece, it does not have a piece. So this is one of the cases where I would actually not prefer composition over inheritance.

  • both designs suffer from the fact that move validation do not only require knowledge of the position of a piece and it's type, but also the state of the remaining chess board. Sure, your example is incomplete for the sake of simplicity, but this is one of the first problems one will have to solve when it comes to implementation.

    Somewhere, the board itself has to be passed into the move validation, and IMHO the design should reflect the decision where this will happen. See this older SWE.SE question Object Oriented Design for chess for suggestions on this topic.

There are other design possibilities here, like making the move validation and chess rules a responsibility of the board rather than the pieces, and let the pieces only assist to these tasks. So there is no "right-or-wrong". To some degree, these designs depend on the experiences and personal preferences of the designers.

Doc Brown
  • 218,378