Prática boa ou ruim para mascarar coleções Java com nomes de classe significativos?

43

Ultimamente, tenho o hábito de "mascarar" coleções Java com nomes de classes amigáveis para humanos. Alguns exemplos simples:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

Ou:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Um colega apontou para mim que isso é uma prática ruim e introduz lag / latency, além de ser um antipadrão OO. Eu posso entender isso introduzindo um muito pequeno minúsculo grau de sobrecarga de desempenho, mas não consigo imaginar que seja significativo. Então eu pergunto: isso é bom ou ruim para fazer e por quê?

    
por herpylderp 27.06.2014 / 20:48
fonte

6 respostas

73

atraso / latência? Eu chamo BS nisso. Deve haver exatamente zero sobrecarga desta prática. ( Editar: Tem sido apontado nos comentários que isso pode, de fato, inibir as otimizações executadas pela VM HotSpot. Eu não sei o suficiente sobre a implementação da VM para confirmar ou negar isso. baseando meu comentário fora da implementação de C ++ de funções virtuais.)

Existe alguma sobrecarga de código. Você tem que criar todos os construtores da classe base que você deseja, encaminhando seus parâmetros.

Eu também não vejo isso como um anti-padrão, por si só. No entanto, vejo isso como uma oportunidade perdida. Em vez de criar uma classe que deriva a classe base apenas para renomear, que tal você criar uma classe que contenha a coleção e ofereça uma interface melhorada específica para cada caso? O seu cache de widgets deve realmente oferecer a interface completa de um mapa? Ou deveria oferecer uma interface especializada?

Além disso, no caso de coleções, o padrão simplesmente não funciona junto com a regra geral de usar interfaces, não implementações - ou seja, no código de coleção simples, você criaria um HashMap<String, Widget> e então o designaria para uma variável do tipo Map<String, Widget> . Seu WidgetCache não pode estender Map<String, Widget> , porque essa é uma interface. Não pode ser uma interface que estenda a interface base, porque HashMap<String, Widget> não implementa essa interface e nenhuma outra coleção padrão. E, embora você possa torná-lo uma classe que estenda HashMap<String, Widget> , você terá que declarar as variáveis como WidgetCache ou Map<String, Widget> , e a primeira perde a flexibilidade de substituir uma coleção diferente (talvez a coleção de carregamento lento de ORM) , enquanto o segundo tipo de derrota o ponto de ter a classe.

Alguns desses contrapontos também se aplicam à minha classe especializada proposta.

Estes são todos os pontos a serem considerados. Pode ou não ser a escolha certa. Em ambos os casos, os argumentos oferecidos pelo seu colega não são válidos. Se ele acha que é um anti-padrão, ele deve nomeá-lo.

    
por 27.06.2014 / 20:58
fonte
23

De acordo com a IBM , isso realmente é um antipadrão. Essas classes semelhantes a typedef são chamadas de tipos psuedo.

O artigo explica muito melhor do que eu, mas tentarei resumi-lo caso o link fique inativo:

  • Qualquer código que espera um WidgetCache não pode manipular um Map<String, Widget>
  • Esses pseudótipos são "virais" ao usar vários pacotes que levam a incompatibilidades, enquanto o tipo base (apenas um mapa bobo < ... >) funcionaria em todos os casos em todos os pacotes.
  • Os pseudo tipos costumam ser concretos, eles não implementam interfaces específicas porque suas classes base implementam apenas a versão genérica.

No artigo, eles propõem o seguinte truque para facilitar a vida sem usar pseudo tipos:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

O que funciona devido à inferência automática de tipos.

(cheguei a essa resposta por meio de esta questão de estouro de pilha relacionada

    
por 28.06.2014 / 10:37
fonte
13

O impacto no desempenho seria limitado, no máximo, a uma pesquisa vtable, que você provavelmente já está incorrendo. Essa não é uma razão válida para se opor a isso.

A situação é comum o suficiente para que a maioria das linguagens de programação estaticamente tipadas tenham uma sintaxe especial para os tipos de alias, geralmente chamados de typedef . Java provavelmente não os copiou porque originalmente não tinha tipos parametrizados. Estender uma turma não é o ideal, devido às razões pelas quais Sebastian abordou tão bem em sua resposta, mas pode ser uma solução razoável para a sintaxe limitada do Java.

Typedefs tem várias vantagens. Eles expressam a intenção do programador mais claramente, com um nome melhor, em um nível mais apropriado de abstração. Eles são mais fáceis de pesquisar para fins de depuração ou refatoração. Considere encontrar em todos os lugares onde um WidgetCache é usado versus encontrar esses usos específicos de um Map . Eles são mais fáceis de alterar, por exemplo, se mais tarde você achar que precisa de um LinkedHashMap ou até mesmo seu próprio contêiner personalizado.

    
por 27.06.2014 / 21:36
fonte
11

Sugiro que você, como já foi mencionado, use composição sobre herança, para que possa expor apenas métodos realmente necessários, com nomes que correspondam ao caso de uso pretendido. Os usuários da sua turma realmente precisam saber que WidgetCache é um mapa? E ser capaz de fazer o que quiserem? Ou eles só precisam saber que é um cache para widgets?

Exemplo de classe da minha base de código, com uma solução para um problema semelhante que você descreveu:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Você pode ver que internamente é "apenas um mapa", mas a interface pública não está mostrando isso. E tem métodos "amigáveis ao programador" como appendMessage(Locale locale, String code, String message) para uma maneira mais fácil e significativa de inserir novas entradas. E os usuários da classe não podem fazer, por exemplo, translations.clear() , porque Translations não está estendendo Map .

Opcionalmente, você sempre pode delegar alguns dos métodos necessários ao mapa usado internamente.

    
por 28.06.2014 / 17:30
fonte
6

Eu vejo isso como um exemplo de abstração significativa. Uma boa abstração tem alguns traços:

  1. Ele oculta detalhes de implementação irrelevantes para o código que os consome.

  2. É tão complexo quanto precisa ser.

Ao estender, você está expondo toda a interface do pai, mas em muitos casos muito disso pode estar melhor escondido, então você quer fazer o que Sebastian Redl sugere e favor a composição da herança e adicione uma instância do pai como membro particular da sua classe personalizada. Qualquer um dos métodos de interface que fazem sentido para sua abstração pode ser facilmente delegado para (no seu caso) a coleção interna.

Quanto a um impacto no desempenho, é sempre uma boa ideia otimizar o código para facilitar a leitura. Se houver suspeita de impacto no desempenho, faça o perfil do código para comparar as duas implementações.

    
por 27.06.2014 / 21:37
fonte
4

+1 para as outras respostas aqui. Também vou acrescentar que, na verdade, é considerado uma prática muito boa pela comunidade DDD (Domain Driven Design). Eles defendem que o seu domínio e as interações com ele devem ter significado de domínio semântico em oposição à estrutura de dados subjacente. Um Map<String, Widget> poderia ser um cache, mas também poderia ser outra coisa, o que você fez corretamente em Minha opinião não tão humilde (IMNSHO) é modelar o que a coleção representa, neste caso, um cache.

Adicionarei uma edição importante em que o wrapper de classe de domínio em torno da estrutura de dados subjacente provavelmente também terá outras variáveis ou funções de membro que realmente tornem uma classe de domínio com interações em oposição a apenas uma estrutura de dados (se somente Java tinha os tipos de valor, vamos obtê-los no Java 10 - prometa!)

Será interessante ver o impacto que os fluxos do Java 8 terão em tudo isso, posso imaginar que talvez algumas interfaces públicas prefiram lidar com um fluxo de (inserir Java primitivo comum ou String) em oposição a um Java objeto.

    
por 27.06.2014 / 21:59
fonte