Boas ou más práticas para mascarar coleções Java com nomes de classe significativos?

46

Ultimamente, tenho o hábito de "mascarar" coleções Java com nomes de classes amigáveis ​​ao ser humano. 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 apresenta atraso / latência, além de ser um anti-padrão OO. Eu posso entender introduzindo um grau muito pequeno de sobrecarga de desempenho, mas não consigo imaginar que seja de todo significativo. Então eu pergunto: isso é bom ou ruim, e por quê?

herpylderp
fonte
11
É muito mais simples que isso. É uma prática ruim porque imagino que você esteja estendendo as implementações da coleção JDK básica do Java. Em Java, você pode estender apenas uma classe, portanto, é necessário pensar e projetar mais quando tiver uma extensão. Em Java, use estender com moderação.
InformedA
ChangeLista compilação será interrompida extends, porque List é uma interface, requer implements. @randomA o que você está imaginando errou o ponto por causa desse erro #
gnat
@gnat Não está esquecendo o ponto, estou assumindo que ele estava estendendo um implementationie HashMapou TreeMape o que ele teve lá foi um erro de digitação.
InformedA
4
Esta é uma prática ruim. MAU MAU MAU. Não faça isso. Todo mundo sabe o que é um Mapa <String, Widget>. Mas um WidgetCache? Agora eu preciso abrir o WidgetCache.java, preciso lembrar que o WidgetCache é apenas um mapa. Tenho que verificar sempre que for lançada uma nova versão que você não adicionou algo novo ao WidgetCache. Deus não, nunca faça isso.
Route de milhas
"Se você visse um ArrayList <ArrayList <? >> sendo passado pelo código, você fugiria gritando...?" Não, estou bastante confortável com coleções genéricas aninhadas. E você deveria ser também.
Kevin Krumwiede 28/09

Respostas:

75

Atraso / Latência? Eu chamo BS por isso. Deve haver exatamente zero sobrecarga nessa prática. ( Editar: foi apontado nos comentários que isso pode, de fato, inibir as otimizações realizadas pela VM do HotSpot. Não sei o suficiente sobre a implementação da VM para confirmar ou negar isso. Eu estava baseando meu comentário no C ++ implementação de funções virtuais.)

Há algum código adicional. Você precisa criar todos os construtores da classe base que desejar, encaminhando seus parâmetros.

Eu também não vejo isso como um antipadrão, por si só. No entanto, eu vejo isso como uma oportunidade perdida. Em vez de criar uma classe que deriva a classe base apenas para renomear, que tal criar uma classe que contenha a coleção e ofereça uma interface aprimorada, específica para cada caso? O cache do widget 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 em conjunto com a regra geral de usar interfaces, não implementações - ou seja, no código de coleção simples, você cria um HashMap<String, Widget>e atribui-o a uma variável do tipo Map<String, Widget>. Você WidgetCachenã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á-la uma classe que se estende HashMap<String, Widget>, você deve declarar as variáveis ​​como WidgetCacheor Map<String, Widget>e a primeira perde a flexibilidade de substituir uma coleção diferente (talvez uma coleção de carregamento lento do ORM), enquanto a segunda derrota o ponto de ter a aula.

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

Estes são todos os pontos a considerar. Pode ou não ser a escolha certa. Nos dois casos, os argumentos oferecidos pelo seu colega não são válidos. Se ele acha que é um antipadrão, deve nomear.

Sebastian Redl
fonte
1
Observe que, embora seja possível ter algum impacto no desempenho, não será tão horrível (perdendo algumas otimizações internas e obtendo um vcall extra no pior caso - não é bom no seu loop mais interno, mas provavelmente é bom).
Voo
5
Essa resposta realmente boa diz a todos "não julgue a decisão pela sobrecarga de desempenho" - e a única discussão abaixo é "como o HotSpot lida com isso? Existe algum impacto irrelevante no desempenho (em 99,9% de todos os casos)" - pessoal, você se incomodou em ler mais do que a primeira frase?
Doc Brown
16
+1 em "Em vez de criar uma classe que deriva a classe base apenas para renomear, que tal criar uma classe que contenha a coleção e ofereça uma interface aprimorada, específica para cada caso?"
user11153
6
Exemplo prático que ilustra o ponto principal desta resposta: A documentação para public class Properties extends Hashtable<Object,Object>in java.utildiz "Como as propriedades herdam do Hashtable, os métodos put e putAll podem ser aplicados a um objeto Properties. Seu uso é fortemente desencorajado, pois permite que o chamador insira entradas cujas chaves ou valores não são Strings. ". A composição teria sido muito mais limpa.
Patricia Shanahan
3
@DocBrown Não, esta resposta afirma que não há impacto no desempenho. Se você fizer afirmações fortes, poderá apoiá-las, caso contrário as pessoas provavelmente o chamarão. O ponto principal dos comentários (nas respostas) é apontar fatos ou suposições imprecisos ou adicionar notas úteis, mas certamente não para parabenizar as pessoas, é para isso que serve o sistema de votação.
Voo
25

De acordo com a IBM, esse é realmente um antipadrão. Essas classes do tipo 'typedef' são chamadas de tipos psuedo.

O artigo explica muito melhor do que eu, mas tentarei resumir se o link for desativado:

  • Qualquer código que espera um WidgetCachenão pode manipular umMap<String, Widget>
  • Esses pseudotipos são 'virais' ao usar vários pacotes, eles 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 tipo.

(Cheguei a esta resposta por meio dessa pergunta de estouro de pilha relacionada)

Roy T.
fonte
13
A necessidade newHashMapagora não é resolvida pelo operador de diamantes?
svick
Absolutamente verdade, esqueci disso. Normalmente não trabalho em Java.
Roy T.
Eu gostaria que as linguagens permitissem um universo de tipos de variáveis ​​que continham referências a instâncias de outros tipos, mas ainda assim suportem a verificação em tempo de compilação, de modo que um valor do tipo WidgetCachepossa ser atribuído a uma variável do tipo WidgetCacheou Map<String,Widget>sem uma conversão, mas existe era um meio pelo qual um método estático WidgetCachepoderia fazer referência Map<String,Widget>e retorná-lo como tipo WidgetCacheapós executar qualquer validação desejada. Esse recurso pode ser especialmente útil para genéricos, que não precisariam ser apagados (desde ... #
306
... os tipos existiriam apenas na mente do compilador). Para muitos tipos mutáveis, existem dois tipos comuns de campo de referência: um que contém a única referência a uma instância que pode ser mutada ou um que contém uma referência compartilhável a uma instância que ninguém pode sofrer mutação. Seria útil poder dar nomes diferentes para esses dois tipos de campos, pois eles exigem padrões de uso diferentes, mas ambos devem manter referências aos mesmos tipos de instâncias de objetos.
Supercat
5
Esse é um ótimo argumento para o motivo pelo qual o Java precisa de aliases de tipo.
GlenPeterson
13

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

A situação é bastante comum, pois a maioria das linguagens de programação com estaticamente tem sintaxe especial para os tipos de alias, geralmente chamados a typedef. O Java provavelmente não os copiou porque originalmente não tinha tipos parametrizados. Estender uma classe não é ideal, devido aos motivos que Sebastian cobriu tão bem em sua resposta, mas pode ser uma solução razoável para a sintaxe limitada do Java.

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

Karl Bielefeldt
fonte
Há também uma sobrecarga minúscula nos construtores.
Stephen C
11

Sugiro que você, como já mencionado, use composição sobre herança, para que você possa expor apenas métodos realmente necessários, com nomes que correspondam ao caso de uso pretendido. Os usuários de sua classe 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 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 possui 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, por exemplo translations.clear(), porque Translationsnão estão se estendendo Map.

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

user11153
fonte
6

Eu vejo isso como um exemplo de uma abstração significativa. Uma boa abstração tem algumas características:

  1. Ele oculta detalhes de implementação que são irrelevantes para o código que o 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 ser melhor ocultado; portanto, você deve fazer o que Sebastian Redl sugere e favorecer a composição sobre a herança e adicionar uma instância do pai como um membro privado da sua classe personalizada. Qualquer um dos métodos de interface que faça sentido para sua abstração pode ser facilmente delegado (no seu caso) à coleção interna.

Quanto ao impacto no desempenho, sempre é uma boa idéia otimizar o código para facilitar a leitura e, se houver suspeita de um impacto no desempenho, crie um perfil do código para comparar as duas implementações.

Mike Partridge
fonte
4

+1 nas outras respostas aqui. Também acrescentarei que é realmente considerado uma prática muito boa pela comunidade DDD (Domain Driven Design). Eles defendem que seu domínio e as interações com ele devem ter significado de domínio semântico, em oposição à estrutura de dados subjacente. A 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, nesse caso, um cache.

Vou adicionar uma edição importante, pois o wrapper da classe de domínio em torno da estrutura de dados subjacente provavelmente também deve ter outras variáveis ​​ou funções de membro que realmente a tornam uma classe de domínio com interações, em vez de apenas uma estrutura de dados (se apenas Java tivesse tipos de valor , vamos obtê-los em Java 10 - prometa!)

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

Martijn Verburg
fonte