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 Horsee um Unicorntipo. Quais são os valores desses tipos? Bem, eu diria o seguinte:
- Os valores desses tipos são representações ou descrições de cavalos e unicórnios (respectivamente);
- São representações ou descrições esquematizadas - não são de forma livre, são construídas de acordo com regras muito estritas.
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é um Ellipse, por assim dizer. Mas isso significa que:
- Existe uma função total que converte qualquer
Circle(descrição do círculo esquematizado) em um Ellipse(tipo diferente de descrição) que descreve os mesmos círculos;
- Existe uma função parcial que pega um
Ellipsee, se descreve um círculo, retorna o correspondente Circle.
Portanto, em termos de programação funcional, seu Unicorntipo não precisa ser um subtipo Horse, você só precisa de operações como estas:
-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse
-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn
E toUnicornprecisa ser o inverso correto de toHorse:
toUnicorn (toHorse x) = Just x
O Maybetipo de Haskell é o que outros idiomas chamam de "opção". Por exemplo, o Optional<Unicorn>tipo Java 8 é um Unicornou 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 é:
- Seu modelo precisa ter um
Horsetipo;
- O
Horsetipo precisa codificar informações suficientes para determinar sem ambiguidade se algum valor descreve um unicórnio;
- Algumas operações do
Horsetipo precisam expor essas informações para que os clientes do tipo possam observar se um dado Horseé um unicórnio;
- Os clientes do
Horsetipo 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 Horsese é um unicórnio". Você é cauteloso com esse modelo, mas acho errado. Se eu lhe der uma lista de Horses, 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:
- Tenha um
Horsetipo;
- Ter
Unicorncomo um subtipo de Horse;
- Use a reflexão do tipo de tempo de execução como a operação acessível ao cliente que discerne se um dado
Horseé um Unicorn.
Isso tem uma grande fraqueza, quando você olha para isso do ângulo "coisa versus descrição" que apresentei acima:
- E se você tiver uma
Horseinstância que descreva um unicórnio, mas não uma Unicorninstâ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 Horsese é uma Unicorninstância não é sinônimo de perguntar Horsese é um unicórnio (se é uma Horsedescriçã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, de Horsesmodo que toda vez que um cliente tenta construir um Horseque descreva um unicórnio, a Unicornclasse é 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 Horses em Unicorns. Este poderia ser um método do Horsetipo:
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
... ou pode ser um objeto externo (seu "objeto separado em um cavalo que informa se o cavalo é um unicórnio ou não"):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
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 Unicornoperação acima, apenas o empacotando de maneiras diferentes (que reconhecidamente terão efeitos negativos em quais operações o Horsetipo precisa expor a seus clientes).