Tudo bem ter objetos que se projetam, mesmo que polua a API de suas subclasses?


33

Eu tenho uma classe base Base,. Tem duas subclasses Sub1e Sub2. Cada subclasse possui alguns métodos adicionais. Por exemplo, Sub1tem Sandwich makeASandwich(Ingredients... ingredients)e Sub2temboolean 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.

Basefornece a maior parte da funcionalidade e tenho uma grande coleção de Baseobjetos. No entanto, todos os Baseobjetos são a Sub1ou a Sub2e, à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. Baseagora possui estes métodos:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

E, é claro, Sub1implementa 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 Sub1e Sub2tenha 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 Sub1instâ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?


12
Todas as três classes agora precisam se conhecer. Adicionando Sub3 envolveria uma série de mudanças de código e adicionando Sub10 seria absolutamente doloroso
Dan Pichelman

15
Ajudaria muito se você nos desse um código real. Há situações em que é apropriado tomar decisões com base na classe específica de algo, mas é impossível dizer se você está justificado no que está fazendo com esses exemplos inventados. Para o que vale a pena, o que você deseja é um Visitante ou uma união marcada .
Doval

1
Desculpe, pensei que facilitaria as coisas para dar exemplos simplificados. Talvez eu publique uma nova pergunta com um escopo mais amplo do que eu quero fazer.
Codebreaker

12
Você está apenas reimplementando a conversão e instanceof, de uma maneira que requer muita digitação, é propenso a erros e dificulta a adição de mais subclasses.
usar o seguinte comando

5
Se Sub1e Sub2nã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?
Pieter Witvoet

Respostas:


27

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 Animalclasse, com Cate Dogcomponentes. Se eu quiser imprimi-los ou mostrá-los na GUI, pode ser um exagero adicionar renderToGUI(...)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:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Agora seu código se torna

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

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

BaseVisitor.applyToArray(bases, new ActOnBase() );

Um pouco mais de massagem e uso de lambdas e streaming do Java 8 permitiriam que você

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Qual IMO é o mais bonito e sucinto possível.

Aqui está um exemplo mais completo do Java 8:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));

+1: esse tipo de coisa é comumente usado em linguagens com suporte de primeira classe para soma-tipos e correspondência de padrões, e é um design válido em Java, mesmo que seja sintaticamente muito feio.
Mankarse

@Mankarse Um pouco de açúcar sintático pode torná-lo muito longe de ser feio - eu o atualizei com um exemplo.
Michael Anderson

Digamos que, hipoteticamente, o OP mude de idéia e decida adicionar a Sub3. O visitante não tem exatamente o mesmo problema que instanceof? Agora você precisa adicionar onSub3ao visitante e ele quebra se você esquecer.
Radiodef 30/04

1
@Radiodef Uma vantagem de usar o padrão de visitante em vez de ativar o tipo diretamente é que, depois de adicionado 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.
Michael Anderson

1
@ FiveNine, se você tiver prazer em adicionar esse exemplo completo ao final da resposta que pode ajudar outras pessoas.
Michael Anderson

83

Da minha perspectiva: seu design está errado .

Traduzido para o idioma natural, você está dizendo o seguinte:

Dado que temos animals, existem catse fish. animalstem propriedades que são comuns a catse fish. Mas isso não é suficiente: existem algumas propriedades que diferem catdefish , portanto, você precisa subclassificar.

Agora você tem o problema, que esqueceu de modelar o movimento . OK. Isso é relativamente fácil:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Mas esse é um design errado. A maneira correta seria:

for(Animal a : animals){
    animal.move()
}

Onde moveseria o comportamento compartilhado implementado de forma diferente por cada animal.

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.

Isso significa: seu design está quebrado.

Minha recomendação: refatorar Base, Sub1e Sub2.


8
Se você oferecer um código real, eu poderia fazer recomendações.
Thomas Junk

9
@codebreaker Se você concorda que seu exemplo não foi bom, recomendo aceitar esta resposta e depois escrever uma nova pergunta. Não revise a pergunta para ter um exemplo diferente, ou as respostas existentes não farão sentido.
Moby Disk

16
@ codebreaker Acho que isso "resolve" o problema, respondendo à pergunta real . A verdadeira questão é: como resolvo meu problema? Refatore seu código corretamente. Refactor para que você .move()ou .attack()e devidamente abstrato thatparte - 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.
WernerCD

11
@codebreaker Se sua hierarquia de classe leva a situações como esta, então , por definição, não faz sentido
Patrick Collins

7
@codebreaker Relacionado ao último comentário de Crisfole, há outra maneira de ver isso que pode ajudar. Por exemplo, com o movevs fly/ 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.
Izkata

9

É 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ções

for (RandomAction action : actions)
   action.act(context);

Onde

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

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)


2
Você entende que meus métodos absurdos são projetados para mostrar as diferenças no que as classes podem fazer quando a subclasse é conhecida (e que elas não têm nada a ver uma com a outra), mas acho que ter um Contextobjeto só piora as coisas. Eu poderia estar apenas passando Objects para os métodos, lançando-os e esperando que sejam do tipo certo.
Codebreaker

1
@ codebreaker, você realmente precisa esclarecer sua pergunta então - na maioria dos sistemas, haveria uma razão válida para chamar várias funções de agnósticas ao que elas fazem; por exemplo, para processar regras em um evento ou executar uma etapa em uma simulação. Geralmente, esses sistemas têm um contexto no qual as ações ocorrem. Se 'getRandomIngredients' e 'getFrequency' não estiverem relacionados, não deverá estar no mesmo objeto e você precisará de uma abordagem diferente, como fazer com que as ações capturem a fonte de ingredientes ou a frequência.
Pete Kirkham

1
Você está certo e acho que não posso salvar essa pergunta. Acabei escolhendo um exemplo ruim, então provavelmente vou postar outro. O getFrequency () e o getIngredients () eram apenas espaços reservados. Eu provavelmente deveria ter colocado "..." como argumento para tornar mais aparente que eu não saberia o que são.
Codebreaker

Esta é a resposta certa da IMO. Entenda que o Contextnão precisa ser uma nova classe ou mesmo uma interface. Normalmente, o contexto é apenas o chamador, portanto, as chamadas estão no formulário action.act(this). Se getFrequency()e getRandomIngredients()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.
QuestionC

Além disso, isso é praticamente idêntico à solução de padrão de visitante. A diferença é que você está implementando os métodos Visitor :: OnSub1 () / Visitor :: OnSub2 () como Sub1 :: act () / Sub2 :: act ()
QuestionC:

4

E se suas subclasses implementarem uma ou mais interfaces que definem o que elas podem fazer? Algo assim:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Então suas aulas ficarão assim:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

E seu loop for se tornará:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Duas grandes vantagens dessa abordagem:

  • nenhum casting envolvido
  • você pode fazer com que cada subclasse implemente quantas interfaces desejar, o que fornece alguma flexibilidade para alterações futuras.

PS: Desculpe pelos nomes das interfaces, não consegui pensar em nada mais legal naquele momento em particular: D.


3
Na verdade, isso não resolve o problema. Você ainda precisa do elenco no loop for. (Caso contrário, você também não precisará do elenco no exemplo do OP).
Taemyr

2

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 BunchOfThingssão mutáveis; alguns não são. Em muitos casos, um objeto que Fred pode querer conter algo que pode ser usado como um BunchOfThingse sabe que nada além de Fred poderá modificá-lo. Este requisito pode ser atendido de duas maneiras:

  1. Fred sabe que ele tem as únicas referências a isso BunchOfThings, e nenhuma referência externa a ela ou BunchOfThingsseus internos existe em qualquer parte do universo. Se ninguém mais tiver uma referência a ele BunchOfThingsou a seus internos, ninguém mais poderá modificá-lo, portanto a restrição será satisfeita.

  2. 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 a BunchOfThings, 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 AsImmutablemé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 anteriormente asDetached, ou seja, uma referência não compartilhada a um objeto mutável ou uma referência compartilhável a um objeto mutável) e asNewMutable(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 asXXmétodos possam retornar tipos ligeiramente diferentes, sua função real é garantir que os objetos retornados atendam às necessidades do programa.


0

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:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Em seguida, uma ou ambas as subclasses substituem canMakeASandwich()e apenas uma implementa cada uma de makeASandwich()e contactAliens().


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:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

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

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Pessoalmente, eu normalmente não gosto do último estilo de capturar uma exceção meia-esperada, devido ao risco de capturar inapropriadamente uma ClassCastExceptionsaída de getRandomIngredientsou makeASandwich, mas YMMV.


2
Capturar ClassCastException é apenas ew. Esse é todo o objetivo da instância.
Radiodef 30/04

@ Radiodef: true, mas um dos objetivos <é evitar a captura ArrayIndexOutOfBoundsException, 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.
21715 Steve Jobs (

@SteveJessop »É mais fácil pedir perdão do que permissão« é o slogan;)
Thomas Junk

0

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:

  1. Podemos cobrir todos os casos na classe base.
  2. Nenhuma classe derivada estrangeira teria que adicionar um novo caso, jamais.
  3. Podemos viver com a política para a qual a chamada deve ficar sob o controle da classe base.
  4. Mas sabemos de nossa ciência que a política do que fazer em uma classe derivada para um método que não está na classe base é a da classe derivada e não a classe base.

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.


-1

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.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

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.


2
Sua resposta sugere que você não leu completamente a questão: o polimorfismo simplesmente não a aborda aqui. Existem vários métodos em cada um dos Sub1 e Sub2, estes são apenas exemplos, e "doAThing" não significa realmente nada. Não corresponde a nada que eu estaria fazendo. Além disso, quanto à sua última frase: e quanto à segurança do tipo garantido?
Codebreaker

@ codebreaker - Corresponde exatamente ao que você está fazendo no loop no exemplo. Se isso não tiver significado, não escreva o loop. Se não faz sentido como método, não faz sentido como corpo de loop.
usar o seguinte comando

1
E seus métodos "garantem" a segurança do tipo da mesma forma que a conversão: lançando uma exceção se o objeto for do tipo errado.
usar o seguinte código

Sei que escolhi um mau exemplo. O problema é que a maioria dos métodos das subclasses se parece mais com getters do que com métodos mutativos. Essas classes não fazem as próprias coisas, apenas fornecem dados, principalmente. O polimorfismo não vai me ajudar lá. Estou lidando com produtores, não com consumidores. Além disso: lançar uma exceção é uma garantia de tempo de execução, que não é tão boa quanto o tempo de compilação.
Codebreaker

1
Meu argumento é que seu código também fornece apenas uma garantia de tempo de execução. Não há nada que impeça alguém de verificar o isSub2 antes de chamar como Sub2.
usar o seguinte código

-2

Você pode enviar ambos makeASandwiche contactAlienspara 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.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

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.


1
Pensei em fazer isso, mas viola o Princípio de Substituição de Liskov e não é tão agradável trabalhar, por exemplo, quando você digita "sub1". e o preenchimento automático aparecer, você verá makeASandwich, mas não contactAliens.
Codebreaker

-5

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 base java.lang.reflect.Fieldejava.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 Memberem nossas mãos, e sabemos que é um Methodou um Field, (ou, raramente, outra coisa), e ainda temos que fazer todo o trabalho instanceofpara 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 retorne null.

  • Cada classe derivada substitui apenas um dos asDerivedClass()métodos, retornando em thisvez de null. Ele não substitui o resto, nem deseja saber nada sobre eles. Então, nenhum IllegalStateExceptions é jogado.

  • A classe base também fornece finalimplementações para toda a isDerivedClass()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 @Deprecatedesse 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 aspalavra - chave. Em C #, você pode dizer DerivedClass derivedInstance = baseInstance as DerivedClasse obterá uma referência a DerivedClassse você baseInstanceera dessa classe ou nullse não era. Isso (teoricamente) tem um desempenho melhor do que o isseguido pelo cast ( isé a palavra-chave C # reconhecidamente melhor nomeada para instanceof), mas o mecanismo personalizado para o qual trabalhamos à mão tem um desempenho ainda melhor: o par de instanceofoperações de Java e elenco do Java também como o asoperador 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 mostraram que os criadores do Java runtime de fato escolher design uma precisão tal para o java.lang.reflect.Member, Field, Methodhierarquia, e nos comentários abaixo eu também mostram que os criadores do C # runtime chegou de forma independente em um equivalente projeto para o System.Reflection.MemberInfo, FieldInfo, MethodInfohierarquia. 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.


13
É legítimo se você se projetou em uma caixa, com certeza. Esse é o problema de muitos desses tipos de perguntas. As pessoas tomam decisões em outras áreas do design que forçam esse tipo de solução imprudente quando a solução real é descobrir por que você acabou nessa situação em primeiro lugar. Um design decente não o levará até aqui. Atualmente, estou olhando para um aplicativo SLOC de 200K e não vejo nenhum lugar em que precisávamos fazer isso. Portanto, não é apenas algo que as pessoas leem em um livro. Não entrar nesse tipo de situação é simplesmente o resultado final de seguir as boas práticas.
Dunk

3
@ Dunk Acabei de mostrar que a necessidade de tal mecanismo existe, por exemplo, na java.lang.reflection.Memberhierarquia. 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.
35630 Mike Nakis #

6
@MikeNakis Os criadores de Java tomaram muitas decisões ruins, de modo que isso por si só não diz muito. De qualquer forma, usar um visitante seria melhor do que fazer um teste ad-hoc e depois lançar.
Doval

3
Além disso, o exemplo é falho. Existe uma Memberinterface, 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ão List<Member>apareça), para que não seja necessário converter. Observe que Classnão fornece um getMembers()método. Obviamente, um programador sem noção pode criar o List<Member>, mas isso faria tão pouco sentido quanto torná-lo um List<Object>e adicionar alguns Integerou Stringa ele.
SJuan76

5
@MikeNakis "Muitas pessoas fazem isso" e "Pessoas famosas fazem isso" não são argumentos reais. Antipadrões são idéias ruins e amplamente usadas por definição, mas essas desculpas justificariam seu uso. Dê razões reais para usar um design ou outro. Meu problema com a transmissão ad-hoc é que todos os usuários da sua API precisam fazer a transmissão certa sempre que o fazem. Um visitante precisa estar correto apenas uma vez. Ocultar o elenco por trás de alguns métodos e retornar em nullvez de uma exceção não o torna muito melhor. Sendo tudo igual, o visitante fica mais seguro ao fazer o mesmo trabalho.
Doval
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.