Supondo uma linguagem com alguma segurança de tipo inerente (por exemplo, não JavaScript):
Dado um método que aceita a SuperType
, sabemos que, na maioria dos casos, podemos ser tentados a executar testes de tipo para escolher uma ação:
public void DoSomethingTo(SuperType o) {
if (o isa SubTypeA) {
o.doSomethingA()
} else {
o.doSomethingB();
}
}
Normalmente, se não sempre, devemos criar um método único e substituível no SuperType
e fazer o seguinte:
public void DoSomethingTo(SuperType o) {
o.doSomething();
}
... em que cada subtipo recebe sua própria doSomething()
implementação. O restante de nosso aplicativo pode então ser adequadamente ignorante se qualquer dado SuperType
é realmente um SubTypeA
ou um SubTypeB
.
Maravilhoso.
Mas ainda temos is a
operações semelhantes na maioria, se não em todas, linguagens de tipo seguro. E isso sugere uma necessidade potencial de teste explícito de tipo.
Então, em que situações, se houver, devemos ou devemos executar testes explícitos de tipo?
Perdoe minha falta de espírito ou falta de criatividade. Eu sei que já fiz isso antes; mas, honestamente, faz muito tempo que não me lembro se o que fiz foi bom! E, na memória recente, acho que não encontrei a necessidade de testar tipos fora do JavaScript do meu cowboy.
fonte
Respostas:
"Nunca" é a resposta canônica para "quando o teste de tipo está bom?" Não há como provar ou refutar isso; faz parte de um sistema de crenças sobre o que faz "bom design" ou "bom design orientado a objetos". Também é hokum.
Para ter certeza, se você tem um conjunto integrado de classes e também mais de uma ou duas funções que precisam desse tipo de teste direto de tipo, provavelmente você ESTÁ FAZENDO ERRADO. O que você realmente precisa é de um método implementado de maneira diferente
SuperType
e em seus subtipos. Isso é parte integrante da programação orientada a objetos, e toda a razão pela qual classes e herança existem.Nesse caso, o teste de tipo explicitamente está errado, não porque o teste de tipo é inerentemente errado, mas porque a linguagem já possui uma maneira limpa, extensível e idiomática de realizar a discriminação de tipo, e você não a usou. Em vez disso, você voltou a um idioma primitivo, frágil e não extensível.
Solução: Use o idioma. Como você sugeriu, adicione um método a cada uma das classes e deixe que a herança padrão e os algoritmos de seleção de método determinem qual caso se aplica. Ou, se você não pode alterar os tipos de base, subclasse e adicione seu método lá.
Tanta coisa para a sabedoria convencional e algumas respostas. Alguns casos em que o teste explícito de tipo faz sentido:
É único. Se você teve muita discriminação de tipo, pode estender os tipos ou subclasses. Mas você não. Você tem apenas um ou dois lugares onde precisa de testes explícitos; portanto, não vale a pena voltar e trabalhar na hierarquia de classes para adicionar as funções como métodos. Ou não vale a pena o esforço prático para adicionar o tipo de generalidade, testes, revisões de projeto, documentação ou outros atributos das classes base para um uso tão simples e limitado. Nesse caso, adicionar uma função que faz testes diretos é racional.
Você não pode ajustar as classes. Você pensa em subclassificar - mas não pode. Muitas classes em Java, por exemplo, são designadas
final
. Você tenta inserir umpublic class ExtendedSubTypeA extends SubTypeA {...}
e o compilador diz, sem incerteza, que o que você está fazendo não é possível. Desculpe, graça e sofisticação do modelo orientado a objetos! Alguém decidiu que você não pode estender seus tipos! Infelizmente, muitas das bibliotecas padrão sãofinal
, e fazer aulasfinal
é uma orientação comum de design. Uma execução final de função é o que fica disponível para você.BTW, isso não se limita aos idiomas estaticamente digitados. Linguagem dinâmica O Python possui várias classes base que, sob as coberturas implementadas em C, não podem realmente ser modificadas. Como o Java, isso inclui a maioria dos tipos padrão.
Seu código é externo. Você está desenvolvendo classes e objetos provenientes de vários servidores de banco de dados, mecanismos de middleware e outras bases de código que não podem ser controladas ou ajustadas. Seu código é apenas um consumidor humilde de objetos gerados em outros lugares. Mesmo se você pudesse subclasses
SuperType
, não poderá obter as bibliotecas das quais depende para gerar objetos em suas subclasses. Eles vão fornecer instâncias dos tipos que eles conhecem, não suas variantes. Isso nem sempre é o caso ... algumas vezes eles são criados para oferecer flexibilidade e instanciam dinamicamente instâncias de classes que você as alimenta. Ou eles fornecem um mecanismo para registrar as subclasses que você deseja que suas fábricas construam. Os analisadores XML parecem particularmente bons em fornecer esses pontos de entrada; veja por exemplo ou lxml em Python . Mas a maioria das bases de código não fornece essas extensões. Eles vão lhe devolver as aulas com as quais foram construídas e as que conhecem. Geralmente, não faz sentido proxyizar seus resultados nos resultados personalizados, apenas para que você possa usar um seletor de tipo puramente orientado a objetos. Se você vai fazer discriminação de tipo, precisará fazê-lo de forma relativamente grosseira. Seu código de teste de tipo parece bastante apropriado.Genéricos da pessoa pobre / despacho múltiplo. Você deseja aceitar uma variedade de tipos diferentes para o seu código e acha que ter uma matriz de métodos muito específicos do tipo não é agradável.
public void add(Object x)
Parece lógico, mas não um conjunto deaddByte
,addShort
,addInt
,addLong
,addFloat
,addDouble
,addBoolean
,addChar
, eaddString
variantes (para citar alguns). Tendo funções ou métodos que assumem um supertipo alto e determinam o que fazer, tipo a tipo - eles não ganharão o Purity Award no Simpósio anual de design Booch-Liskov, mas abandonarão o A nomeação húngara fornecerá uma API mais simples. De certo modo, vocêis-a
ouis-instance-of
o teste está simulando genérico ou multi-despacho em um contexto de linguagem que não o suporta nativamente.O suporte a idiomas integrados para genéricos e digitação de pato reduz a necessidade de verificação de tipo, tornando mais provável "fazer algo elegante e apropriado". A seleção múltipla de despacho / interface vista em idiomas como Julia e Go substituem similarmente o teste direto de tipo por mecanismos internos para a seleção baseada em tipo de "o que fazer". Mas nem todos os idiomas suportam isso. Java, por exemplo, geralmente é de despacho único, e seus idiomas não são super amigáveis para se digitar.
Mas mesmo com todos esses recursos de discriminação de tipo - herança, genéricos, tipagem de pato e despacho múltiplo - às vezes é conveniente ter uma rotina única e consolidada que faz com que você esteja fazendo algo com base no tipo do objeto claro e imediato. Na metaprogramação , achei essencialmente inevitável. Se o retorno às perguntas diretas do tipo constitui "pragmatismo em ação" ou "codificação suja" dependerá da sua filosofia e crenças de design.
fonte
(1.0).Equals(1.0f)
produzindo true [o argumento promovedouble
], mas(1.0f).Equals(1.0)
produzindo false [o argumento promoveobject
]; em Java,Math.round(123456789*1.0)
gera 123456789, masMath.round(123456789*1.0)
gera 123456792 [o argumento promove aofloat
invés dedouble
].Math.round
parecem idênticas para mim. Qual é a diferença?Math.round(123456789)
[indicativo do que pode acontecer se alguém reescritasMath.round(thing.getPosition() * COUNTS_PER_MIL)
para retornar um valor de posição sem escala, não percebendo quegetPosition
retorna umint
oulong
.]A principal situação que eu sempre precisei foi ao comparar dois objetos, como em um
equals(other)
método, que podem exigir algoritmos diferentes, dependendo do tipo exato deother
. Mesmo assim, é bastante raro.A outra situação que tive, novamente, muito raramente, é após a desserialização ou análise, onde às vezes você precisa que ele seja convertido com segurança para um tipo mais específico.
Além disso, às vezes você só precisa de um hack para contornar o código de terceiros que você não controla. É uma daquelas coisas que você realmente não deseja usar regularmente, mas está feliz por estar lá quando você realmente precisa.
fonte
BaseClass base = deserialize(input)
, porque ainda não conhece o tipo, eif (base instanceof Derived) derived = (Derived)base
armazena-o como seu tipo derivado exato.O caso padrão (mas espero que raro) se parece com o seguinte: se na seguinte situação
as funções
DoSomethingA
ouDoSomethingB
não podem ser facilmente implementadas como funções-membro da árvore de herança deSuperType
/SubTypeA
/SubTypeB
. Por exemplo, seDoSomethingXXX
a essa biblioteca significaria introduzir uma dependência proibida.Observe que geralmente há situações em que você pode contornar esse problema (por exemplo, criando um wrapper ou adaptador para
SubTypeA
eSubTypeB
ou tentandoDoSomething
reimplementar completamente em termos de operações básicas deSuperType
), mas às vezes essas soluções não valem o aborrecimento ou coisas mais complicadas e menos extensíveis do que fazer o teste de tipo explícito.Um exemplo do meu trabalho de ontem: eu tive uma situação em que eu iria paralelizar o processamento de uma lista de objetos (do tipo
SuperType
, com exatamente dois subtipos diferentes, onde é extremamente improvável que haja mais). A versão incomparável continha dois loops: um loop para objetos do subtipo A, chamadoDoSomethingA
, e um segundo loop para objetos do subtipo B, chamadoDoSomethingB
.Os métodos "DoSomethingA" e "DoSomethingB" são cálculos demorados, usando informações de contexto que não estão disponíveis no escopo dos subtipos A e B. (portanto, não faz sentido implementá-los como funções-membro dos subtipos). Do ponto de vista do novo "loop paralelo", torna as coisas muito mais fáceis ao lidar com elas de maneira uniforme, então implementei uma função semelhante à
DoSomethingTo
de cima. No entanto, examinar as implementações de "DoSomethingA" e "DoSomethingB" mostra que elas funcionam de maneira muito diferente internamente. Portanto, tentar implementar um "DoSomething" genérico, estendendo-seSuperType
com muitos métodos abstratos, não iria realmente funcionar, ou significaria projetar as coisas completamente.fonte
SuperType
e é subclasses?NSJSONSerialization
no Obj-C), mas não deseja simplesmente confiar que a resposta contém o tipo que você espera; portanto, antes de usá-lo, verifique-o (por exemploif ([theResponse isKindOfClass:[NSArray class]])...
) .Como o tio Bob chama:
Em um de seus episódios de Clean Coder, ele deu um exemplo de uma chamada de função usada para retornar
Employee
s.Manager
é um subtipo deEmployee
. Vamos supor que temos um serviço de aplicativo que aceita umManager
ID e o convoca para o escritório :) A funçãogetEmployeeById()
retorna um supertipoEmployee
, mas quero verificar se um gerente é retornado nesse caso de uso.Por exemplo:
Aqui, estou verificando se o funcionário retornado pela consulta é realmente um gerente (ou seja, espero que seja um gerente e, caso contrário, falhe rapidamente).
Não é o melhor exemplo, mas é tio Bob, afinal.
Atualizar
Atualizei o exemplo o máximo que me lembro de memória.
fonte
Manager
a implementação desummon()
apenas não lança a exceção neste exemplo?CEO
pode convocarManager
s.Employee
, ele deve se preocupar apenas em obter algo que se comporte como umEmployee
. Se diferentes subclasses deEmployee
têm permissões, responsabilidades, etc. diferentes, o que torna o teste de tipo uma opção melhor do que um sistema de permissões real?Nunca.
is
cláusula, ou (em alguns idiomas ou dependendo do cenário) porque você não pode estender o tipo sem modificar as partes internas da função fazendo ais
verificação.is
verificações são um forte sinal de que você está violando o Princípio de Substituição de Liskov . Qualquer coisa que funcioneSuperType
deve ser completamente ignorante de quais subtipos podem existir.Tudo isso dito, os
is
cheques podem ser menos ruins do que outras alternativas. Colocar todas as funcionalidades comuns em uma classe base é difícil e muitas vezes leva a problemas piores. Usar uma classe única que tenha uma flag ou enum para qual "tipo" a instância é ... é pior do que horrível, já que agora você está espalhando a evasão do sistema de tipos para todos os consumidores.Em resumo, você deve sempre considerar as verificações de tipo como um forte cheiro de código. Mas, como em todas as diretrizes, haverá momentos em que você será forçado a escolher entre qual violação de diretrizes é a menos ofensiva.
fonte
instanceof
vaza detalhes de implementação e quebra a abstração.IEnumerable<T>
não promete que um "último" elemento exista. Se o seu método precisar desse elemento, ele deverá exigir um tipo que garanta a existência de um. E os subtipos desse tipo podem fornecer implementações eficientes do método "Last".Se você possui uma grande base de código (mais de 100 mil linhas de código) e está perto do envio ou está trabalhando em uma filial que posteriormente precisará ser mesclada, há, portanto, muito custo / risco para alterar muitas situações.
Às vezes, você tem a opção de um grande refrator do sistema ou de um simples "teste de tipo" localizado. Isso cria uma dívida técnica que deve ser paga o mais rápido possível, mas geralmente não é.
(É impossível criar um exemplo, pois qualquer código pequeno o suficiente para ser usado como exemplo também é pequeno o suficiente para que o melhor design seja claramente visível.)
Ou, em outras palavras, quando o objetivo é receber seu salário em vez de obter "votos positivos" pela limpeza do seu projeto.
O outro caso comum é o código da interface do usuário, quando, por exemplo, você mostra uma interface do usuário diferente para alguns tipos de funcionários, mas claramente não deseja que os conceitos da interface do usuário escapem para todas as suas classes de "domínio".
Você pode usar o "teste de tipo" para decidir qual versão da interface do usuário exibir ou ter uma tabela de pesquisa sofisticada que converte de "classes de domínio" em "classes de interface do usuário". A tabela de pesquisa é apenas uma maneira de ocultar o "teste de tipo" em um só lugar.
(O código de atualização do banco de dados pode ter os mesmos problemas que o código da interface do usuário, no entanto, você costuma ter apenas um conjunto de códigos de atualização do banco de dados, mas pode ter várias telas diferentes que precisam se adaptar ao tipo de objeto que está sendo mostrado.)
fonte
A implementação do LINQ usa muita verificação de tipo para possíveis otimizações de desempenho e, em seguida, um fallback para IEnumerable.
O exemplo mais óbvio é provavelmente o método ElementAt (pequeno trecho da fonte .NET 4.5):
Mas há muitos lugares na classe Enumerable onde um padrão semelhante é usado.
Portanto, talvez otimizar o desempenho para um subtipo comumente usado seja um uso válido. Não tenho certeza de como isso poderia ter sido projetado melhor.
fonte
IEnumerable<T>
incluindo muitos métodos, como os deList<T>
, juntamente com umaFeatures
propriedade que indica quais métodos podem funcionar bem, devagar ou não, bem como várias suposições que um consumidor pode fazer com segurança sobre a coleção (por exemplo, é garantido que seu tamanho e / ou seu conteúdo existente nunca será alterado [um tipo pode suportar,Add
enquanto ainda garante que o conteúdo existente seria imutável]).Há um exemplo que aparece frequentemente no desenvolvimento de jogos, especificamente na detecção de colisões, que é difícil de lidar sem o uso de alguma forma de teste de tipo.
Suponha que todos os objetos do jogo derivem de uma classe base comum
GameObject
. Cada objecto tem uma forma de colisão corpo rígidoCollisionShape
o qual pode fornecer uma interface comum (para dizer posição de consulta, orientação, etc), mas as formas de colisão reais serão todas as subclasses de betão, tais comoSphere
,Box
,ConvexHull
, etc armazenar informação específica para esse tipo de objecto geométrico (veja aqui um exemplo real)Agora, para testar uma colisão, preciso escrever uma função para cada par de tipos de forma de colisão:
que contêm a matemática específica necessária para executar uma interseção desses dois tipos geométricos.
Em cada 'tick' do meu loop de jogo, preciso verificar pares de objetos em busca de colisões. Mas eu só tenho acesso a se
GameObject
seusCollisionShape
s correspondentes . Claramente, preciso conhecer tipos concretos para saber qual função de detecção de colisão chamar. Nem mesmo o envio duplo (que logicamente não é diferente de verificar o tipo) pode ajudar aqui *.Na prática, nessa situação, os mecanismos de física que eu vi (Bullet e Havok) dependem de testes de tipo de uma forma ou de outra.
Não estou dizendo que essa é necessariamente uma boa solução, é apenas que pode ser a melhor de um pequeno número de soluções possíveis para esse problema
* Tecnicamente, é possível usar o despacho duplo de uma maneira horrenda e complicada que exigiria combinações N (N + 1) / 2 (onde N é o número de tipos de formas que você possui) e apenas ofuscaria o que você está realmente fazendo. é descobrir simultaneamente os tipos das duas formas, por isso não considero que seja uma solução realista.
fonte
Às vezes, você não deseja adicionar um método comum a todas as classes, porque realmente não é responsabilidade deles executar essa tarefa específica.
Por exemplo, você deseja desenhar algumas entidades, mas não deseja adicionar o código de desenho diretamente a elas (o que faz sentido). Em idiomas que não oferecem suporte a vários despachos, você pode acabar com o seguinte código:
Isso se torna problemático quando esse código aparece em vários locais e você precisa modificá-lo em qualquer lugar ao adicionar um novo tipo de entidade. Se for esse o caso, pode ser evitado usando o padrão Visitor, mas, às vezes, é melhor manter as coisas simples e não fazer o overengineer. Essas são as situações em que o teste de tipo está OK.
fonte
A única vez que uso é em combinação com a reflexão. Mas mesmo assim a verificação dinâmica é principalmente, não codificada para uma classe específica (ou apenas codificada para classes especiais como
String
ouList
).Por verificação dinâmica, quero dizer:
e não codificado
fonte
Teste de tipo e conversão de tipo são dois conceitos muito próximos. Tão intimamente relacionado que me sinto confiante em dizer que você nunca deve fazer um teste de tipo, a menos que sua intenção seja digitar o objeto com base no resultado.
Quando você pensa no design ideal orientado a objetos, o teste de tipo (e a conversão) nunca deve acontecer. Mas, esperançosamente, até agora você já descobriu que a programação orientada a objetos não é o ideal. Às vezes, especialmente com código de nível inferior, o código não pode permanecer fiel ao ideal. Este é o caso de ArrayLists em Java; como eles não sabem em tempo de execução que classe está sendo armazenada na matriz, eles criam
Object[]
matrizes e as convertem estaticamente no tipo correto.Tem sido apontado que uma necessidade comum de teste de tipo (e conversão de tipo) vem do
Equals
método, que na maioria dos idiomas deve ser simplesObject
. A implementação deve ter algumas verificações detalhadas para verificar se os dois objetos são do mesmo tipo, o que exige ser capaz de testar qual o tipo.O teste de tipo também surge com frequência na reflexão. Freqüentemente, você terá métodos que retornam
Object[]
ou alguma outra matriz genérica e deseja retirar todos osFoo
objetos por qualquer motivo. Este é um uso perfeitamente legítimo de teste e conversão de tipo.Em geral, o teste de tipo é ruim quando associa desnecessariamente seu código à forma como uma implementação específica foi gravada. Isso pode facilmente levar à necessidade de um teste específico para cada tipo ou combinação de tipos, como se você deseja encontrar a interseção de linhas, retângulos e círculos, e a função de interseção possui um algoritmo diferente para cada combinação. Seu objetivo é colocar todos os detalhes específicos de um tipo de objeto no mesmo local que esse objeto, pois isso facilitará a manutenção e a extensão do seu código.
fonte
ArrayLists
não conhece a classe que está sendo armazenada em tempo de execução porque o Java não tinha genéricos e, quando foram introduzidos, a Oracle optou pela compatibilidade com versões anteriores com código sem genéricos.equals
tem o mesmo problema e é uma decisão questionável do projeto; comparações de igualdade não fazem sentido para todos os tipos.String x = (String) myListOfStrings.get(0)
Object
até que fosse acessado; Os genéricos em Java fornecem apenas a conversão implícita, protegida pelas regras do compilador.É aceitável em um caso em que você precise tomar uma decisão que envolva dois tipos e essa decisão seja encapsulada em um objeto fora da hierarquia de tipos. Por exemplo, digamos que você esteja planejando qual objeto será processado a seguir em uma lista de objetos aguardando processamento:
Agora, digamos que nossa lógica de negócios seja literalmente "todos os carros têm precedência sobre barcos e caminhões". A adição de uma
Priority
propriedade à classe não permite que você expresse essa lógica de negócios de maneira limpa, pois você terminará com isso:O problema é que agora, para entender a ordem de prioridade, é necessário examinar todas as subclasses ou, em outras palavras, você adicionou o acoplamento às subclasses.
Obviamente, você deve transformar as prioridades em constantes e colocá-las em uma classe, o que ajuda a manter o agendamento da lógica de negócios:
No entanto, na realidade, o algoritmo de agendamento é algo que pode mudar no futuro e, eventualmente, pode depender de mais do que apenas digitar. Por exemplo, pode-se dizer que "caminhões com um peso de 5000 kg têm prioridade especial sobre todos os outros veículos". É por isso que o algoritmo de agendamento pertence à sua própria classe, e é uma boa ideia inspecionar o tipo para determinar qual deles deve seguir primeiro:
Essa é a maneira mais direta de implementar a lógica de negócios e ainda a mais flexível para futuras mudanças.
fonte
null
, aString
ou aString[]
. Se 99% dos objetos precisarem de exatamente uma sequência, encapsular cada sequência em um construído separadamenteString[]
poderá adicionar uma sobrecarga considerável de armazenamento. O tratamento do caso de cadeia única usando uma referência direta aString
exigirá mais código, mas economizará armazenamento e poderá tornar as coisas mais rápidas.O teste de tipo é uma ferramenta, use-o com sabedoria e pode ser um aliado poderoso. Use mal e seu código começará a cheirar.
Em nosso software, recebemos mensagens pela rede em resposta a solicitações. Todas as mensagens desserializadas compartilhavam uma classe base comum
Message
.As classes em si eram muito simples, apenas a carga útil como as propriedades e rotinas C # digitadas para ordenar e remover a ordenação (na verdade, eu gerei a maioria das classes usando modelos t4 da descrição XML do formato da mensagem)
Código seria algo como:
Concedido, alguém poderia argumentar que a arquitetura da mensagem poderia ser melhor projetada, mas foi projetada há muito tempo e não para C #, por isso é o que é. Aqui, o teste de tipo resolveu um problema real para nós de uma maneira não muito pobre.
Vale ressaltar que o C # 7.0 está obtendo uma correspondência de padrões (que em muitos aspectos é o teste de tipo com esteróides) não pode ser tão ruim ...
fonte
Faça um analisador JSON genérico. O resultado de uma análise bem-sucedida é uma matriz, um dicionário, uma sequência, um número, um valor booleano ou nulo. Pode ser qualquer um desses. E os elementos de uma matriz ou os valores em um dicionário podem ser novamente qualquer um desses tipos. Como os dados são fornecidos de fora do seu programa, você deve aceitar qualquer resultado (ou seja, você deve aceitá-lo sem travar; você pode rejeitar um resultado que não é o que você espera).
fonte