Sempre faz sentido "programar para uma interface" em Java?

9

Eu já vi a discussão nesta pergunta sobre como uma classe que implementa a partir de uma interface seria instanciada. No meu caso, estou escrevendo um programa muito pequeno em Java que usa uma instância de TreeMape, de acordo com a opinião de todos, deve ser instanciado como:

Map<X> map = new TreeMap<X>();

No meu programa, estou chamando a função map.pollFirstEntry(), que não está declarada na Mapinterface (e algumas outras que também estão presentes na Mapinterface). Eu consegui fazer isso transmitindo para TreeMap<X>todos os lugares que eu chamo de método como:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

Entendo as vantagens das diretrizes de inicialização descritas acima para programas grandes, no entanto, para um programa muito pequeno em que esse objeto não seria passado para outros métodos, consideraria desnecessário. Ainda assim, estou escrevendo esse código de exemplo como parte de um pedido de emprego e não quero que meu código fique mal ou desorganizado. Qual seria a solução mais elegante?

EDIT: Gostaria de salientar que estou mais interessado nas boas práticas gerais de codificação, em vez da aplicação da função específica TreeMap. Como algumas das respostas já apontaram (e marquei como respondida a primeira), o nível de abstração mais alto possível deve ser usado, sem perder a funcionalidade.

jimijazz
fonte
11
Use um TreeMap quando precisar dos recursos. Provavelmente, essa foi uma opção de design por um motivo específico, por isso também deve fazer parte da implementação.
@JordanReiter, devo apenas adicionar uma nova pergunta com o mesmo conteúdo ou existe um mecanismo interno de postagem cruzada?
Jimijazz
2
"Eu entendo as vantagens das diretrizes de inicialização como descrito acima para grandes programas" Ter a lançar em todo o lugar não é vantajoso, não importa o que o tamanho do programa
Ben Aaronson

Respostas:

23

"Programar em uma interface" não significa "usar a versão mais abstrata possível". Nesse caso, todo mundo usaria Object.

O que isso significa é que você deve definir seu programa com a menor abstração possível sem perder a funcionalidade . Se você precisar de um TreeMap, precisará definir um contrato usando a TreeMap.

Jeroen Vannevel
fonte
2
O TreeMap não é uma interface, é uma classe de implementação. Ele implementa as interfaces Map, SortedMap e NavigableMap. O método descrito faz parte da interface NavigableMap . O uso do TreeMap impediria que a implementação alternasse para um ConcurrentSkipListMap (por exemplo), que é o ponto inteiro da codificação para uma interface e não da implementação.
3
@ MichaelT: Eu não olhei para a abstração exata que ele precisa nesse cenário específico, então usei TreeMapcomo exemplo. O "programa para uma interface" não deve ser considerado literalmente como uma interface ou uma classe abstrata - uma implementação também pode ser considerada uma interface.
Jeroen Vannevel
11
Mesmo que a interface / métodos públicos de uma classe de implementação sejam tecnicamente uma 'interface', ela quebra o conceito por trás do LSP e impede a substituição de uma subclasse diferente, e é por isso que você deseja programar para um public interfacee não para 'os métodos públicos de uma implementação' .
@JeroenVannevel Concordo que a programação para uma interface pode ser feita quando a interface é realmente representada por uma classe. No entanto, não vejo o benefício que o uso TreeMapteria sobre SortedMapouNavigableMap
toniedzwiedz
16

Se você ainda deseja usar uma interface, pode usar

NavigableMap <X, Y> map = new TreeMap<X, Y>();

não é necessário sempre usar uma interface, mas geralmente há um ponto em que você deseja ter uma visão mais geral que permita substituir a implementação (talvez para teste) e isso é fácil se todas as referências ao objeto forem abstraídas como o tipo de interface.


fonte
3

O ponto por trás da codificação para uma interface em vez de uma implementação é evitar o vazamento de detalhes da implementação que, de outra forma, restringiriam o seu programa.

Considere a situação em que a versão original do seu código usou HashMapae a expôs.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Isso significa que qualquer alteração getFoo()é uma alteração na API que interrompe e deixaria as pessoas infeliz. Se tudo o que você está garantindo é que fooé um mapa, você deve devolvê-lo.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Isso oferece a flexibilidade de alterar como as coisas funcionam dentro do seu código. Você percebe que foorealmente precisa ser um mapa que retorne as coisas em uma ordem específica.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

E isso não quebra nada pelo resto do código.

Mais tarde, você pode fortalecer o contrato que a classe dá sem quebrar nada.

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Isso mergulha no princípio de substituição de Liskov

A substituibilidade é um princípio na programação orientada a objetos. Ele afirma que, em um programa de computador, se S é um subtipo de T, objetos do tipo T podem ser substituídos por objetos do tipo S (ou seja, objetos do tipo S podem substituir objetos do tipo T) sem alterar nenhuma das opções desejáveis propriedades desse programa (correção, tarefa executada etc.).

Como NavigableMap é um subtipo de Mapa, essa substituição pode ser feita sem alterar o programa.

A exposição dos tipos de implementação dificulta a alteração de como o programa funciona internamente quando é necessário fazer uma alteração. Esse é um processo doloroso e muitas vezes cria soluções feias que servem apenas para infligir mais dor ao codificador posteriormente (estou olhando para o codificador anterior que continuava baralhar dados entre um LinkedHashMap e um TreeMap por algum motivo - confie em mim sempre que eu veja seu nome em svn culpa, eu me preocupo).

Você ainda deseja evitar o vazamento de tipos de implementação. Por exemplo, você pode implementar o ConcurrentSkipListMap em vez de algumas características de desempenho ou simplesmente gostar das instruções de importação, java.util.concurrent.ConcurrentSkipListMape não java.util.TreeMapnas instruções de importação ou qualquer outra coisa.


fonte
1

Concordando com as outras respostas de que você deve usar a classe (ou interface) mais genérica da qual realmente precisa de algo, neste caso o TreeMap (ou como alguém sugeriu o NavigableMap). Mas eu gostaria de acrescentar que isso certamente é melhor do que transmitir para todo o lado, o que seria um cheiro muito maior em qualquer caso. Consulte /programming/4167304/why-should-casting-be-avoided por alguns motivos.

Sebastiaan van den Broek
fonte
1

Trata-se de comunicar sua intenção de como o objeto deve ser usado. Por exemplo, se seu método espera um Mapobjeto com uma ordem de iteração previsível :

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

Então, se você absolutamente precisar informar aos chamadores o método acima, ele também retorna um Mapobjeto com uma ordem de iteração previsível , porque existe essa expectativa por algum motivo:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Obviamente, os chamadores ainda podem tratar o objeto de retorno Mapcomo tal, mas isso está além do escopo do seu método:

private Map<String, String> output = processOrderedMap(input);

Dando um passo atrás

O conselho geral de codificação para uma interface é (geralmente) aplicável, porque geralmente é a interface que fornece a garantia de que o objeto deve ser capaz de executar, também conhecido como contrato . Muitos iniciantes começam com HashMap<K, V> map = new HashMap<>()e são aconselhados a declarar isso como a Map, porque a HashMapnão oferece nada mais do que aquilo que Mapse deve fazer. A partir disso, eles serão capazes de entender (espero) por que seus métodos devem incluir um em Mapvez de um HashMap, e isso permite que eles percebam o recurso de herança no OOP.

Para citar apenas uma linha da entrada da Wikipedia do princípio favorito de todos, relacionado a este tópico:

É uma relação semântica, e não meramente sintática, porque pretende garantir a interoperabilidade semântica dos tipos em uma hierarquia ...

Em outras palavras, o uso de uma Mapdeclaração não é porque faz sentido "sintaticamente", mas as invocações no objeto devem apenas se importar com o que é um tipo de Map.

Código de limpeza

Acho que isso também me permite escrever um código mais limpo, principalmente quando se trata de testes de unidade. Criar um HashMapcom uma única entrada de teste somente leitura leva mais de uma linha (excluindo o uso da inicialização entre chaves), quando eu posso substituí-lo facilmente por Collections.singletonMap().

hjk
fonte