Herança vs Composição de peças de xadrez


9

Uma pesquisa rápida dessa troca de pilha mostra que, em geral, a composição é geralmente considerada mais flexível que a herança, mas, como sempre, depende do projeto etc., e há momentos em que a herança é a melhor escolha. Quero fazer um jogo de xadrez 3D em que cada peça tenha uma malha, possivelmente animações diferentes e assim por diante. Neste exemplo concreto, parece que você poderia argumentar sobre as duas abordagens. Estou errado?

A herança seria algo parecido com isto (com construtor adequado, etc.)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

que certamente obedece ao princípio "é-a", enquanto a composição se pareceria com isso

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

É mais uma questão de preferência pessoal ou uma abordagem é claramente uma escolha mais inteligente do que a outra aqui?

EDIT: Eu acho que aprendi algo com cada resposta, então me sinto mal por escolher apenas uma. Minha solução final se inspirará em vários dos posts aqui (ainda trabalhando nisso). Obrigado a todos que tiveram tempo para responder.


Acredito que ambos podem existir lado a lado, esses dois têm finalidades diferentes, mas não são exclusivos um do outro. O xadrez é composto de diferentes peças e tábuas, e as peças são de tipos diferentes. Essas peças compartilham algumas propriedades e comportamentos básicos e também possuem propriedades e comportamentos específicos. Então, de acordo com mim, a composição deve ser aplicada sobre o tabuleiro e as peças, enquanto a herança deve ser seguida sobre os tipos de peças.
Hitesh Gaur

Embora algumas pessoas alegem que toda a herança é ruim, acho que a idéia geral é que a herança da interface seja boa / boa e que a herança da implementação seja útil, mas pode ser problemática. Qualquer coisa além de um único nível de herança é questionável, na minha experiência. Pode ser bom para além disso, mas não é simples sair sem fazer uma bagunça complicada.
JimmyJames 16/05/19

O padrão de estratégia é comum na programação de jogos por um motivo. var Pawn = new Piece(howIMove, howITake, whatILookLike)parece muito mais simples, mais gerenciável e mais sustentável para mim do que uma hierarquia de herança.
Ant P

@ AntP Esse é um bom ponto, obrigado!
entanto

@AntP Faça uma resposta! ... Há muito mérito nisso!
Svidgen 17/05/19

Respostas:


4

À primeira vista, sua resposta finge que a solução "composição" não usa herança. Mas acho que você simplesmente esqueceu de adicionar isso:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(e mais dessas derivações, uma para cada um dos seis tipos de peças). Agora, isso se parece mais com o padrão clássico de "Estratégia" e também está utilizando herança.

Infelizmente, esta solução tem uma falha: cada uma Pieceagora contém suas informações de tipo redundantemente duas vezes:

  • dentro da variável de membro type

  • interior movementComponent(representado pelo subtipo)

Essa redundância é o que realmente pode causar problemas - uma classe simples como a Piecedeve fornecer uma "única fonte de verdade", não duas.

Obviamente, você pode tentar armazenar as informações de tipo apenas em typee também não criar classes filho MovementComponent. Mas este projeto provavelmente levaria a uma " switch(type)" declaração enorme na implementação de GetValidMoveSquares. E essa é definitivamente uma forte indicação de que a herança é a melhor escolha aqui .

Nota no projeto "herança", é muito fácil para fornecer o campo "tipo" de uma forma não redundante: adicionar um método virtual GetType()para BasePiecee implementá-lo em conformidade em cada classe de base.

Com relação à "promoção": estou aqui com o @svidgen, considero os argumentos apresentados na resposta do @ TheCatWhisperer discutíveis.

Interpretar "promoção" como uma troca física de peças, em vez de interpretá-la como uma alteração do tipo da mesma peça, me parece muito mais natural. Portanto, implementar isso de maneira semelhante - trocando uma peça por outra de um tipo diferente - provavelmente não causará grandes problemas - pelo menos não no caso específico do xadrez.


Obrigado por me dizer o nome do padrão, eu não sabia que ele se chamava Estratégia. Você também tem razão. Perdi a herança do componente movimento na minha pergunta. O tipo é realmente redundante? Se eu quiser iluminar todas as rainhas do quadro, pareceria estranho verificar qual componente de movimento elas tinham, além disso, eu precisaria moldar a componente de movimento para ver que tipo de coisa seria super feia, não?
entanto

@ CanisleepYet: o problema com o campo "tipo" proposto é que ele pode divergir do subtipo de movementComponent. Veja minha edição como fornecer um campo de tipo de uma maneira segura no design "herança".
Doc Brown

4

Eu provavelmente preferiria a opção mais simples, a menos que você tenha razões explícitas e bem articuladas ou fortes preocupações de extensibilidade e manutenção.

A opção de herança é menos linha de código e menos complexidade. Cada tipo de peça tem uma relação "imutável" de 1 para 1 com suas características de movimento. (Diferentemente da representação, que é 1 para 1, mas não necessariamente imutável - você pode oferecer várias "skins".)

Se você quebrar essa relação imutável 1-to-1 entre as peças e seu comportamento / movimento em várias classes, você está provavelmente única acrescentando complexidade - e não muito mais. Portanto, se eu estivesse revisando ou herdando esse código, esperaria ver bons motivos documentados para a complexidade.

Tudo isso dito, uma opção ainda mais simples , a IMO, é criar uma Piece interface que suas peças individuais implementem . Na maioria dos idiomas, isso é realmente muito diferente da herança , porque uma interface não o impede de implementar outra interface . ... Nesse caso, você simplesmente não recebe nenhum comportamento de classe base: você gostaria de colocar o comportamento compartilhado em outro lugar.


Não acredito que a solução de herança seja 'menos linhas de código'. Talvez nesta classe, mas não no geral.
JimmyJames 16/05/19

@JimmyJames Really? ... Apenas conte as linhas de código no OP ... reconhecidamente não a solução inteira, mas não é um mau indicador de qual solução é mais LOC e complexidade a ser mantida.
Svidgen 16/05/19

Eu não quero enfatizar muito isso, porque tudo é ideal neste momento, mas sim, acho que sim. Minha opinião é que esse pedaço de código é insignificante comparado a todo o aplicativo. Se isso simplificar a implementação das regras (discutível), você poderá salvar dezenas ou mesmo centenas de linhas de código.
JimmyJames 16/05/19

Obrigado, não pensei na interface, mas você pode estar certo de que este é o melhor caminho a percorrer. Mais simples é quase sempre melhor.
entanto

2

O problema do xadrez é que as regras do jogo e da peça são prescritas e fixadas. Qualquer design que funcione bem - use o que quiser! Experimente e experimente todos.

No mundo dos negócios, no entanto, nada é tão rigorosamente prescrito assim - as regras e os requisitos de negócios mudam com o tempo e os programas precisam mudar com o tempo para acomodar. É aqui que os dados "é um" vs. "tem um" fazem a diferença. Aqui, a simplicidade facilita a manutenção de soluções complexas ao longo do tempo e a alteração dos requisitos. Em geral, nos negócios, também precisamos lidar com a persistência, que também pode envolver um banco de dados. Portanto, temos regras como não usar várias classes quando uma única classe fará o trabalho e não usar herança quando a composição for suficiente. Mas essas regras são todas voltadas para tornar o código sustentável a longo prazo, diante de regras e requisitos alterados - o que não é o caso do xadrez.

Com o xadrez, o caminho de manutenção mais provável a longo prazo é que seu programa precisa ficar cada vez mais inteligente, o que eventualmente significa que as otimizações de velocidade e armazenamento serão dominantes. Para isso, você geralmente precisará fazer trocas que sacrificam a legibilidade pelo desempenho, e assim, mesmo o melhor design de OO acabará por ser esquecido.


Note que houve muitos jogos de sucesso que adotam um conceito e, em seguida, lançam algumas coisas novas na mistura. Se você quiser fazer isso no futuro com seu jogo de xadrez, deve levar em consideração possíveis alterações futuras.
Flater

2

Eu começaria fazendo algumas observações sobre o domínio do problema (ou seja, regras do xadrez):

  • O conjunto de movimentos válidos para uma peça depende não apenas do tipo da peça, mas também do estado do tabuleiro (o peão pode avançar se o quadrado estiver vazio / pode se mover na diagonal ao capturar uma peça).
  • Certas ações envolvem a remoção de uma peça existente do jogo (promoção / captura de uma peça) ou a movimentação de várias peças (rolagem).

Elas são difíceis de modelar como uma responsabilidade da peça em si, e nem herança nem composição são ótimas aqui. Seria mais natural modelar essas regras como cidadãos de primeira classe:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRuleé uma interface simples, mas flexível, que permite que as implementações atualizem o estado da placa de qualquer maneira necessária para dar suporte a movimentos complexos, como conversão e promoção. Para o xadrez padrão, você teria uma implementação para cada tipo de peça, mas também pode conectar regras diferentes em uma Gameinstância para suportar diferentes variantes de xadrez.


Abordagem interessante - Boa comida para reflexão
CanISleepAgora 17/05/19

1

Eu pensaria que, neste caso, a herança seria a escolha mais limpa. A composição pode ser um pouco mais elegante, mas me parece um pouco mais forçada.

Se você estava planejando desenvolver outros jogos que usem peças móveis que se comportam de maneira diferente, a composição pode ser uma escolha melhor, especialmente se você empregou um padrão de fábrica para gerar as peças necessárias para cada jogo.


2
Como você lidaria com a promoção usando herança?
TheCatWhisperer

4
@TheCatWhisperer Como você lida com isso quando está jogando o jogo fisicamente? (Espero que você substituir a peça - você não re-esculpir o peão, não é !?)
svidgen

0

que certamente obedece ao princípio "é-a"

Certo, então você usa herança. Você ainda pode compor na classe Piece, mas pelo menos terá uma espinha dorsal. A composição é mais flexível, mas a flexibilidade é superestimada, em geral, e certamente neste caso, porque as chances de alguém apresentar um novo tipo de peça que exigiria que você abandonasse seu modelo rígido são nulas. O jogo de xadrez não vai mudar tão cedo. A herança fornece um modelo de orientação, algo para se relacionar também, ensino de código, direção. Composição nem tanto, as entidades de domínio mais importantes não se destacam, é covarde, você precisa entender o jogo para entender o código.


graças para adicionar o ponto de que, com composição é mais difícil raciocinar sobre o código
CanISleepYet

0

Por favor, não use herança aqui. Embora as outras respostas certamente tenham muitas pérolas de sabedoria, usar herança aqui certamente seria um erro. Usar a composição não apenas ajuda a lidar com problemas que você não antecipa, mas já vejo aqui um problema que a herança não pode lidar normalmente.

A promoção, quando um peão é convertido em uma peça mais valiosa, pode ser um problema de herança. Tecnicamente, você pode lidar com isso substituindo uma peça por outra, mas essa abordagem tem limitações. Uma dessas limitações seria o rastreamento de estatísticas por peça, onde você pode não querer redefinir as informações da peça após a promoção (ou escrever o código extra para copiá-las).

Agora, quanto ao seu design de composição na sua pergunta, acho que é desnecessariamente complicado. Não suspeito que você precise de um componente separado para cada ação e possa se ater a uma classe por tipo de peça. O tipo enum também pode ser desnecessário.

Eu definiria uma Piececlasse (como você já tem), assim como uma PieceTypeclasse. Os PieceTypedefine classe todos os modos que um peão, rainha, ect deve definem, tais como, CanMoveTo, GetPieceTypeName, ect. Em seguida, os Pawne Queen, classes ect seria praticamente herdam da PieceTypeclasse.


3
Os problemas que você vê com promoção e substituição são triviais, IMO. A separação de preocupações exige praticamente que esse "rastreamento por peça" (ou o que seja) seja tratado independentemente de qualquer maneira . ... Quaisquer vantagens potenciais que sua solução oferece são ofuscadas pelas preocupações imaginárias que você está tentando resolver, IMO.
Svidgen 15/05/19

5
Para esclarecer: Sem dúvida, há alguns méritos na sua solução proposta. Porém, é difícil encontrar na sua resposta, que se concentra principalmente em problemas realmente fáceis de resolver na "solução de herança". ... Isso diminui (para mim, de qualquer maneira) qualquer que seja o mérito que você esteja tentando comunicar sobre sua opinião.
Svidgen 15/05/19

11
Se Piecenão for virtual, concordo com esta solução. Embora o design da classe possa parecer um pouco mais complexo na superfície, acho que a implementação será mais simples em geral. As principais coisas que estariam associadas à identidade e Piecenão à sua PieceTypesão e, potencialmente, à sua história de movimento.
JimmyJames 16/05/19

11
@svidgen Uma implementação que usa uma peça composta e uma implementação que usa uma peça baseada em herança parecerá basicamente idêntica, além dos detalhes descritos nesta solução. Esta solução de composição adiciona um único nível de indireção. Não vejo isso como algo que valha a pena evitar.
JimmyJames 16/05/19

2
@JimmyJames Right. Mas, o histórico de movimentação de várias peças precisa ser rastreado e correlacionado - não é algo que uma única peça deva ser responsável, IMO. É uma "preocupação separada" se você gosta de coisas do SOLID.
Svidgen 16/05/19

0

Do meu ponto de vista, com as informações fornecidas, não é sensato responder se herança ou composição devem ser o caminho a seguir.

  • Herança e composição são apenas ferramentas no cinto de ferramentas do designer orientado a objetos.
  • Você pode usar essas ferramentas para projetar muitos modelos diferentes para o domínio do problema.
  • Como existem muitos desses modelos que podem representar um domínio, dependendo do ponto de vista do designer, você pode combinar herança e composição de várias maneiras para criar modelos funcionalmente equivalentes.

Seus modelos são totalmente diferentes; no primeiro, você modelou em torno do conceito de peças de xadrez; no segundo, no conceito de movimento das peças. Eles podem ser funcionalmente equivalentes, e eu escolheria o que melhor representa o domínio e me ajuda a raciocinar sobre ele com mais facilidade.

Além disso, em seu segundo design, o fato de sua Piececlasse ter um typecampo revela claramente que há um problema de design aqui, pois a peça em si pode ser de vários tipos. Não parece que isso provavelmente deva usar algum tipo de herança, onde a peça em si é do tipo?

Então, veja bem, é muito difícil argumentar sobre sua solução. O que importa não é se você usou herança ou composição, mas se o seu modelo reflete com precisão o domínio do problema e se é útil argumentar sobre ele e fornecer uma implementação dele.

É necessária alguma experiência para usar a herança e a composição corretamente, mas elas são ferramentas totalmente diferentes e não acredito que uma possa "substituir" uma pela outra, embora eu concorde que um sistema possa ser projetado inteiramente usando apenas a composição.


Você enfatiza que é o modelo que é importante e não a ferramenta. Em relação ao tipo redundante, a herança realmente ajuda? Por exemplo, se eu quiser destacar todos os peões ou verificar se uma peça era um rei, não precisaria de fundição?
entanto

-1

Bem, eu não sugiro.

Embora a orientação a objetos seja uma boa ferramenta e muitas vezes ajude a simplificar as coisas, ela não é a única ferramenta no compartimento de ferramentas.

Em vez disso, considere implementar uma classe de quadro, contendo uma simples coluna de derivação.
A interpretação e o cálculo das coisas são feitos nas funções-membro usando mascaramento de bits e comutação.

Use o MSB para o proprietário, deixando 7 bits para a peça, embora reserve zero para vazio.
Alternativamente, zero para vazio, sinal para o proprietário, valor absoluto para a peça.

Se desejar, você pode usar campos de bits para evitar o mascaramento manual, embora eles não sejam portáveis:

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};

Interessante ... e apto, considerando que é uma questão de C ++. Mas, não sendo um programador de C ++, esse não seria meu primeiro (ou segundo ou terceiro ) instinto! ... Talvez essa seja a resposta mais em C ++ aqui!
Svidgen 15/05/19

7
Essa é uma microoptimização que nenhum aplicativo real deve seguir.
Lie Ryan

@LieRyan Se tudo que você tem é POO, tudo cheira a um objeto. E se é realmente esse "micro" também é uma pergunta interessante. Dependendo do que você faz com isso, isso pode ser bastante significativo.
Deduplicator

Heh ... Dito isto , C ++ é orientado a objetos. Eu suponho que há uma razão o OP está usando C ++ em vez de apenas C .
Svidgen 16/05/19

3
Aqui, estou menos preocupado com a falta de OOP, mas ofusco o código com alguns ajustes. A menos que você esteja escrevendo código para a Nintendo de 8 bits ou escrevendo algoritmos que exijam lidar com milhões de cópias do estado da placa, isso é uma micro otimização prematura e desaconselhável. Nos cenários normais, provavelmente não será mais rápido, porque o acesso a bits desalinhados exige operações extras de bits.
Lie Ryan
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.