Eu tenho uma classe base Base
,. Tem duas subclasses Sub1
e Sub2
. Cada subclasse possui alguns métodos adicionais. Por exemplo, Sub1
tem Sandwich makeASandwich(Ingredients... ingredients)
e Sub2
temboolean contactAliens(Frequency onFrequency)
.
Como esses métodos usam parâmetros diferentes e fazem coisas totalmente diferentes, eles são completamente incompatíveis, e não posso simplesmente usar o polimorfismo para resolver esse problema.
Base
fornece a maior parte da funcionalidade e tenho uma grande coleção de Base
objetos. No entanto, todos os Base
objetos são a Sub1
ou a Sub2
e, às vezes, preciso saber quais são.
Parece uma má idéia fazer o seguinte:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Então, criei uma estratégia para evitar isso sem vazamento. Base
agora possui estes métodos:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
E, é claro, Sub1
implementa esses métodos como
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
E Sub2
implementa da maneira oposta.
Infelizmente, agora Sub1
e Sub2
tenha esses métodos em sua própria API. Então eu posso fazer isso, por exemplo Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
Dessa forma, se o objeto é conhecido por ser apenas um Base
, esses métodos não são preteridos e podem ser usados para "converter" a si mesmo para um tipo diferente, para que eu possa invocar os métodos da subclasse. De certa forma, isso me parece elegante, mas, por outro lado, estou abusando das anotações obsoletas como uma maneira de "remover" os métodos de uma classe.
Como uma Sub1
instância realmente é uma Base, faz sentido usar herança em vez de encapsulamento. O que estou fazendo bem? Existe uma maneira melhor de resolver este problema?
fonte
instanceof
, de uma maneira que requer muita digitação, é propenso a erros e dificulta a adição de mais subclasses.Sub1
eSub2
não pode ser usado de forma intercambiável, por que você os trata como tal? Por que não acompanhar seus 'fabricantes de sanduíches' e 'alienígenas' separadamente?Respostas:
Nem sempre faz sentido adicionar funções à classe base, conforme sugerido em algumas das outras respostas. A adição de muitas funções de maiúsculas e minúsculas especiais pode resultar na ligação de componentes não relacionados.
Por exemplo, eu posso ter uma
Animal
classe, comCat
eDog
componentes. Se eu quiser imprimi-los ou mostrá-los na GUI, pode ser um exagero adicionarrenderToGUI(...)
e adicionarsendToPrinter(...)
à classe base.A abordagem que você está usando, usando verificações de tipo e elencos, é frágil - mas pelo menos mantém as preocupações separadas.
No entanto, se você se encontra fazendo esses tipos de verificações / transmissões com frequência, uma opção é implementar o padrão de visitante / expedição dupla para ele. Parece mais ou menos assim:
Agora seu código se torna
O principal benefício é que, se você adicionar uma subclasse, obterá erros de compilação em vez de casos ausentes silenciosamente. A nova classe de visitantes também se torna um bom alvo para atrair funções. Por exemplo, pode fazer sentido mudar
getRandomIngredients()
paraActOnBase
.Você também pode extrair a lógica de loop: por exemplo, o fragmento acima pode se tornar
Um pouco mais de massagem e uso de lambdas e streaming do Java 8 permitiriam que você
Qual IMO é o mais bonito e sucinto possível.
Aqui está um exemplo mais completo do Java 8:
fonte
Sub3
. O visitante não tem exatamente o mesmo problema queinstanceof
? Agora você precisa adicionaronSub3
ao visitante e ele quebra se você esquecer.onSub3
à interface do visitante, você recebe um erro de compilação em cada local que cria um novo Visitante que ainda não foi atualizado. Por outro lado, a ativação do tipo gera, na melhor das hipóteses, um erro em tempo de execução - que pode ser mais difícil de disparar.Da minha perspectiva: seu design está errado .
Traduzido para o idioma natural, você está dizendo o seguinte:
Dado que temos
animals
, existemcats
efish
.animals
tem propriedades que são comuns acats
efish
. Mas isso não é suficiente: existem algumas propriedades que diferemcat
defish
, portanto, você precisa subclassificar.Agora você tem o problema, que esqueceu de modelar o movimento . OK. Isso é relativamente fácil:
Mas esse é um design errado. A maneira correta seria:
Onde
move
seria o comportamento compartilhado implementado de forma diferente por cada animal.Isso significa: seu design está quebrado.
Minha recomendação: refatorar
Base
,Sub1
eSub2
.fonte
.move()
ou.attack()
e devidamente abstratothat
parte - em vez de ter.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
, etc. Como você refatorar corretamente? Desconhecido sem exemplos melhores ... mas ainda assim geralmente a resposta certa - a menos que o código de exemplo e mais detalhes sugiram o contrário.move
vsfly
/run
/ etc, isso pode ser explicado em uma frase como: Você deve dizer ao objeto o que fazer, não como fazê-lo. Em termos de acessadores, como você mencionou, é mais parecido com o caso real em um comentário aqui, você deve fazer perguntas ao objeto, não inspecionar seu estado.É um pouco difícil imaginar uma circunstância em que você tenha um grupo de coisas e queira que eles façam um sanduíche ou entrem em contato com alienígenas. Na maioria dos casos em que você encontra essa conversão, operará com um tipo - por exemplo, no clang, você filtra um conjunto de nós para declarações em que getAsFunction retorna não nulo, em vez de fazer algo diferente para cada nó da lista.
Pode ser que você precise executar uma sequência de ação e não é realmente relevante que os objetos que executam a ação estejam relacionados.
Então, em vez de uma lista de
Base
, trabalhe na lista de açõesOnde
Você pode, se apropriado, fazer com que o Base implemente um método para retornar a ação, ou qualquer outro meio necessário para selecionar a ação nas instâncias da sua lista base (já que você diz que não pode usar o polimorfismo, portanto, presumivelmente, a ação a ser tomada não é uma função da classe, mas alguma outra propriedade das bases; caso contrário, você apenas daria a Base o método act (Context)
fonte
Context
objeto só piora as coisas. Eu poderia estar apenas passandoObject
s para os métodos, lançando-os e esperando que sejam do tipo certo.Context
não precisa ser uma nova classe ou mesmo uma interface. Normalmente, o contexto é apenas o chamador, portanto, as chamadas estão no formulárioaction.act(this)
. SegetFrequency()
egetRandomIngredients()
são métodos estáticos, talvez você nem precise de um contexto. É assim que eu implementaria, digamos, uma fila de empregos, com a qual seu problema parece muito.E se suas subclasses implementarem uma ou mais interfaces que definem o que elas podem fazer? Algo assim:
Então suas aulas ficarão assim:
E seu loop for se tornará:
Duas grandes vantagens dessa abordagem:
PS: Desculpe pelos nomes das interfaces, não consegui pensar em nada mais legal naquele momento em particular: D.
fonte
A abordagem pode ser boa nos casos em que quase qualquer tipo dentro de uma família seja diretamente utilizável como implementação de alguma interface que atenda a algum critério ou possa ser usado para criar uma implementação dessa interface. Os tipos de coleção incorporados IMHO teriam se beneficiado com esse padrão, mas como eles não fazem isso para fins de exemplo, inventarei uma interface de coleção
BunchOfThings<T>
.Algumas implementações de
BunchOfThings
são mutáveis; alguns não são. Em muitos casos, um objeto que Fred pode querer conter algo que pode ser usado como umBunchOfThings
e sabe que nada além de Fred poderá modificá-lo. Este requisito pode ser atendido de duas maneiras:Fred sabe que ele tem as únicas referências a isso
BunchOfThings
, e nenhuma referência externa a ela ouBunchOfThings
seus internos existe em qualquer parte do universo. Se ninguém mais tiver uma referência a eleBunchOfThings
ou a seus internos, ninguém mais poderá modificá-lo, portanto a restrição será satisfeita.Nem o
BunchOfThings
, nem qualquer um de seus elementos internos para os quais existem referências externas, pode ser modificado por qualquer meio. Se absolutamente ninguém puder modificar aBunchOfThings
, a restrição será satisfeita.Uma maneira de satisfazer a restrição seria copiar incondicionalmente qualquer objeto recebido (processando recursivamente qualquer componente aninhado). Outra seria testar se um objeto recebido promete imutabilidade e, se não, fazer uma cópia dele e fazer o mesmo com qualquer componente aninhado. Uma alternativa, que pode ser mais limpa que a segunda e mais rápida que a primeira, é oferecer um
AsImmutable
método que solicite que um objeto faça uma cópia imutável de si mesmo (usandoAsImmutable
em qualquer componente aninhado que o suporte).Métodos relacionados também podem ser fornecidos
asDetached
(para uso quando o código recebe um objeto e não sabe se ele deseja modificá-lo; nesse caso, um objeto mutável deve ser substituído por um novo objeto mutável, mas um objeto imutável pode ser mantido como está),asMutable
(nos casos em que um objeto sabe que reterá um objeto retornado anteriormenteasDetached
, ou seja, uma referência não compartilhada a um objeto mutável ou uma referência compartilhável a um objeto mutável) easNewMutable
(nos casos em que o código recebe um objeto externo) referência e sabe que vai querer alterar uma cópia dos dados nele - se os dados recebidos forem mutáveis, não há razão para começar fazendo uma cópia imutável que será imediatamente usada para criar uma cópia mutável e depois abandonada).Observe que, embora os
asXX
métodos possam retornar tipos ligeiramente diferentes, sua função real é garantir que os objetos retornados atendam às necessidades do programa.fonte
Ignorando a questão de saber se você tem um bom design ou não, e supondo que seja bom ou pelo menos aceitável, eu gostaria de considerar os recursos das subclasses, não o tipo.
Portanto:
Mova algum conhecimento da existência de sanduíches e alienígenas para a classe base, mesmo sabendo que algumas instâncias da classe base não podem fazê-lo. Implemente-o na classe base para lançar exceções e altere o código para:
Em seguida, uma ou ambas as subclasses substituem
canMakeASandwich()
e apenas uma implementa cada uma demakeASandwich()
econtactAliens()
.Use interfaces, não subclasses concretas, para detectar quais recursos um tipo possui. Deixe a classe base em paz e altere o código para:
ou possivelmente (e sinta-se à vontade para ignorar essa opção se ela não se adequar ao seu estilo ou, se for o caso, a qualquer estilo Java que você considere sensato):
Pessoalmente, eu normalmente não gosto do último estilo de capturar uma exceção meia-esperada, devido ao risco de capturar inapropriadamente uma
ClassCastException
saída degetRandomIngredients
oumakeASandwich
, mas YMMV.fonte
<
é evitar a capturaArrayIndexOutOfBoundsException
, e algumas pessoas decidem fazer isso também. Sem explicar o sabor ;-) Basicamente, o meu "problema" aqui é o trabalho em Python, onde o estilo preferido geralmente é que capturar qualquer exceção é preferível a testar com antecedência se a exceção ocorrerá, por mais simples que seja o teste avançado . Talvez a possibilidade não valha a pena mencionar em Java.Aqui temos um caso interessante de uma classe base que faz downcasts para suas próprias classes derivadas. Sabemos muito bem que isso normalmente é ruim, mas se quisermos dizer que encontramos um bom motivo, vejamos e vejamos quais podem ser as restrições:
Se 4, temos: 5. A política da classe derivada está sempre sob o mesmo controle político que a política da classe base.
2 e 5 implicam diretamente que podemos enumerar todas as classes derivadas, o que significa que não deveria haver classes derivadas externas.
Mas aqui está a coisa. Se eles são todos seus, você pode substituir o if por uma abstração que é uma chamada de método virtual (mesmo que seja uma bobagem) e se livrar do if e da transmissão automática. Portanto, não faça isso. Um design melhor está disponível.
fonte
Coloque um método abstrato na classe base que faça uma coisa no Sub1 e o outro no Sub2 e chame esse método abstrato nos seus loops mistos.
Observe que isso pode exigir outras alterações, dependendo de como são definidos getRandomIngredients e getFrequency. Mas, honestamente, dentro da classe que entra em contato com alienígenas é provavelmente um lugar melhor para definir o getFrequency de qualquer maneira.
Aliás, seus métodos asSub1 e asSub2 são definitivamente uma prática ruim. Se você fizer isso, faça o casting. Não há nada que esses métodos lhe dê que a transmissão não o faça.
fonte
Você pode enviar ambos
makeASandwich
econtactAliens
para a classe base e depois implementá-los com implementações fictícias. Como os tipos / argumentos de retorno não podem ser resolvidos para uma classe base comum.Existem desvantagens óbvias nesse tipo de coisa, como o que isso significa para o contrato do método e o que você pode e não pode inferir sobre o contexto do resultado. Você não pode pensar que, desde que eu não fiz um sanduíche, os ingredientes foram usados na tentativa, etc.
fonte
O que você está fazendo é perfeitamente legítimo. Não preste atenção aos opositores que apenas reiteram o dogma porque o leem em alguns livros. Dogma não tem lugar na engenharia.
Empreguei o mesmo mecanismo algumas vezes e posso dizer com confiança que o tempo de execução do java também poderia ter feito a mesma coisa em pelo menos um local em que posso pensar, melhorando assim o desempenho, a usabilidade e a legibilidade do código que usa.
Tomemos, por exemplo
java.lang.reflect.Member
, qual é a basejava.lang.reflect.Field
ejava.lang.reflect.Method
. (A hierarquia real é um pouco mais complicada do que isso, mas isso é irrelevante.) Os campos e métodos são animais muito diferentes: um tem um valor que você pode obter ou definir, enquanto o outro não tem tal coisa, mas pode ser chamado com vários parâmetros e pode retornar um valor. Portanto, campos e métodos são membros, mas o que você pode fazer com eles é tão diferente um do outro quanto fazer sanduíches versus entrar em contato com alienígenas.Agora, ao escrever um código que usa reflexão, muitas vezes temos um
Member
em nossas mãos, e sabemos que é umMethod
ou umField
, (ou, raramente, outra coisa), e ainda temos que fazer todo o trabalhoinstanceof
para descobrir com precisão o que é e então temos que lançá-lo para obter uma referência adequada a ele. (E isso não é apenas tedioso, mas também não funciona muito bem.)Method
classe poderia facilmente implementar o padrão que você está descrevendo, facilitando assim a vida de milhares de programadores.Obviamente, essa técnica só é viável em hierarquias pequenas e bem definidas de classes intimamente acopladas das quais você tem (e sempre terá) controle no nível de fonte: você não deseja fazer isso se a hierarquia de classes for passível de ser estendido por pessoas que não têm liberdade para refatorar a classe base.
Aqui está como o que eu fiz difere do que você fez:
A classe base fornece uma implementação padrão para toda a
asDerivedClass()
família de métodos, fazendo com que cada um deles retornenull
.Cada classe derivada substitui apenas um dos
asDerivedClass()
métodos, retornando emthis
vez denull
. Ele não substitui o resto, nem deseja saber nada sobre eles. Então, nenhumIllegalStateException
s é jogado.A classe base também fornece
final
implementações para toda aisDerivedClass()
família de métodos, codificada da seguinte maneira:return asDerivedClass() != null;
Dessa forma, o número de métodos que precisam ser substituídos por classes derivadas é minimizado.Não uso
@Deprecated
esse mecanismo porque não pensei nisso. Agora que você me deu a idéia, vou usá-la, obrigado!O C # possui um mecanismo relacionado incorporado através do uso da
as
palavra - chave. Em C #, você pode dizerDerivedClass derivedInstance = baseInstance as DerivedClass
e obterá uma referência aDerivedClass
se vocêbaseInstance
era dessa classe ounull
se não era. Isso (teoricamente) tem um desempenho melhor do que ois
seguido pelo cast (is
é a palavra-chave C # reconhecidamente melhor nomeada parainstanceof
), mas o mecanismo personalizado para o qual trabalhamos à mão tem um desempenho ainda melhor: o par deinstanceof
operações de Java e elenco do Java também como oas
operador de C # não executa tão rápido quanto a única chamada de método virtual de nossa abordagem personalizada.Apresento a proposição de que essa técnica deve ser declarada como um padrão e que um bom nome deve ser encontrado para ela.
Puxa, obrigado pelos votos negativos!
Um resumo da controvérsia, para poupar o trabalho de ler os comentários:
A objeção das pessoas parece ser a de que o design original estava errado, o que significa que você nunca deve ter classes muito diferentes derivadas de uma classe base comum ou que, mesmo se o fizer, o código que usa essa hierarquia nunca deve estar na posição de ter uma referência básica e precisando descobrir a classe derivada. Portanto, eles dizem que o mecanismo de auto-projeção proposto por esta pergunta e por minha resposta, que melhora o uso do design original, nunca deveria ter sido necessário em primeiro lugar. (Eles realmente não dizem nada sobre o mecanismo de auto-projeção, apenas reclamam da natureza dos projetos aos quais o mecanismo deve ser aplicado.)
No entanto, no exemplo acima eu já mostraram que os criadores do Java runtime de fato escolher design uma precisão tal para o
java.lang.reflect.Member
,Field
,Method
hierarquia, e nos comentários abaixo eu também mostram que os criadores do C # runtime chegou de forma independente em um equivalente projeto para oSystem.Reflection.MemberInfo
,FieldInfo
,MethodInfo
hierarquia. Portanto, esses são dois cenários diferentes do mundo real, que estão bem debaixo do nariz de todos e que têm soluções comprovadamente viáveis usando exatamente esses designs.É para isso que todos os comentários a seguir se resumem. O mecanismo de auto-fundição é dificilmente mencionado.
fonte
java.lang.reflection.Member
hierarquia. Os criadores do Java Runtime entraram nesse tipo de situação, não acho que você diria que eles se projetaram em uma caixa ou os culpar por não seguirem algum tipo de prática recomendada? Portanto, o que estou dizendo aqui é que, depois de ter esse tipo de situação, esse mecanismo é uma boa maneira de melhorar as coisas.Member
interface, mas serve apenas para verificar os qualificadores de acesso. Normalmente, você obtém uma lista de métodos ou propriedades e a usa diretamente (sem misturá-las, para que nãoList<Member>
apareça), para que não seja necessário converter. Observe queClass
não fornece umgetMembers()
método. Obviamente, um programador sem noção pode criar oList<Member>
, mas isso faria tão pouco sentido quanto torná-lo umList<Object>
e adicionar algunsInteger
ouString
a ele.null
vez de uma exceção não o torna muito melhor. Sendo tudo igual, o visitante fica mais seguro ao fazer o mesmo trabalho.