Le implementazioni non sono intercambiabili e devono trasmettere spesso un odore di codice?

0

Sto costruendo un piccolo gioco simile agli scacchi. Mi piacerebbe poter riutilizzare la struttura anche per un'altra versione di dama. Sto modellando i giochi con le interfacce (mostrando solo quelli rilevanti):

RuleOrchestrator.class

public interface RuleOrchestrator {
    Collection<Move> allowedMoves(Player player, Move move);
}

Player.class

public interface Player {
    Move getMove(Board board);
}

AIEngine.class

public interface AIEngine {
    Move computeBestMove(Board board, RuleOrchestrator ruleOrchestrator);
}

Il design generale sembra andare bene per entrambi i giochi. Tuttavia, mi trovo a fare parecchi casting. Dopo averci pensato, sono giunto a ciò che sembra la radice del problema: sto usando le interfacce solo per definire il flusso di lavoro generale dei giochi, ma molte volte le implementazioni non sono intercambiabili. Le due implementazioni di AIEngine funzionano con qualsiasi Board e RuleOrchestrator , ma quasi ogni altra classe concreta funziona solo con le classi concrete nel proprio "regno" (gioco).

Chess.class

public class Chess implements Game {

    private final Player player1;
    private final Player player2;
    private final RuleOrchestrator ruleOrchestrator;
    private final Board board;

    public Chess(String player1, String player2, int size) {
        this.player1 = new HumanPlayer(player1);
        this.player2 = new HumanPlayer(player2);
        this.board = new ChessBoard(size);
        this.ruleOrchestrator = new ChessOrchestrator(board); // 1
    }
    // ...
}

ChessOrchestrator.class

public class ChessOrchestrator implements RuleOrchestrator {
    private final Board board; // 2
    public ChessOrchestrator(Board board) {
        this.board = board;
    }

    @Override
    public Collection<Move> allowedMoves(Player player, Move move) {
        // Do stuff
        ... ((ChessBoard)board).getKing(); // 3
        ChessMove chessMove = (ChessMove) move; // 4
        // More things
        // ...
    }
}

Ad esempio, ChessOrchestrator si aspetta di lavorare con una scacchiera. Ha bisogno di informazioni specifiche sugli scacchi e chiama metodi specifici per ChessBoard . Probabilmente non ha senso definire il campo in // 2 con tipo Board . L'utilizzo di ChessBoard eviterebbe di dover eseguire il cast. Quindi, avrei bisogno o di lavorare direttamente con ChessBoard anche in Chess , o casting in linea // 1. Inoltre, move deve anche essere castato a ChessMove su // 4.

Non solo non mi piacciono questi cast, ma temo che siano il sintomo di un problema più grande. È un odore di codice? Come posso evitare questi casting e migliorare la progettazione dell'app?

    
posta garci560 01.09.2017 - 01:00
fonte

2 risposte

6

How can I avoid these castings and improve the app's design?

Questo sembra un candidato per l'uso di tipi generici. È possibile scrivere una base generica per fornire un flusso di lavoro di base, anche se si utilizza un argomento di tipo, nel senso che per la famiglia Chess si fornisce ChessBoard .

Potresti avere un'interfaccia IOrchestrator<BoardType> , e qui puoi dichiarare metodi che funzionano su BoardType .

Potresti implementare ulteriormente una classe base generica astratta Orchestrator<BoardType> implements IOrchestrator<BoardType> . Questa classe astratta può fornire un campo board di tipo BoardType e alcune implementazioni di metodo predefinite per l'interfaccia.

Potresti avere ChessOrchestrator extends Orchestrator<ChessBoard> , che può usare il campo board di BoardType sapendo che è un ChessBoard , quindi senza casting.

    
risposta data 01.09.2017 - 01:34
fonte
3

Le implementazioni della stessa interfaccia che non sono intercambiabili violano il Principio di sostituzione di Liskov , e in effetti in tali casi la necessità di il cast è un odore.

In sostanza, la classe di implementazione viola l'interfaccia poiché formula ipotesi aggiuntive sugli argomenti di input dei metodi, supposizioni che non fanno parte dell'interfaccia. Il codice che utilizza l'interfaccia RuleOrchestrator deve passare il sottotipo corretto di Board al metodo allowedMoves, nel senso che deve conoscere qualcosa sulla classe di implementazione concreta di RuleOrchestrator che non è espressa nell'interfaccia. Ciò significa che il codice di consumo è strettamente associato all'implementazione.

    
risposta data 01.09.2017 - 02:12
fonte

Leggi altre domande sui tag