A pergunta acima é um exemplo abstrato de um problema comum que encontro no código legado, ou mais precisamente, problemas resultantes de tentativas anteriores de solucionar esse problema.
Posso pensar em pelo menos um método de estrutura .NET que se destina a solucionar esse problema, como o Enumerable.OfType<T>
método Mas o fato de você finalmente interrogar o tipo de um objeto em tempo de execução não fica bem comigo.
Além de perguntar a cada cavalo "Você é um unicórnio?" as seguintes abordagens também vêm à mente:
- Lance uma exceção quando for feita uma tentativa de obter o comprimento do chifre de um não-unicórnio (exponha a funcionalidade não apropriada para cada cavalo)
- Retorne um valor padrão ou mágico para o comprimento do chifre de um não-unicórnio (requer verificações padrão salpicadas em qualquer código que queira triturar estatísticas de chifres em um grupo de cavalos que poderiam ser todos não-unicórnios)
- Elimine a herança e crie um objeto separado em um cavalo que diga se o cavalo é um unicórnio ou não (o que está potencialmente empurrando o mesmo problema para uma camada)
Tenho a sensação de que isso será melhor respondido com uma "não resposta". Mas como você aborda esse problema e, se depender, qual é o contexto em torno de sua decisão?
Também gostaria de saber se esse problema ainda existe no código funcional (ou se existe apenas em linguagens funcionais compatíveis com mutabilidade?)
Isso foi sinalizado como uma possível duplicata da seguinte pergunta: Como evitar o downcasting?
A resposta a essa pergunta pressupõe que alguém esteja na posse de um HornMeasurer
pelo qual todas as medições de buzina devem ser feitas. Mas isso é uma imposição em uma base de código formada sob o princípio igualitário de que todos devem ser livres para medir o chifre de um cavalo.
Na ausência de a HornMeasurer
, a abordagem da resposta aceita reflete a abordagem baseada em exceção listada acima.
Também houve alguma confusão nos comentários sobre se cavalos e unicórnios são ambos eqüinos ou se um unicórnio é uma subespécie mágica de cavalo. Ambas as possibilidades devem ser consideradas - talvez uma seja preferível à outra?
fonte
Respostas:
Supondo que você queira tratar um
Unicorn
tipo especialHorse
, existem basicamente duas maneiras de modelá-lo. A maneira mais tradicional é o relacionamento da subclasse. Você pode evitar a verificação do tipo e do downcast simplesmente refatorando seu código para manter sempre as listas separadas nos contextos em que é importante e combiná-las apenas nos contextos em que você nunca se importa comUnicorn
características. Em outras palavras, você o organiza para nunca entrar na situação em que você precisa extrair unicórnios de um rebanho de cavalos. Isso parece difícil no começo, mas é possível em 99,99% dos casos e, geralmente, torna seu código muito mais limpo.A outra maneira de modelar um unicórnio é apenas dando a todos os cavalos um comprimento de chifre opcional. Então você pode testar se é um unicórnio, verificando se ele possui um comprimento de chifre e encontrar o comprimento médio de todos os unicórnios por (em Scala):
Esse método tem a vantagem de ser mais direto, com uma única classe, mas a desvantagem de ser muito menos extensível e ter uma maneira indireta de verificar a existência de "unicórnio". O truque se você optar por esta solução é reconhecer, quando começar a estendê-la com freqüência, que precisa mudar para uma arquitetura mais flexível. Esse tipo de solução é muito mais popular em linguagens funcionais, nas quais você tem funções simples e poderosas, como
flatMap
filtrar facilmente osNone
itens.fonte
Horse
tinha umaIsUnicorn
propriedade e algum tipo deUnicornStuff
propriedade com o comprimento da buzina (ao escalar para o piloto / glitter mencionado em sua pergunta).Você praticamente cobriu todas as opções. Se você tiver um comportamento dependente de um subtipo específico e estiver misturado com outros tipos, seu código deverá estar ciente desse subtipo; isso é simples raciocínio lógico.
Pessoalmente, eu apenas aceitaria
horses.OfType<Unicorn>().Average(u => u.HornLength)
. Ele expressa a intenção do código com muita clareza, o que geralmente é a coisa mais importante, já que alguém terá que mantê-lo mais tarde.fonte
Unicorn
s de qualquer maneira (para o registro que você pode omitirreturn
).Não há nada errado no .NET com:
Usar o equivalente Linq também é bom:
Com base na pergunta que você fez no título, este é o código que eu esperaria encontrar. Se a pergunta fizesse algo como "qual é a média de animais com chifres", isso seria diferente:
Observe que, ao usar o Linq,
Average
(eMin
eMax
) lançarão uma exceção se o enumerável estiver vazio e o tipo T não for anulável. Isso ocorre porque a média realmente é indefinida (0/0). Então, realmente, você precisa de algo assim:Editar
Eu só acho que isso precisa ser adicionado ... uma das razões pelas quais uma pergunta como essa não se encaixa bem com os programadores orientados a objetos é que ela assume que estamos usando classes e objetos para modelar uma estrutura de dados. A idéia original orientada a objetos no estilo Smalltalk era estruturar seu programa a partir de módulos que foram instanciados como objetos e executaram serviços para você quando você enviou uma mensagem. O fato de também podermos usar classes e objetos para modelar uma estrutura de dados é um efeito colateral (útil), mas são duas coisas diferentes. Eu nem acho que o último deva ser considerado programação orientada a objetos, já que você pode fazer a mesma coisa com a
struct
, mas isso não seria tão bonito.Se você estiver usando a programação orientada a objetos para criar serviços que fazem as coisas para você, em seguida, perguntar se esse serviço é realmente algum outro serviço ou implementação concreta é geralmente desaprovado por boas razões. Você recebeu uma interface (normalmente por injeção de dependência) e deve codificar para essa interface / contrato.
Por outro lado, se você estiver (mis-) usando as idéias de classe / objeto / interface para criar uma estrutura ou modelo de dados, pessoalmente não vejo problema em usar a idéia é ao máximo. Se você definiu que os unicórnios são um subtipo de cavalos e faz totalmente sentido em seu domínio, vá em frente e consulte os cavalos em seu rebanho para encontrar os unicórnios. Afinal, em um caso como esse, normalmente estamos tentando criar uma linguagem específica de domínio para expressar melhor as soluções dos problemas que temos que resolver. Nesse sentido, não há nada de errado com
.OfType<Unicorn>()
etc.Por fim, pegar uma coleção de itens e filtrá-la no tipo é realmente apenas programação funcional, não programação orientada a objetos. Felizmente, idiomas como C # estão à vontade para lidar com ambos os paradigmas agora.
fonte
animal
é umUnicorn
; apenas faça a conversão em vez de usaras
ou use potencialmente ainda melhoras
e verifique se há nulo.O problema com esta afirmação é que, não importa qual mecanismo você use, você sempre estará interrogando o objeto para saber qual é o tipo. Isso pode ser RTTI ou pode ser uma união ou uma estrutura de dados simples, onde você pergunta
if horn > 0
. As especificidades exatas mudam um pouco, mas a intenção é a mesma - você pergunta ao objeto sobre si mesmo de alguma maneira para ver se você deve interrogá-lo ainda mais.Dado isso, faz sentido usar o suporte do seu idioma para fazer isso. No .NET você usaria,
typeof
por exemplo.A razão para fazer isso vai além de apenas usar bem o seu idioma. Se você tem um objeto que se parece com outro, mas com pequenas alterações, é provável que encontre mais diferenças ao longo do tempo. No seu exemplo de unicórnios / cavalos, você pode dizer que há apenas o comprimento da buzina ... mas amanhã você estará verificando se um ciclista em potencial é virgem ou se o cocô é brilhante. (um exemplo clássico do mundo real seria os widgets da GUI que derivam de uma base comum e você deve procurar caixas de seleção e caixas de listagem de maneira diferente. O número de diferenças seria grande demais para simplesmente criar um único superobjeto que contenha todas as permutações possíveis de dados )
Se a verificação do tipo de um objeto em tempo de execução não for boa, sua alternativa é dividir os diferentes objetos desde o início - em vez de armazenar um único rebanho de unicórnios / cavalos, você tem duas coleções - uma para cavalos e outra para unicórnios . Isso pode funcionar muito bem, mesmo se você armazená-los em um contêiner especializado (por exemplo, um multimapa onde a chave é o tipo de objeto ... mas, mesmo que os armazenemos em 2 grupos, voltamos a interrogar o tipo de objeto !)
Certamente uma abordagem baseada em exceção está errada. Usar exceções como o fluxo normal do programa é um cheiro de código (se você tivesse uma manada de unicórnios e um burro com uma concha colada na cabeça, então eu diria que a abordagem baseada em exceções é boa, mas se você tem uma manada de unicórnios e os cavalos então verificando cada unicórnio não são inesperados. As exceções são para circunstâncias excepcionais, não para uma
if
declaração complicada ). De qualquer forma, o uso de exceções para esse problema é simplesmente interrogar o tipo de objeto em tempo de execução; somente aqui você está utilizando incorretamente o recurso de idioma para verificar se há objetos que não sejam unicórnios. Você também pode codificar em umif horn > 0
e pelo menos processe sua coleção de maneira rápida e clara, usando menos linhas de código e evitando problemas decorrentes de outras exceções (por exemplo, uma coleção vazia ou tentando medir a concha do mar)fonte
if horn > 0
é basicamente a maneira como esse problema é resolvido inicialmente. Em seguida, os problemas que geralmente surgem são quando você deseja checar os pilotos e o glitter ehorn > 0
são enterrados em todo o lugar em código não relacionado (também o código sofre bugs misteriosos devido à falta de checagens quando a buzina é 0). Além disso, subclassificar o cavalo depois do fato geralmente é a proposta mais cara, então geralmente não estou inclinado a fazê-lo se eles ainda estiverem reunidos no final do refator. Por isso certamente se torne "o quão feio são as alternativas"Como a pergunta tem uma
functional-programming
etiqueta, poderíamos usar um tipo de soma para refletir os dois sabores dos cavalos e a correspondência de padrões para desambiguar entre eles. Por exemplo, em F #:Na OOP, o FP tem a vantagem da separação de dados / funções, o que talvez o poupe da "consciência culpada" (injustificada?) De violar o nível de abstração ao fazer o downcast para subtipos específicos a partir de uma lista de objetos de um supertipo.
Em contraste com as soluções OO propostas em outras respostas, a correspondência de padrões também fornece um ponto de extensão mais fácil caso outras espécies Horned
Equine
apareçam um dia.fonte
Uma forma curta da mesma resposta no final requer a leitura de um livro ou artigo da web.
Padrão do visitante
O problema tem uma mistura de cavalos e unicórnios. (A violação do princípio de substituição de Liskov é um problema comum nas bases de código herdadas.)
Adicione um método ao cavalo e a todas as subclasses
A interface do visitante eqüino se parece com isso em java / c #
Para medir chifres agora escrevemos ....
O padrão de visitantes é criticado por dificultar a refatoração e o crescimento.
Resposta curta: Use o padrão de design Visitor para obter despacho duplo.
veja também https://en.wikipedia.org/wiki/Visitor_pattern
consulte também http://c2.com/cgi/wiki?VisitorPattern para discussão dos visitantes.
veja também Design Patterns de Gamma et al.
fonte
Supondo que, em sua arquitetura, os unicórnios sejam uma subespécie de cavalo e você encontre lugares onde você obtém uma coleção de
Horse
onde alguns deles podem estarUnicorn
, eu pessoalmente adotaria o primeiro método (.OfType<Unicorn>()...
) porque é a maneira mais direta de expressar sua intenção . Para quem aparece mais tarde (incluindo você mesmo em três meses), é imediatamente óbvio o que você está tentando realizar com esse código: escolha os unicórnios dentre os cavalos.Os outros métodos listados são apenas outra maneira de fazer a pergunta "Você é um unicórnio?". Por exemplo, se você usar algum tipo de método baseado em exceção para medir buzinas, poderá ter um código parecido com este:
Então agora a exceção se torna o indicador de que algo não é um unicórnio. E agora isso não é mais uma situação excepcional , mas faz parte do fluxo normal do programa. E usar uma exceção em vez de uma
if
parece ainda mais suja do que apenas fazer a verificação de tipo.Digamos que você siga a rota do valor mágico para verificar chifres em cavalos. Então agora suas aulas são mais ou menos assim:
Agora sua
Horse
turma precisa conhecer aUnicorn
turma e ter métodos extras para lidar com coisas que não lhe interessam. Agora imagine que você também temPegasus
s eZebra
s que herdamHorse
. AgoraHorse
precisa de umFly
método, bem comoMeasureWings
,CountStripes
, etc. E então aUnicorn
classe recebe estes métodos também. Agora, todas as suas classes precisam se conhecer e você poluiu as classes com vários métodos que não deveriam estar lá apenas para evitar perguntar ao sistema de tipos "Isso é um unicórnio?"Então, que tal acrescentar algo a
Horse
s para dizer se algo é aUnicorn
e lidar com todas as medições de buzina? Bem, agora você precisa verificar a existência desse objeto para saber se algo é um unicórnio (que substitui apenas um teste por outro). Ele também confunde um pouco as águas, pois agora você pode ter umList<Horse> unicorns
que realmente contém todos os unicórnios, mas o sistema de tipos e o depurador não podem lhe dizer isso facilmente. "Mas eu sei que são todos os unicórnios", você diz, "o nome ainda diz". Bem, e se algo tivesse um nome ruim? Ou, digamos, você escreveu algo com a suposição de que realmente seriam todos os unicórnios, mas os requisitos mudaram e agora também pode haver pegasi misturado? (Como nada disso acontece, especialmente no software / sarcasmo legado.) Agora, o sistema de tipos alegremente colocará seu pegasi nos seus unicórnios. Se sua variável tivesse sido declarada comoList<Unicorn>
o compilador (ou o ambiente de tempo de execução) seria adequado se você tentasse misturar pegasi ou cavalos.Finalmente, todos esses métodos são apenas um substituto para a verificação do sistema de tipos. Pessoalmente, eu preferiria não reinventar a roda aqui e espero que meu código funcione tão bem quanto algo incorporado e que tenha sido testado por milhares de outros codificadores milhares de vezes.
Por fim, o código precisa ser compreensível para você . O computador descobrirá, independentemente de como você o escreve. Você é quem deve depurá-lo e ser capaz de argumentar sobre isso. Faça a escolha que facilita seu trabalho. Se, por algum motivo, um desses outros métodos oferecer uma vantagem que supera o código mais claro nos dois pontos em que apareceria, siga em frente. Mas isso depende da sua base de código.
fonte
if(horse.IsUnicorn) horse.MeasureHorn();
e as exceções não seriam capturadas - elas seriam acionadas quando!horse.IsUnicorn
e você estiver em um contexto de medição de unicórnios ou dentroMeasureHorn
de um não-unicórnio. Dessa forma, quando a exceção é lançada, você não mascara os erros, ela explode completamente e é um sinal de que algo precisa ser corrigido. Obviamente, isso é apropriado apenas para certos cenários, mas é uma implementação que não usa lançamento de exceção para determinar um caminho de execução.Bem, parece que seu domínio semântico tem um relacionamento IS-A, mas você é um pouco cauteloso ao usar subtipos / herança para modelar isso - principalmente por causa da reflexão do tipo de tempo de execução. Porém, acho que você tem medo da coisa errada - a subtipagem realmente traz perigos, mas o fato de você estar consultando um objeto em tempo de execução não é o problema. Você verá o que eu quero dizer.
A programação orientada a objetos se apoiou bastante na noção de relacionamentos IS-A, sem dúvida se apoiou demais nela, levando a dois conceitos críticos famosos:
Mas acho que há outra maneira, mais baseada em programação funcional, de analisar os relacionamentos IS-A que talvez não tenham essas dificuldades. Primeiro, queremos modelar cavalos e unicórnios em nosso programa, portanto, teremos um
Horse
e umUnicorn
tipo. Quais são os valores desses tipos? Bem, eu diria o seguinte:Isso pode parecer óbvio, mas acho que uma das maneiras pelas quais as pessoas se envolvem em questões como o problema da elipse circular é não se importar com esses pontos com cuidado suficiente. Todo círculo é uma elipse, mas isso não significa que toda descrição esquematizada de um círculo é automaticamente uma descrição esquematizada de uma elipse de acordo com um esquema diferente. Em outras palavras, apenas porque um círculo é uma elipse não significa que a
Circle
é umEllipse
, por assim dizer. Mas isso significa que:Circle
(descrição do círculo esquematizado) em umEllipse
(tipo diferente de descrição) que descreve os mesmos círculos;Ellipse
e, se descreve um círculo, retorna o correspondenteCircle
.Portanto, em termos de programação funcional, seu
Unicorn
tipo não precisa ser um subtipoHorse
, você só precisa de operações como estas:E
toUnicorn
precisa ser o inverso correto detoHorse
:O
Maybe
tipo de Haskell é o que outros idiomas chamam de "opção". Por exemplo, oOptional<Unicorn>
tipo Java 8 é umUnicorn
ou nada. Observe que duas de suas alternativas - lançando uma exceção ou retornando um "valor padrão ou mágico" - são muito semelhantes aos tipos de opção.Então, basicamente, o que eu fiz aqui é reconstruir o conceito de relação IS-A em termos de tipos e funções, sem usar subtipos ou herança. O que eu tiraria disso é:
Horse
tipo;Horse
tipo precisa codificar informações suficientes para determinar sem ambiguidade se algum valor descreve um unicórnio;Horse
tipo precisam expor essas informações para que os clientes do tipo possam observar se um dadoHorse
é um unicórnio;Horse
tipo terão que usar essas últimas operações em tempo de execução para discriminar entre unicórnios e cavalos.Portanto, este é fundamentalmente um modelo "pergunte a todos
Horse
se é um unicórnio". Você é cauteloso com esse modelo, mas acho errado. Se eu lhe der uma lista deHorse
s, tudo o que o tipo garante é que as coisas que os itens da lista descrevem são cavalos - então você inevitavelmente precisará fazer algo em tempo de execução para dizer quais deles são unicórnios. Portanto, não há como contornar isso, eu acho - você precisa implementar operações que farão isso por você.Na programação orientada a objetos, a maneira familiar de fazer isso é a seguinte:
Horse
tipo;Unicorn
como um subtipo deHorse
;Horse
é umUnicorn
.Isso tem uma grande fraqueza, quando você olha para isso do ângulo "coisa versus descrição" que apresentei acima:
Horse
instância que descreva um unicórnio, mas não umaUnicorn
instância?Voltando ao início, acho que é a parte mais assustadora do uso de subtipagem e downcasts para modelar esse relacionamento IS-A - não o fato de que você precisa fazer uma verificação de tempo de execução. Abusar um pouco da tipografia, perguntar
Horse
se é umaUnicorn
instância não é sinônimo de perguntarHorse
se é um unicórnio (se é umaHorse
descrição de um cavalo que também é um unicórnio). A menos que seu programa tenha se esforçado bastante para encapsular o código que constrói, deHorses
modo que toda vez que um cliente tenta construir umHorse
que descreva um unicórnio, aUnicorn
classe é instanciada. Na minha experiência, raramente os programadores fazem isso com cuidado.Então, eu iria com a abordagem em que há uma operação explícita e não downcast que converte
Horse
s emUnicorn
s. Este poderia ser um método doHorse
tipo:... ou pode ser um objeto externo (seu "objeto separado em um cavalo que informa se o cavalo é um unicórnio ou não"):
A escolha entre eles é uma questão de como o seu programa está organizado - nos dois casos, você tem o equivalente da minha
Horse -> Maybe Unicorn
operação acima, apenas o empacotando de maneiras diferentes (que reconhecidamente terão efeitos negativos em quais operações oHorse
tipo precisa expor a seus clientes).fonte
O comentário do OP em outra resposta esclareceu a pergunta, pensei
Assim formulado, acho que precisamos de mais informações. A resposta provavelmente depende de várias coisas:
herd.averageHornLength()
parece combinar com o nosso modelo conceitual.Em geral, porém, eu nem pensaria em herança e subtipos aqui. Você tem uma lista de objetos. Alguns desses objetos podem ser identificados como unicórnios, talvez porque tenham um
hornLength()
método. Filtre a lista com base nessa propriedade exclusiva do unicórnio. Agora, o problema foi reduzido para calcular a média do comprimento dos chifres de uma lista de unicórnios.OP, deixe-me saber se ainda estou entendendo mal ...
fonte
HerdMember
) que inicializamos com um cavalo ou um unicórnio (liberando o cavalo e o unicórnio da necessidade de um relacionamento de subtipo )HerdMember
fica livre para implementar da maneiraisUnicorn()
que achar melhor, e a solução de filtragem que sugiro a seguir.Um método GetUnicorns () que retorna um IEnumerable parece a solução mais elegante, flexível e universal para mim. Dessa forma, você pode lidar com qualquer (combinação de) características que determinam se um cavalo passará como um unicórnio, não apenas o tipo de classe ou o valor de uma propriedade específica.
fonte
horses.ofType<Unicorn>...
construções. Ter umaGetUnicorns
função seria uma linha, mas seria ainda mais resistente a mudanças na relação cavalo / unicórnio da perspectiva do chamador.IEnumerable<Horse>
, embora seu critério de unicórnio esteja em um só lugar, ele está encapsulado, para que seus chamadores tenham que fazer suposições sobre o porquê precisam de unicórnios (eu posso obter ensopado de mariscos pedindo a sopa do dia hoje, mas isso não acontece '' significa que eu vou conseguir amanhã fazendo a mesma coisa). Além disso, você precisa expor um valor padrão para um chifre noHorse
. Se esseUnicorn
for o seu próprio tipo, você precisará criar um novo tipo e manter os mapeamentos de tipos, o que pode gerar sobrecarga.