Quando parar a herança?

9

Era uma vez, fiz uma pergunta no Stack Overflow sobre herança.

Eu disse que projeto um mecanismo de xadrez da maneira OOP. Então, eu herdo todas as minhas peças da classe abstrata Piece, mas a herança ainda continua. Deixe-me mostrar por código

public abstract class Piece
{
    public void MakeMove();
    public void TakeBackMove();
}

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Programadores consideraram meu projeto um pouco mais do que engenharia e sugeriram remover classes de peças coloridas e manter a cor das peças como membro da propriedade, como abaixo.

public abstract class Piece
{
    public Color Color { get; set; }
    public abstract void MakeMove();
    public abstract void TakeBackMove();
}

Dessa maneira, Piece pode conhecer sua cor. Após a implementação, vi a minha implementação como abaixo.

public abstract class Pawn: Piece
{
    public override void MakeMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }

    public override void TakeBackMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }
}

Agora vejo que manter a propriedade color causando declarações if nas implementações. Isso me faz sentir que precisamos de peças de cores específicas na hierarquia de herança.

Nesse caso, você teria aulas como WhitePawn, BlackPawn ou iria projetar mantendo a propriedade Color?

Sem esse problema, como você gostaria de iniciar o design? Manter a propriedade Color ou ter uma solução de herança?

Editar: quero especificar que meu exemplo pode não corresponder completamente ao exemplo da vida real. Portanto, antes de tentar adivinhar os detalhes da implementação, concentre a questão.

Na verdade, eu simplesmente pergunto se o uso da propriedade Color causará se as instruções usarem melhor a herança?

Sangue fresco
fonte
3
Eu realmente não entendo por que você modelaria as peças assim. Quando você chama MakeMove (), que movimento ele fará? Eu diria que o que você realmente precisa para começar é modelar o estado do quadro e ver quais classes você precisa para isso
jk.
11
Penso que o seu equívoco básico não é perceber que no xadrez a "cor" é realmente um atributo do jogador e não da peça. É por isso que dizemos "negros se mexem" etc., a cor está lá para identificar qual jogador é o dono da peça. Um peão segue as mesmas regras e restrições de movimento, não importa de que cor seja a única variação: qual direção é "para frente".
James Anderson
5
quando você não pode imaginar "quando parar" a herança, é melhor abandoná-la completamente. Você pode reintroduzir a herança no estágio posterior de design / implementação / manutenção, quando a hierarquia se tornar óbvia para você. Eu uso essa abordagem, funciona como um encanto
mosquito
11
A questão mais desafiadora é criar ou não uma subclasse para cada tipo de peça. O tipo de peça determina mais aspectos comportamentais do que a cor da peça.
NoChance 18/04
3
Se você está tentado a iniciar um design com ABCs (Abstract Base Classes) nos faz um favor e não ... Os casos em que os ABCs são realmente úteis são muito raros e, mesmo assim, geralmente é mais útil usar uma interface em vez de.
Evan solha

Respostas:

12

Imagine que você projeta um aplicativo que contém veículos. Você cria algo assim?

class BlackVehicle : Vehicle { }
class DarkBlueVehicle : Vehicle { }
class DarkGreenVehicle : Vehicle { }
// hundreds of other classes...

Na vida real, a cor é uma propriedade de um objeto (um carro ou uma peça de xadrez) e deve ser representada por uma propriedade.

São MakeMovee TakeBackMovediferentes para negros e brancos? Nem um pouco: as regras são as mesmas para os dois jogadores, o que significa que esses dois métodos serão exatamente os mesmos para pretos e brancos , o que significa que você está adicionando uma condição em que não precisa.

Por outro lado, se você tiver WhitePawne BlackPawn, não ficarei surpreso se, cedo ou tarde, você escrever:

var piece = (Pawn)this.CurrentPiece;
if (piece is WhitePawn)
{
}
else // The pice is of type BlackPawn
{
}

ou até algo como:

public abstract class Pawn
{
    public Color Player
    {
        get
        {
            return this is WhitePawn ? Color.White : Color.Black;
        }
    }
}

// Now imagine doing the same thing for every other piece.
Arseni Mourzenko
fonte
4
@ Freshblood Eu acho que seria melhor ter uma directionpropriedade e usá-la para mover do que usar a propriedade color. Idealmente, a cor deve ser usada apenas para renderização, não para lógica.
Gyan aka Gary Buyn
7
Além disso, as peças se movem na mesma direção, para frente, para trás, esquerda, direita. A diferença é de que lado do tabuleiro eles começam. Em uma estrada, carros em faixas opostas avançam.
Slipset
11
@Blrfl Você pode estar certo de que a directionpropriedade é um booleano. Não vejo o que há de errado nisso. O que me confundiu sobre a questão até o comentário de Freshblood acima é por que ele gostaria de mudar a lógica do movimento com base na cor. Eu diria que isso é mais difícil de entender porque não faz sentido a cor afetar o comportamento. Se tudo o que você quer fazer é distinguir qual jogador possui qual peça, você pode colocá-las em duas coleções separadas e evitar a necessidade de uma propriedade ou herança. As peças em si podem ser alegremente inconscientes de qual jogador elas pertencem.
Gyan aka Gary Buyn
11
Eu acho que estamos olhando a mesma coisa de duas perspectivas diferentes. Se os dois lados fossem vermelhos e azuis , seu comportamento seria diferente de quando são preto e branco? Eu diria que não, e é por isso que digo que a cor não é a causa de diferenças de comportamento. É apenas uma distinção sutil, mas é por isso que eu usaria uma directionou talvez uma playerpropriedade em vez de uma colorna lógica do jogo.
Gyan aka Gary Buyn
11
@Freshblood white castle's moves differ from black's- como? Você quer dizer roque? O castelo do lado do rei e o castelo do lado da rainha são 100% simétricos para preto e branco.
28912 Konrad Morawski
4

O que você deve se perguntar é por que você realmente precisa dessas declarações na sua classe de peão:

    if (this.Color == Color.White)
    {

    }
    else
    {
    }

Tenho certeza de que será suficiente ter um pequeno conjunto de funções como

public abstract class Piece
{
    bool DoesPieceHaveSameColor(Piece otherPiece) { /*...*/   }

    Color OtherColor{get{/*...*/}}
    // ...
}

na sua classe Piece e implemente todas as funções Pawn(e as demais derivações de Piece) usando essas funções, sem nunca ter nenhuma dessas ifinstruções acima. A única exceção pode ser a função de determinar a direção do movimento de um peão por sua cor, já que isso é específico para peões, mas em quase todos os outros casos você não precisará de nenhum código específico de cor.

A outra pergunta interessante é se você realmente deve ter subclasses diferentes para diferentes tipos de peças. Isso me parece razoável e natural, já que as regras para os movimentos de cada peça são diferentes e você certamente pode implementá-lo usando diferentes funções sobrecarregadas para cada tipo de peça.

Claro, pode-se tentar modelar isso de uma forma mais genérica, separando as propriedades dos pedaços de suas regras em movimento, mas isso pode acabar em uma classe abstrata MovingRulee uma hierarquia de subclasse MovingRulePawn, MovingRuleQueen... eu não iria iniciar-se que maneira, uma vez que poderia facilmente levar a outra forma de excesso de engenharia.

Doc Brown
fonte
+1 por apontar que o código específico da cor é provavelmente um não-não. Os peões brancos e pretos se comportam essencialmente da mesma maneira, assim como todas as outras peças.
Konrad Morawski
2

A herança não é tão útil quando a extensão não afeta os recursos externos do objeto .

A propriedade Color não afeta como um objeto Piece é usado . Por exemplo, todas as peças podem chamar um método moveForward ou moveBackwards (mesmo se a movimentação não for legal), mas a lógica encapsulada da Piece usa a propriedade Color para determinar o valor absoluto que representa um movimento para frente ou para trás (-1 ou + 1 no "eixo y"), no que diz respeito à sua API, sua superclasse e subclasse são objetos idênticos (pois todos expõem os mesmos métodos).

Pessoalmente, eu definiria algumas constantes que representam cada tipo de peça e, em seguida, usaria uma instrução switch para ramificar em métodos privados que retornam possíveis movimentos para "this" instância.

leo
fonte
O movimento para trás é ilegal apenas em peões. E eles realmente não precisam saber se são pretos ou brancos - é suficiente (e lógico) para fazê-los distinguir para frente versus para trás. A Boardturma (por exemplo) pode se encarregar de converter esses conceitos em -1 ou +1, dependendo da cor da peça.
Konrad Morawski
0

Pessoalmente, eu não herdaria nada Piece, porque acho que você não ganha nada com isso. Presumivelmente, uma peça não se importa com a maneira como ela se move, apenas quais quadrados são um destino válido para a sua próxima jogada.

Talvez você possa criar uma Calculadora de Movimentos Válidos que conheça os padrões de movimento disponíveis para um tipo de peça e seja capaz de recuperar uma lista de quadrados de destino válidos, por exemplo

interface IMoveCalculator
{
    List<Square> GetValidDestinations();
}

class KnightMoveCalculator : IMoveCalculator;
class QueenMoveCalculator : IMoveCalculator;
class PawnMoveCalculator : IMoveCalculator; 
// etc.

class Piece
{
    IMoveCalculator move_calculator;
public:
    Piece(IMoveCalculator mc) : move_calculator(mc) {}
}
Ben Cottrell
fonte
Mas quem determina qual peça recebe qual calculadora de movimento? Se você tivesse uma classe base de peças e tivesse PawnPiece, poderia fazer essa suposição. Mas, então, a essa taxa, a peça em si poderia apenas implementar como ele se move, como projetado na pergunta original
Steven Striga
@WeekendWarrior - "Pessoalmente, eu não herdaria nada Piece". Parece que Piece é responsável por mais do que simplesmente calcular movimentos, que é a razão para dividir o movimento como uma preocupação separada. Determinar qual peça fica com qual calculadora seria uma questão de injeção de dependência, por exemplovar my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell
Este seria um bom candidato para o padrão de peso mosca. Há 16 peões em um tabuleiro de xadrez e todos compartilham o mesmo comportamento . Esse comportamento pode ser facilmente extraído em um único peso de mosca. O mesmo vale para todas as outras peças.
precisa saber é o seguinte
-2

Nesta resposta, estou assumindo que, por "mecanismo de xadrez", o autor da pergunta significa "IA de xadrez", não apenas um mecanismo de validação de movimento.

Eu acho que usar OOP para peças e seus movimentos válidos é uma má idéia por dois motivos:

  1. A força de um mecanismo de xadrez depende muito da rapidez com que ele pode manipular os tabuleiros . Considerando o nível de otimização descrito neste artigo, não acho que uma chamada de função regular seja acessível. Uma chamada para um método virtual adicionaria um nível de indireção e incorreria em uma penalidade de desempenho ainda mais alta. O custo do POO não é acessível por lá.
  2. Os benefícios do POO, como a extensibilidade, são inúteis para as peças, pois o xadrez provavelmente não receberá novas peças tão cedo. Uma alternativa viável à hierarquia de tipos baseada em herança inclui o uso de enums e switch. Muito de baixa tecnologia e tipo C, mas difícil de superar em termos de desempenho.

Afastar-me das considerações de desempenho, incluindo a cor da peça na hierarquia de tipos, é na minha opinião uma má ideia. Você se encontrará em situações em que precisa verificar se duas peças têm cores diferentes, por exemplo, para capturar. A verificação dessa condição é feita facilmente usando uma propriedade Fazer isso usando is WhitePiece-tests seria mais feio em comparação.

Há também outra razão prática para evitar a geração de movimentos de movimentação para métodos específicos de peças, a saber, que peças diferentes compartilham movimentos comuns, por exemplo, torres e rainhas. Para evitar código duplicado, você teria que fatorar o código comum em um método base e ter mais uma chamada dispendiosa.

Dito isto, se é o primeiro mecanismo de xadrez que você está escrevendo, eu recomendaria definitivamente seguir princípios de programação limpos e evitar os truques de otimização descritos pelo autor de Crafty. Lidar com as especificidades das regras do xadrez (rolagem, promoção, passante) e técnicas complexas de IA (hashing boards, corte alfa-beta) já é bastante desafiador.

Joh
fonte
11
Pode não ter a intenção de escrever um mecanismo de xadrez muito mais rápido.
precisa saber é o seguinte
5
-1. Julgamentos como "hipoteticamente poderia se tornar um problema de desempenho, por isso é ruim", são IMHO quase sempre pura superstição. E herança não é apenas "extensibilidade" no sentido em que você a menciona. Regras de xadrez diferentes se aplicam a peças diferentes, e cada tipo de peça com uma implementação diferente de um método WhichMovesArePossibleé uma abordagem razoável.
Doc Brown
2
Se você não precisar de extensibilidade, é preferível uma abordagem baseada em switch / enum, porque é mais rápida que um envio de método virtual. Um mecanismo de xadrez é pesado para a CPU, e as operações que são feitas com mais freqüência (e, portanto, são críticas à eficiência) estão executando movimentos e desfazendo-os. Apenas um nível acima que lista todos os movimentos possíveis. Espero que isso também seja crítico em termos de tempo. O fato é que eu programei máquinas de xadrez em C. Mesmo sem OOP, as otimizações de baixo nível na representação da placa foram significativas. Que tipo de experiência você tem para dispensar a minha?
Joh
2
@Joh: Não vou desrespeitar sua experiência, mas tenho certeza de que não foi isso que o OP buscou. E embora as otimizações de baixo nível possam ser significativas para vários problemas, considero um erro torná-las uma base para decisões de design de alto nível - especialmente quando alguém não enfrenta nenhum problema de desempenho até o momento. Minha experiência é que Knuth estava muito certo ao dizer "Otimização prematura é a raiz de todo mal".
Doc Brown
11
-1 Eu tenho que concordar com a doc. O fato é que esse "mecanismo" do qual o OP está falando poderia ser simplesmente o mecanismo de validação para um jogo de xadrez para 2 jogadores. Nesse caso, pensar no desempenho de OOP versus qualquer outro estilo é completamente ridículo, validações simples de momentos levariam milissegundos, mesmo em um dispositivo ARM de gama baixa. Não leia mais os requisitos do que realmente é indicado.
precisa saber é o seguinte