Dada uma manada de cavalos, como encontro o comprimento médio de chifres de todos os unicórnios?

30

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 HornMeasurerpelo 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?

moarboilerplate
fonte
22
Os cavalos não têm chifres, então a média é indefinida (0/0).
21416 Scott Whitlock
3
@moarboilerplate De 10 a infinito.
Nanny
4
@ StephenP: Isso não funcionaria matematicamente para este caso; todos esses 0s distorceriam a média.
Mason Wheeler
3
Se sua pergunta for melhor respondida com uma não resposta, ela não pertence a um site de perguntas e respostas; reddit, quora ou outros sites baseados em discussão são criados para coisas do tipo não-resposta ... dito isso, acho que pode ser claramente responsável se você estiver procurando o código que o @MasonWheeler deu, se não, acho que não tenho ideia o que você está tentando perguntar ..
Jimmy Hoffa
3
@JimmyHoffa "você está fazendo errado" passa a ser uma "não resposta" aceitável e, muitas vezes, melhor do que "bem, aqui está uma maneira de fazer isso" - nenhuma discussão prolongada é necessária.
moarboilerplate

Respostas:

11

Supondo que você queira tratar um Unicorntipo especial Horse, 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 com Unicorncaracterí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):

case class Horse(val hornLength: Option[Double])

val horse = Horse(None)
val unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))

val herd = List(horse, unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size

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 flatMapfiltrar facilmente os Noneitens.

Karl Bielefeldt
fonte
7
Obviamente, isso pressupõe que a única diferença entre um cavalo comum e um unicórnio é o chifre. Se não for esse o caso, as coisas ficam muito mais complicadas muito rapidamente.
Mason Wheeler
@MasonWheeler apenas no segundo método apresentado.
moarboilerplate
11
Veja os comentários sobre como os não-unicórnios e unicórnios nunca devem ser escritos juntos em um cenário de herança até que você esteja em um contexto em que não se importe com unicórnios. Claro, .OfType () pode resolver o problema e fazer as coisas funcionarem, mas está resolvendo um problema que nem deveria existir em primeiro lugar. Quanto à segunda abordagem, ela funciona porque as opções são muito superiores a depender de null para implicar algo. Eu acho que a segunda abordagem pode ser alcançada no OO com um compromisso se você encapsular as características do unicórnio em uma propriedade independente e for extremamente vigilante.
moarboilerplate
11
comprometa-se se você encapsular as características do unicórnio em uma propriedade independente e for extremamente vigilante - por que tornar a vida difícil para si mesmo? Use typeof diretamente e economize vários problemas futuros.
Gbjbaanb
@gbjbaanb Eu consideraria essa abordagem realmente realmente apropriada para cenários em que uma anêmica Horsetinha uma IsUnicornpropriedade e algum tipo de UnicornStuffpropriedade com o comprimento da buzina (ao escalar para o piloto / glitter mencionado em sua pergunta).
moarboilerplate
38

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.

Mason Wheeler
fonte
Por favor, perdoe-me se minha sintaxe lambda não estiver correta; Eu não sou muito codificador de C # e nunca consigo manter detalhes misteriosos como esse diretamente. Deve ficar claro o que eu quero dizer, no entanto.
Mason Wheeler
11
Não se preocupe, o problema está praticamente resolvido quando a lista contém apenas Unicorns de qualquer maneira (para o registro que você pode omitir return).
moarboilerplate
4
Esta é a resposta que eu procuraria se quisesse resolver o problema rapidamente. Mas não é a resposta se eu quiser refatorar o código para ser mais plausível.
Andy Andy
6
Essa é definitivamente a resposta, a menos que você precise de um nível absurdo de otimização. A clareza e legibilidade disso fazem praticamente todo o resto discutível.
David diz Restabelecer Monica
11
@DavidGrinberg, e se escrever este método limpo e legível significasse que você primeiro teria que implementar uma estrutura de herança que antes não existia?
moarboilerplate
9

Não há nada errado no .NET com:

var unicorn = animal as Unicorn;
if(unicorn != null)
{
    sum += unicorn.HornLength;
    count++;
}

Usar o equivalente Linq também é bom:

var averageUnicornHornLength = animals
    .OfType<Unicorn>()
    .Select(x => x.HornLength)
    .Average();

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:

var averageHornedAnimalHornLength = animals
    .OfType<IHornedAnimal>()
    .Select(x => x.HornLength)
    .Average();

Observe que, ao usar o Linq, Average(e Mine Max) 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:

var hornedAnimals = animals
    .OfType<IHornedAnimal>()
    .ToList();
if(hornedAnimals.Count > 0)
{
    var averageHornLengthOfHornedAnimals = hornedAnimals
        .Average(x => x.HornLength);
}
else
{
    // deal with it in your own way...
}

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.

Scott Whitlock
fonte
7
Você já sabe que animal é um Unicorn ; apenas faça a conversão em vez de usar asou use potencialmente ainda melhor as e verifique se há nulo.
Philip Kendall
3

Mas o fato de você finalmente interrogar o tipo de um objeto em tempo de execução não fica bem comigo.

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, typeofpor 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 ifdeclaraçã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)

gbjbaanb
fonte
Em um contexto legado, 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 e horn > 0sã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"
moarboilerplate
@moarboilerplate você diz você mesmo, escolhe a solução mais fácil e barata e ela se transforma em uma confusão. É por isso que as linguagens OO foram inventadas, como uma solução para esse tipo de problema. cavalo de subclasse pode parecer caro a princípio, mas logo se paga. Continuar com a solução simples, mas enlameada, custa cada vez mais com o tempo.
Gbjbaanb
3

Como a pergunta tem uma functional-programmingetiqueta, 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 #:

type Equine =
| Horse
| Unicorn of hornLength: float

module equines =

  let averageHornLength (equines : Equine list) =
    equines 
    |> List.choose (fun x -> 
      match x with
      | Unicorn u -> Some(u)
      | _ -> None)
    |> List.average

let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]

printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5

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 Equineapareçam um dia.

guillaume31
fonte
2

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

Horse.visit(EquineVisitor v)

A interface do visitante eqüino se parece com isso em java / c #

interface EquineVisitor {
  void visitHorse(Horse z);
  void visitUnicorn(Unicorn z);
}

Unicorn.visit(EquineVisitor v){
   v.visitUnicorn(this);
}

Horse.visit(EquineVisitor v){
   v.visitHorse(this);
}

Para medir chifres agora escrevemos ....

class HornMeasurer implements EquineVistor {
    void visitHorse(Horse h){} // ignore horses
    void visitUnicorn(Unicorn u){
         double len = u.getHornLength();
         totalLength+=len;
         unicornCount++;
    }

    double getAverageLength(){
          return totalLength/unicornCount;
    }

    double totalLength=0;
    int unicornCount=0;
}

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.

Tim Williscroft
fonte
Eu estava prestes a responder com o padrão de visitantes. Tive que rolar uma maneira surpreendente de descobrir se alguém já havia mencionado isso!
precisa saber é o seguinte
0

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 Horseonde alguns deles podem estar Unicorn, 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:

foreach (var horse in horses)
{
    try
    {
        var length = horse.MeasureHorn();
        //...
    }
    catch (NoHornException e)
    {
        continue;
    }
}

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 ifparece 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:

class Horse
{
    public double MeasureHorn() { return -1; }
    //...
}

class Unicorn : Horse
{
    public override double MeasureHorn { return _hornLength; }
    //...
}

Agora sua Horseturma precisa conhecer a Unicornturma e ter métodos extras para lidar com coisas que não lhe interessam. Agora imagine que você também tem Pegasuss e Zebras que herdam Horse. Agora Horseprecisa de um Flymétodo, bem como MeasureWings, CountStripes, etc. E então a Unicornclasse 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 Horses para dizer se algo é a Unicorne 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> unicornsque 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 como List<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.

Becuzz
fonte
A exceção silenciosa é definitivamente ruim - minha proposta era uma verificação que seria if(horse.IsUnicorn) horse.MeasureHorn();e as exceções não seriam capturadas - elas seriam acionadas quando !horse.IsUnicorne você estiver em um contexto de medição de unicórnios ou dentro MeasureHornde 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.
moarboilerplate
0

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:

  1. Os valores desses tipos são representações ou descrições de cavalos e unicórnios (respectivamente);
  2. 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:

  1. 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;
  2. 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 é:

  1. Seu modelo precisa ter um Horsetipo;
  2. O Horsetipo precisa codificar informações suficientes para determinar sem ambiguidade se algum valor descreve um unicórnio;
  3. 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;
  4. 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).

sacundim
fonte
-1

O comentário do OP em outra resposta esclareceu a pergunta, pensei

isso também faz parte do que a pergunta está fazendo. Se eu tenho um rebanho de cavalos, e alguns deles são conceitualmente unicórnios, como eles deveriam existir para que o problema possa ser resolvido de maneira limpa, sem muitos impactos negativos?

Assim formulado, acho que precisamos de mais informações. A resposta provavelmente depende de várias coisas:

  • Nossas instalações linguísticas. Por exemplo, eu provavelmente abordaria isso de forma diferente em ruby, javascript e Java.
  • Os próprios conceitos: O que é um cavalo e o que é um unicórnio? Quais dados estão associados a cada um? Eles são exatamente os mesmos, exceto a buzina, ou eles têm outras diferenças?
  • De que outra forma os estamos usando, além de calcular as médias de comprimento das buzinas? E os rebanhos? Talvez devêssemos modelá-los também? Nós os usamos em outro lugar? herd.averageHornLength()parece combinar com o nosso modelo conceitual.
  • Como os objetos cavalo e unicórnio estão sendo criados? A alteração desse código está dentro dos limites de nossa refatoração?

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 umhornLength() 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 ...

Jonah
fonte
11
Pontos justos. Para evitar que o problema se torne ainda mais abstrato, precisamos fazer algumas suposições razoáveis: 1) uma linguagem fortemente tipada 2) o rebanho restringe os cavalos a um tipo, provavelmente devido a uma coleção 3) técnicas como a tipagem de patos provavelmente devem ser evitadas . Quanto ao que pode ser alterado, não são necessariamente quaisquer limitações, mas cada tipo de mudança tem suas próprias conseqüências únicas ...
moarboilerplate
Se o rebanho restringe cavalos a um tipo, não são nossa única herança de escolhas (não gosta dessa opção) ou um objeto wrapper (digamos HerdMember) que inicializamos com um cavalo ou um unicórnio (liberando o cavalo e o unicórnio da necessidade de um relacionamento de subtipo ) HerdMemberfica livre para implementar da maneira isUnicorn()que achar melhor, e a solução de filtragem que sugiro a seguir.
Jonah
Em alguns idiomas, hornLength () pode ser misturado e, se for esse o caso, pode ser uma solução válida. No entanto, em idiomas em que a digitação é menos flexível, é necessário recorrer a algumas técnicas hackeadas para fazer a mesma coisa, ou algo como colocar o comprimento do chifre em um cavalo, onde isso pode causar confusão no código, porque um cavalo não ' conceitualmente, tem chifres. Além disso, se a fazer cálculos matemáticos, incluindo valores padrão pode distorcer os resultados (ver comentários sob pergunta original)
moarboilerplate
Mixins, porém, a menos que estejam sendo executados, são apenas herança com outro nome. Seu comentário "um cavalo não tem chifres conceitualmente" refere-se ao meu comentário sobre a necessidade de saber mais sobre o que são, se nossa resposta precisar incluir como modelamos cavalos e unicórnios e qual é a relação deles. Qualquer solução que inclua valores padrão está imediatamente errada.
Jonah
Você está certo de que, para obter uma solução precisa para uma manifestação específica desse problema, precisa ter muito contexto. Para responder sua pergunta sobre um cavalo com um chifre e ligá-lo a mixins, eu estava pensando em um cenário em que um comprimento de chifre misturado a um cavalo que não é um unicórnio é um erro. Considere uma característica do Scala que possui uma implementação padrão para hornLength que gera uma exceção. Um tipo de unicórnio pode substituir essa implementação e, se um cavalo chegar a um contexto em que hornLength é avaliado, é uma exceção.
moarboilerplate
-2

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.

Martin Maat
fonte
Eu concordo com isto. Mason Wheeler também tem uma boa solução em sua resposta, mas se você precisar destacar unicórnios por muitas razões diferentes em lugares diferentes, seu código terá muitas horses.ofType<Unicorn>...construções. Ter uma GetUnicornsfunção seria uma linha, mas seria ainda mais resistente a mudanças na relação cavalo / unicórnio da perspectiva do chamador.
Shaz
@Ryan Se você devolver um 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 no Horse. Se esse Unicornfor o seu próprio tipo, você precisará criar um novo tipo e manter os mapeamentos de tipos, o que pode gerar sobrecarga.
moarboilerplate
11
@moarboilerplate: Consideramos tudo isso favorável à solução. A parte da beleza é que é independente de qualquer detalhe de implementação do unicórnio. Quer você discrimine com base em um membro de dados, classe ou hora do dia (todos esses cavalos podem se transformar em unicórnios à meia-noite, se a lua estiver certa para tudo o que sei), a solução permanece, a interface permanece a mesma.
Martin Maat