Quando usar genéricos no design de interface

11

Tenho algumas interfaces que pretendo que terceiros implementem no futuro e forneço uma implementação básica. Vou usar apenas alguns para mostrar o exemplo.

Atualmente, eles são definidos como

Item:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

Agora, Iteme ItemStackFactoryposso absolutamente prever que terceiros precisem estendê-lo no futuro. ItemStackContainertambém pode ser estendido no futuro, mas não das maneiras que posso prever, fora da implementação padrão fornecida.

Agora, estou tentando tornar essa biblioteca o mais robusta possível; isso ainda está nos estágios iniciais (pré-pré-alfa), portanto pode ser um ato de excesso de engenharia (YAGNI). Este é um local apropriado para usar genéricos?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

E

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

Receio que isso possa tornar as implementações e o uso mais difíceis de ler e entender; Eu acho que é a recomendação para evitar aninhar genéricos sempre que possível.

Zymus
fonte
1
Parece que você está expondo os detalhes da implementação de qualquer maneira. Por que o chamador precisa trabalhar e conhecer / se importar com pilhas, em vez de quantidades simples?
cHao 1/09/15
Isso é algo que eu não tinha pensado. Isso fornece uma boa visão sobre esse problema específico. Vou fazer as alterações no meu código.
Zymus 01/09/2015

Respostas:

9

Você usa genéricos em sua interface quando é provável que sua implementação também seja genérica.

Por exemplo, qualquer estrutura de dados que possa aceitar objetos arbitrários é um bom candidato para uma interface genérica. Exemplos: List<T>e Dictionary<K,V>.

Qualquer situação em que você queira melhorar a segurança de tipo de maneira generalizada é um bom candidato para genéricos. A List<String>é uma lista que opera apenas em cadeias.

Qualquer situação em que você deseja aplicar o SRP de maneira generalizada e evitar métodos específicos de tipo é um bom candidato para genéricos. Por exemplo, um PersonDocument, PlaceDocument e ThingDocument podem ser substituídos por um Document<T>.

O padrão Abstract Factory é um bom caso de uso para um genérico, enquanto um Método Factory comum simplesmente cria objetos que são herdados de uma interface comum e concreta.

Robert Harvey
fonte
Eu realmente não concordo com a idéia de que interfaces genéricas devem ser combinadas com implementações genéricas. Declarar um parâmetro de tipo genérico em uma interface e refiná-lo ou instancia-lo em várias implementações é uma técnica poderosa. É especialmente comum em Scala. Interfaces coisas abstratas; classes especializá-los.
Benjamin Hodgson
@BenjaminHodgson: isso significa apenas que você está produzindo implementações em uma interface genérica para tipos específicos. A abordagem geral ainda é genérica, embora um pouco menos.
Robert Harvey
Concordo. Talvez eu tenha entendido mal a sua resposta - você parecia estar desaconselhando esse tipo de design.
Benjamin Hodgson
@BenjaminHodgson: Um método Factory não seria escrito contra uma interface concreta , em vez de uma interface genérica? (embora um Abstract Factory seria genérico)
Robert Harvey
Acho que estamos concordando :) Eu provavelmente escreveria o exemplo do OP como algo parecido class StackFactory { Stack<T> Create<T>(T item, int count); }. Posso escrever um exemplo do que eu quis dizer com "refinar um parâmetro de tipo em uma subclasse" em uma resposta, se você desejar mais esclarecimentos, embora a resposta esteja relacionada tangencialmente à pergunta original.
Benjamin Hodgson
8

Seu plano de como introduzir generalidade em sua interface parece estar correto para mim. No entanto, sua pergunta sobre se isso seria uma boa ideia ou não requer uma resposta um pouco mais complicada.

Ao longo dos anos, entrevistei vários candidatos a cargos de Engenharia de Software em nome de empresas nas quais trabalhei e percebi que a maioria dos candidatos a emprego lá fora se sente confortável com o uso de classes genéricas (por exemplo, o classes de coleção padrão), mas não com a gravação de classes genéricas. A maioria nunca escreveu uma única classe genérica em sua carreira e parece que estaria completamente perdida se fosse solicitada a escrever uma. Portanto, se você forçar o uso de genéricos a alguém que queira implementar sua interface ou estender suas implementações básicas, poderá estar deixando de lado as pessoas.

O que você pode tentar, no entanto, é fornecer versões genéricas e não genéricas de suas interfaces e implementações básicas, para que as pessoas que não fazem genéricos possam viver felizes em seu mundo estreito e pesado, enquanto as pessoas que genéricos podem colher os benefícios de sua compreensão mais ampla do idioma.

Mike Nakis
fonte
3

Agora, Iteme ItemStackFactoryposso absolutamente prever que terceiros precisem estendê-lo no futuro.

A decisão é sua. Se você não fornecer genéricos, eles precisarão atualizar sua implementação Itemsempre que usarem:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

Você deve pelo menos tornar ItemStackgenérico da maneira que sugere. O benefício mais profundo dos genéricos é que você quase nunca precisa transmitir nada, e oferecer código que força os usuários a transmitir é IMHO um crime.

OldCurmudgeon
fonte
2

Uau ... Eu tenho uma opinião completamente diferente sobre isso.

Eu posso prever absolutamente alguma necessidade de terceiros

Isto é um erro. Você não deve escrever código "para o futuro".

Além disso, as interfaces são escritas de cima para baixo. Você não olha para uma classe e diz "Ei, essa classe poderia implementar totalmente uma interface e então eu posso usá-la em outro lugar também". Essa é uma abordagem errada, porque apenas a classe que usa uma interface sabe realmente o que a interface deve ser. Em vez disso, você deve estar pensando "Nesse ponto, minha classe precisa de uma dependência. Deixe-me definir uma interface para essa dependência. Quando terminar de escrever essa classe, escreverei uma implementação dessa dependência". Se alguém reutilizará sua interface e substituirá sua implementação padrão pela própria é totalmente irrelevante. Eles podem nunca fazer. Isso não significa que a interface foi inútil. Faz com que seu código siga o princípio Inversion of Control, que torna seu código extensível em muitos lugares "

anton1980
fonte
0

Eu acho que o uso de genéricos na sua situação é muito complexo.

Se você escolher a versão genérica das interfaces, o design indicará que

  1. ItemStackContainer <A> contém um único tipo de pilha, A. (ou seja, ItemStackContainer não pode conter vários tipos de pilha.)
  2. O ItemStack é dedicado a um tipo específico de item. Portanto, não há tipo de abstração de todos os tipos de pilhas.
  3. Você precisa definir um ItemStackFactory específico para cada tipo de item. É considerado muito bom como padrão de fábrica.
  4. Escreva um código mais difícil, como ItemStack <? estende Item>, diminuindo a manutenção.

Essas suposições e restrições implícitas são coisas muito importantes para se decidir com cuidado. Portanto, especialmente nos estágios iniciais, esses projetos prematuros não devem ser introduzidos. Simples, fácil de entender, design comum é desejado.

Akio Takahashi
fonte
0

Eu diria que depende principalmente dessa pergunta: se alguém precisar estender a funcionalidade de Iteme ItemStackFactory,

  • eles simplesmente sobrescrevem métodos e, assim, as instâncias de suas subclasses serão usadas exatamente como as classes base, apenas se comportarão de maneira diferente?
  • ou eles adicionarão membros e usarão as subclasses de maneira diferente?

No primeiro caso, não há necessidade de genéricos; no segundo, provavelmente existe.

Michael Borgwardt
fonte
0

Talvez você possa ter uma hierarquia de interfaces, onde Foo é uma interface e Bar é outra. Sempre que um tipo deve ser ambos, é um FooAndBar.

Para evitar excesso de desgin, faça apenas o tipo FooAndBar quando necessário e mantenha uma lista de interfaces que nunca estenderão nada.

Isso é muito mais confortável para a maioria dos programadores (a maioria gosta da verificação de tipo ou nunca gosta de digitar estática de qualquer tipo). Muito poucas pessoas escreveram sua própria aula de genéricos.

Também como nota: as interfaces são sobre quais ações possíveis podem ser executadas. Na verdade, eles devem ser verbos, não substantivos.

Akash Patel
fonte
Essa pode ser uma alternativa ao uso de genéricos, mas a pergunta original perguntou sobre quando usar os genéricos sobre o que você sugeriu. Você pode melhorar sua resposta para sugerir que os genéricos não são mais necessários, mas o uso de múltiplas interfaces é a resposta para todos?
Jay Elston