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.
fonte
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.Respostas:
À 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:
(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
Piece
agora 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
Piece
deve fornecer uma "única fonte de verdade", não duas.Obviamente, você pode tentar armazenar as informações de tipo apenas em
type
e também não criar classes filhoMovementComponent
. Mas este projeto provavelmente levaria a uma "switch(type)
" declaração enorme na implementação deGetValidMoveSquares
. 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()
paraBasePiece
e 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.
fonte
movementComponent
. Veja minha edição como fornecer um campo de tipo de uma maneira segura no design "herança".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.fonte
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.
fonte
Eu começaria fazendo algumas observações sobre o domínio do problema (ou seja, regras do xadrez):
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:
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 umaGame
instância para suportar diferentes variantes de xadrez.fonte
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.
fonte
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.
fonte
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
Piece
classe (como você já tem), assim como umaPieceType
classe. OsPieceType
define classe todos os modos que um peão, rainha, ect deve definem, tais como,CanMoveTo
,GetPieceTypeName
, ect. Em seguida, osPawn
eQueen
, classes ect seria praticamente herdam daPieceType
classe.fonte
Piece
nã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 ePiece
não à suaPieceType
são e, potencialmente, à sua história de movimento.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.
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
Piece
classe ter umtype
campo 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.
fonte
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:
fonte