Eu li que quando seu programa precisa saber qual classe é um objeto, geralmente indica uma falha de design, por isso quero saber qual é uma boa prática para lidar com isso. Estou implementando uma classe Shape com diferentes subclasses herdadas, como Circle, Polygon ou Rectangle e tenho algoritmos diferentes para saber se um Circle colide com um Polygon ou Rectangle. Suponhamos que temos duas instâncias de Shape e queremos saber se uma colide com a outra. Nesse método, devo inferir que tipo de subclasse é o objeto que estou colidindo para saber qual algoritmo devo chamar, mas esse é um mau design ou prática? Foi assim que resolvi.
abstract class Shape {
ShapeType getType();
bool collide(Shape other);
}
class Circle : Shape {
getType() { return Type.Circle; }
bool collide(Shape other) {
if(other.getType() == Type.Rect) {
collideCircleRect(this, (Rect) other);
} else if(other.getType() == Type.Polygon) {
collideCirclePolygon(this, (Polygon) other);
}
}
}
Esse é um padrão de design ruim? Como eu poderia resolver isso sem precisar inferir os tipos de subclasse?
design
object-oriented
class
Alejandro
fonte
fonte
Shape
objetos abstratos . Sua lógica depende das partes internas de outros objetos, a menos que você esteja checando pontos de limite de colisãobool collide(x, y)
(um subconjunto de pontos de controle pode ser uma boa escolha). Caso contrário, você precisará verificar o tipo de alguma forma - se houver realmente necessidade de abstrações, a produção deCollision
tipos (para objetos na área do ator atual) deve ser a abordagem correta.Respostas:
Polimorfismo
Contanto que você use
getType()
ou algo assim, você não está usando polimorfismo.Entendo que você precisa saber que tipo tem. Mas qualquer trabalho que você queira fazer sabendo que realmente deve ser empurrado para a classe. Então você apenas diz quando fazê-lo.
Este princípio é chamado de dizer, não pergunte . A seguir, ajuda a não espalhar detalhes como digitar e criar uma lógica que atue sobre eles. Fazer isso transforma uma classe de dentro para fora. É melhor manter esse comportamento dentro da classe para que ele possa mudar quando a classe mudar.
Encapsulamento
Você pode me dizer que nenhuma outra forma será necessária, mas eu não acredito em você e nem deveria.
Um bom efeito de seguir o encapsulamento é que é fácil adicionar novos tipos porque seus detalhes não se espalham no código em que aparecem
if
e naswitch
lógica. O código para um novo tipo deve estar em um só lugar.Um sistema de detecção de colisão ignorante
Deixe-me mostrar como eu projetaria um sistema de detecção de colisão com desempenho e funcione com qualquer formato 2D, não se importando com o tipo.
Digamos que você deveria desenhar isso. Parece simples. São todos círculos. É tentador criar uma classe de círculo que entenda colisões. O problema é que isso nos leva a uma linha de pensamento que desmorona quando precisamos de 1000 círculos.
Não deveríamos estar pensando em círculos. Deveríamos estar pensando em pixels.
E se eu disser a você que o mesmo código que você usa para desenhar esses caras é o que você pode usar para detectar quando eles tocam ou mesmo em quais deles o usuário está clicando.
Aqui, desenhei cada círculo com uma cor única (se seus olhos forem bons o suficiente para ver o contorno preto, apenas ignore). Isso significa que todos os pixels nesta imagem oculta são mapeados de volta para o que a atraiu. Um hashmap cuida disso muito bem. Você pode realmente fazer polimorfismo dessa maneira.
Essa imagem você nunca precisa mostrar ao usuário. Você o cria com o mesmo código que desenhou o primeiro. Apenas com cores diferentes.
Quando o usuário clica em um círculo, eu sei exatamente qual círculo, porque apenas um círculo é dessa cor.
Quando desenho um círculo em cima de outro, posso ler rapidamente todos os pixels que estou prestes a substituir, despejando-os em um conjunto. Quando terminei os pontos de ajuste para todos os círculos com os quais colidimos e agora só preciso ligar para cada um uma vez para notificá-lo da colisão.
Um novo tipo: Retângulos
Tudo isso foi feito com círculos, mas eu pergunto: seria diferente com retângulos?
Nenhum conhecimento de círculo vazou no sistema de detecção. Ele não se importa com raio, circunferência ou ponto central. Ele se importa com pixels e cores.
A única parte desse sistema de colisão que precisa ser empurrada para as formas individuais é uma cor única. Fora isso, as formas podem apenas pensar em desenhar suas formas. É no que eles são bons de qualquer maneira.
Agora, quando você escreve a lógica da colisão, não se importa com o subtipo que possui. Você diz para colidir e diz o que encontrou sob a forma que está pretendendo desenhar. Não há necessidade de saber o tipo. E isso significa que você pode adicionar quantos subtipos quiser, sem precisar atualizar o código em outras classes.
Opções de implementação
Realmente, não precisa ser de uma cor única. Pode ser referências a objetos reais e salvar um nível de indireção. Mas aqueles não pareceriam tão agradáveis quando desenhados nesta resposta.
Este é apenas um exemplo de implementação. Certamente existem outros. O que isso foi feito para mostrar é que, quanto mais próximo você deixar esses subtipos de formas ficarem com sua única responsabilidade, melhor o sistema inteiro funcionará. Provavelmente, existem soluções mais rápidas e com menos uso intensivo de memória, mas se elas me forçarem a espalhar o conhecimento dos subtipos, eu seria relutante em usá-las mesmo com os ganhos de desempenho. Eu não os usaria a menos que claramente precisasse deles.
Expedição dupla
Até agora, ignorei completamente o duplo envio . Eu fiz isso porque eu podia. Contanto que a lógica da colisão não se importe com os dois tipos colididos, você não precisa dela. Se você não precisar, não use. Se você acha que pode precisar, deixe de lidar com isso o máximo que puder. Essa atitude é chamada YAGNI .
Se você realmente precisar de diferentes tipos de colisões, pergunte a si mesmo se n subtipos de forma realmente precisam de 2 tipos de colisões. Até agora, trabalhei muito para facilitar a adição de outro subtipo de forma. Não quero estragar tudo com uma implementação de despacho duplo que força os círculos a saber que existem quadrados.
De qualquer forma, quantos tipos de colisões existem? Um pouco de especulação (coisa perigosa) inventa colisões elásticas (saltitantes), inelásticas (pegajosas), enérgicas (explosivas) e destrutivas (perigosas). Pode haver mais, mas se isso for menor que n 2, não vamos exagerar nossas colisões.
Isso significa que, quando meu torpedo atinge algo que aceita danos, não precisa SABER que atingiu uma nave espacial. Só precisa dizer: "Ha ha! Você sofreu 5 pontos de dano".
Coisas que causam dano enviam mensagens de dano para coisas que aceitam mensagens de dano. Feito dessa maneira, você pode adicionar novas formas sem informar as outras formas sobre a nova forma. Você acaba se espalhando por novos tipos de colisões.
A nave espacial pode enviar de volta para a rampa "Ha ha! Você sofreu 100 pontos de dano". bem como "Agora você está preso ao meu casco". E a torp pode enviar de volta "Bem, eu terminei, então esqueça de mim".
Em nenhum momento os dois sabem exatamente o que são. Eles apenas sabem como se comunicar através de uma interface de colisão.
Agora, com certeza, o envio duplo permite controlar as coisas mais intimamente do que isso, mas você realmente quer isso ?
Se você, pelo menos, pense em fazer o envio duplo através de abstrações de quais tipos de colisões uma forma aceita e não na implementação real da forma. Além disso, o comportamento de colisão é algo que você pode injetar como dependência e delegar a essa dependência.
atuação
O desempenho é sempre crítico. Mas isso não significa que é sempre um problema. Teste de desempenho. Não basta especular. Sacrificar tudo o mais em nome do desempenho geralmente não leva ao código de execução de qualquer maneira.
fonte
A descrição do problema parece que você deve usar os métodos multimídia (também conhecidos como despacho múltiplo), neste caso específico - despacho duplo . A primeira resposta foi longa sobre como lidar genericamente com formas em colisão na renderização de varredura, mas acredito que o OP queria uma solução "vetorial" ou talvez todo o problema tenha sido reformulado em termos de Shapes, que é um exemplo clássico das explicações de OOP.
Mesmo o artigo da wikipedia citado usa a mesma metáfora de colisão, deixe-me citar (o Python não possui métodos múltiplos, como em outras línguas):
Portanto, a próxima pergunta é como obter suporte para vários métodos na sua linguagem de programação.
fonte
Esse problema requer reprojeto em dois níveis.
Primeiro, você precisa extrair a lógica para detectar a colisão entre as formas. Isso significa que você não violaria o OCP toda vez que precisar adicionar uma nova forma ao modelo. Imagine que você já tenha Círculo, Quadrado e Retângulo definidos. Você poderia fazer assim:
Em seguida, você deve providenciar para que o método apropriado seja chamado, dependendo da forma que o chama. Você pode fazer isso usando polimorfismo e padrão de visitante . Para conseguir isso, precisamos ter o modelo de objeto apropriado em vigor. Primeiro, todas as formas devem aderir à mesma interface:
Em seguida, precisamos ter uma classe de visitante pai:
Estou usando uma classe aqui em vez da interface, porque preciso que cada objeto de visitante tenha um atributo do
ShapeCollisionDetector
tipoToda implementação de
IShape
interface instanciaria o visitante apropriado e chamaria oAccept
método apropriado do objeto com o qual o objeto de chamada interage, assim:E visitantes específicos ficariam assim:
Dessa forma, você não precisa alterar as classes de forma toda vez que adicionar uma nova forma e não precisa verificar o tipo de forma para chamar o método de detecção de colisão apropriado.
Uma desvantagem desta solução é que, se você adicionar uma nova forma, precisará estender a classe ShapeVisitor com o método dessa forma (por exemplo
VisitTriangle(Triangle triangle)
) e, consequentemente, precisará implementar esse método em todos os outros visitantes. No entanto, como essa é uma extensão, no sentido de que nenhum método existente é alterado, mas apenas o novo é adicionado, isso não viola o OCP e a sobrecarga de código é mínima. Além disso, usando a classeShapeCollisionDetector
, você evita a violação do SRP e a redundância de código.fonte
Seu problema básico é que, na maioria das linguagens de programação OO modernas, a sobrecarga de funções não funciona com ligação dinâmica (ou seja, o tipo de argumento da função é determinado em tempo de compilação). O que você precisaria é de uma chamada de método virtual virtual em dois objetos em vez de apenas um. Tais métodos são chamados de multi-métodos . No entanto, existem maneiras de emular esse comportamento em linguagens como Java, C ++, etc. É aqui que o despacho duplo é muito útil.
A idéia básica é que você use o polimorfismo duas vezes. Quando duas formas colidem, você pode chamar o método de colisão correto de um dos objetos através do polimorfismo e passar o outro objeto do tipo genérico de forma. No método chamado você, então saber se a esse objeto é um círculo, retângulo ou o que quer. Em seguida, você chama o método de colisão no objeto de forma passado e passa a este objeto. Essa segunda chamada encontra novamente o tipo de objeto correto através do polimorfismo.
Uma grande desvantagem dessa técnica, no entanto, é que cada classe na hierarquia precisa conhecer todos os irmãos. Isso gera uma alta carga de manutenção se uma nova forma for adicionada posteriormente.
fonte
Talvez essa não seja a melhor maneira de abordar esse problema
A colisão de formas do behing matemático é específica das combinações de formas. Significando que o número de sub-rotinas necessárias será o quadrado do número de formas suportadas pelo sistema. As colisões de formas não são realmente operações sobre formas, mas operações que assumem formas como parâmetros.
Estratégia de sobrecarga do operador
Se você não puder simplificar o problema matemático subjacente, recomendo a abordagem de sobrecarga do operador. Algo como:
No inicializador estático, usaria a reflexão para fazer um mapa dos métodos para implementar um dissipador di-dinâmico no método genérico de colisão (Shape s1, Shape s2). O intializer estático também pode ter uma lógica para detectar funções de colisão ausentes e relatá-las, recusando-se a carregar a classe.
É semelhante à sobrecarga do operador C ++. No C ++, a sobrecarga do operador é muito confusa porque você tem um conjunto fixo de símbolos que pode sobrecarregar. No entanto, o conceito é muito interessante e pode ser replicado com funções estáticas.
A razão pela qual eu usaria essa abordagem é que a colisão não é uma operação sobre um objeto. Uma colisão é uma operação externa que diz alguma relação sobre dois objetos arbitrários. Além disso, o inicializador estático poderá verificar se eu perdi alguma função de colisão.
Simplifique seu problema de matemática, se possível
Como mencionei, o número de funções de colisão é o quadrado do número de tipos de formas. Isso significa que em um sistema com apenas 20 formas você precisará de 400 rotinas, com 21 formas 441 e assim por diante. Isso não é facilmente extensível.
Mas você pode simplificar sua matemática . Em vez de estender a função de colisão, você pode rasterizar ou triangular todas as formas. Dessa forma, o mecanismo de colisão não precisa ser extensível. Colisão, Distância, Interseção, Mesclagem e várias outras funções serão universais.
Triangulate
Você notou a maioria dos pacotes e jogos em 3D triangular tudo? Essa é uma das formas de simplificar a matemática. Isso se aplica também às formas 2D. Polys podem ser triangulados. Círculos e splines podem ser aproximados a polígonos.
Mais uma vez ... você terá uma única função de colisão. Sua classe se torna então:
E suas operações:
Mais simples, não é?
Rasterizar
Você pode rasterizar sua forma para ter uma única função de colisão.
A rasterização pode parecer uma solução radical, mas pode ser acessível e rápida, dependendo da precisão das colisões de forma. Se eles não precisarem ser precisos (como em um jogo), você poderá ter bitmaps de baixa resolução. A maioria dos aplicativos não precisa de precisão absoluta na matemática.
Aproximações podem ser boas o suficiente. O supercomputador ANTON para simulação de biologia é um exemplo. Sua matemática descarta muitos efeitos quânticos que são difíceis de calcular e até agora as simulações feitas são consistentes com os experimentos feitos no mundo real. Os modelos de gráficos de computador PBR usados em mecanismos de jogos e pacotes de renderização simplificam a redução da potência do computador necessária para renderizar cada quadro. Na verdade, não é fisicamente preciso, mas está perto o suficiente para ser convincente a olho nu.
fonte