Est-il toujours judicieux de «programmer vers une interface» en Java?

9

J'ai vu la discussion à cette question concernant la façon dont une classe qui implémente à partir d'une interface serait instanciée. Dans mon cas, j'écris un très petit programme en Java qui utilise une instance de TreeMap, et selon l'opinion de chacun, il devrait être instancié comme:

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

Dans mon programme, j'appelle la fonction map.pollFirstEntry(), qui n'est pas déclarée dans l' Mapinterface (et quelques autres qui sont également présents dans l' Mapinterface). J'ai réussi à le faire en lançant un TreeMap<X>appel partout où j'appelle cette méthode comme:

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

Je comprends les avantages des directives d'initialisation décrites ci-dessus pour les grands programmes, mais pour un très petit programme où cet objet ne serait pas transmis à d'autres méthodes, je pense que ce n'est pas nécessaire. Pourtant, j'écris cet exemple de code dans le cadre d'une demande d'emploi, et je ne veux pas que mon code soit mal ni encombré. Quelle serait la solution la plus élégante?

EDIT: Je voudrais souligner que je suis plus intéressé par les bonnes bonnes pratiques de codage au lieu de l'application de la fonction spécifique TreeMap. Comme certaines des réponses l'ont déjà souligné (et j'ai marqué comme étant la première à le faire), le niveau d'abstraction le plus élevé possible devrait être utilisé, sans perdre la fonctionnalité.

jimijazz
la source
1
Utilisez un TreeMap lorsque vous avez besoin des fonctionnalités. C'était probablement un choix de conception pour une raison spécifique, donc cela devrait également faire partie de la mise en œuvre.
@JordanReiter dois-je simplement ajouter une nouvelle question avec le même contenu, ou existe-t-il un mécanisme de publication croisée interne?
jimijazz
2
"Je comprends les avantages des directives d'initialisation décrites ci-dessus pour les grands programmes" Le fait de devoir diffuser un peu partout n'est pas avantageux, quelle que soit la taille du programme
Ben Aaronson

Réponses:

23

"Programmer sur une interface" ne signifie pas "utiliser la version la plus abstraite possible". Dans ce cas, tout le monde utiliserait Object.

Cela signifie que vous devez définir votre programme par rapport à l'abstraction la plus faible possible sans perdre de fonctionnalité . Si vous avez besoin d'un, TreeMapvous devrez définir un contrat à l'aide d'un TreeMap.

Jeroen Vannevel
la source
2
TreeMap n'est pas une interface, c'est une classe d'implémentation. Il implémente les interfaces Map, SortedMap et NavigableMap. La méthode décrite fait partie de l' interface NavigableMap . L'utilisation de TreeMap empêcherait l'implémentation de basculer vers un ConcurrentSkipListMap (par exemple) qui est le point entier du codage vers une interface plutôt que l'implémentation.
3
@MichaelT: Je n'ai pas regardé l'abstraction exacte dont il a besoin dans ce scénario spécifique, j'ai donc utilisé TreeMapcomme exemple. «Programmer vers une interface» ne doit pas être considéré comme une interface ou une classe abstraite - une implémentation peut également être considérée comme une interface.
Jeroen Vannevel
1
Même si l'interface / les méthodes publiques d'une classe d'implémentation sont techniquement une «interface», elle rompt le concept derrière LSP et empêche la substitution d'une sous-classe différente, c'est pourquoi vous voulez programmer vers une public interfaceplutôt que «les méthodes publiques d'une implémentation» .
@JeroenVannevel Je suis d'accord que la programmation d'une interface peut être effectuée lorsque l' interface est réellement représentée par une classe. Cependant, je ne vois pas quel avantage l'utilisation TreeMapaurait sur SortedMapouNavigableMap
toniedzwiedz
16

Si vous souhaitez toujours utiliser une interface, vous pouvez utiliser

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

il n'est pas nécessaire de toujours utiliser une interface mais il y a souvent un point où vous voulez avoir une vue plus générale qui vous permet de remplacer l'implémentation (peut-être pour les tests) et c'est facile si toutes les références à l'objet sont abstraites comme type d'interface.


la source
3

Le point derrière le codage vers une interface plutôt qu'une implémentation est d'éviter la fuite des détails d'implémentation qui contraindraient autrement votre programme.

Considérez la situation où la version originale de votre code a utilisé un HashMapet l'a exposé.

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

Cela signifie que toute modification apportée getFoo()est une rupture de l'API et rendrait les utilisateurs malheureux. Si tout ce que vous garantissez est fooune carte, vous devez le retourner à la place.

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

Cela vous donne la flexibilité de changer la façon dont les choses fonctionnent dans votre code. Vous vous rendez compte que cela foodoit être une carte qui renvoie les choses dans un ordre particulier.

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

Et cela ne casse rien pour le reste du code.

Vous pourrez par la suite renforcer le contrat donné par la classe sans rien casser non plus.

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

Cela plonge dans le principe de substitution de Liskov

La substituabilité est un principe de la programmation orientée objet. Il indique que, dans un programme informatique, si S est un sous-type de T, les objets de type T peuvent être remplacés par des objets de type S (c'est-à-dire que les objets de type S peuvent remplacer des objets de type T) sans altérer aucun des éléments souhaitables. propriétés de ce programme (exactitude, tâche exécutée, etc.).

Étant donné que NavigableMap est un sous-type de carte, cette substitution peut être effectuée sans modifier le programme.

L'exposition des types d'implémentation rend difficile la modification du fonctionnement interne du programme lorsqu'un changement doit être effectué. Ceci est un processus douloureux et crée de nombreuses solutions de contournement laides qui ne servent qu'à infliger plus de douleur au codeur plus tard (je vous regarde le codeur précédent qui a continué à mélanger les données entre un LinkedHashMap et un TreeMap pour une raison quelconque - croyez-moi, chaque fois que je voir votre nom en svn blâme je m'inquiète).

Vous souhaiterez toujours éviter les fuites de types d'implémentation. Par exemple, vous souhaiterez peut-être implémenter ConcurrentSkipListMap à la place en raison de certaines caractéristiques de performances ou vous aimerez simplement java.util.concurrent.ConcurrentSkipListMapplutôt que java.util.TreeMapdans les instructions d'importation ou autre chose.


la source
1

En accord avec les autres réponses, vous devez utiliser la classe (ou l'interface) la plus générique dont vous avez réellement besoin de quelque chose, dans ce cas TreeMap (ou comme quelqu'un a suggéré NavigableMap). Mais je voudrais ajouter que c'est certainement mieux que de le lancer partout, ce qui serait en tout cas une odeur beaucoup plus grande. Voir /programming/4167304/why-should-casting-be-avoided pour certaines raisons.

Sebastiaan van den Broek
la source
1

Il s'agit de communiquer votre intention sur la façon dont l'objet doit être utilisé. Par exemple, si votre méthode attend un Mapobjet avec un ordre d'itération prévisible :

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

Ensuite, si vous devez absolument informer les appelants de la méthode ci-dessus, elle retourne également un Mapobjet avec un ordre d'itération prévisible , car il existe une telle attente pour une raison quelconque:

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

Bien sûr, les appelants peuvent toujours traiter l'objet de retour en tant Mapque tel, mais cela dépasse le cadre de votre méthode:

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

Prendre du recul

Les conseils généraux de codage sur une interface sont (généralement) applicables, car généralement c'est l'interface qui fournit la garantie de ce que l'objet devrait être capable d'exécuter, alias le contrat . De nombreux débutants commencent par HashMap<K, V> map = new HashMap<>()et il est conseillé de le déclarer comme un Map, car un HashMapn'offre rien de plus que ce qu'un Mapest censé faire. À partir de cela, ils seront alors en mesure de comprendre (espérons-le) pourquoi leurs méthodes devraient intégrer un Mapau lieu d'un HashMap, ce qui leur permet de réaliser la fonction d'héritage dans la POO.

Pour citer une seule ligne de l'entrée Wikipedia du principe préféré de tout le monde lié à ce sujet:

C'est une relation sémantique plutôt que simplement syntaxique car elle entend garantir l'interopérabilité sémantique des types dans une hiérarchie ...

En d'autres termes, l'utilisation d'une Mapdéclaration n'est pas parce qu'elle a un sens «syntaxiquement», mais plutôt que les invocations sur l'objet ne devraient se soucier que de son type Map.

Code plus propre

Je trouve que cela me permet parfois d'écrire du code plus propre, surtout en ce qui concerne les tests unitaires. La création d'un HashMapavec une seule entrée de test en lecture seule prend plus d'une ligne (à l'exclusion de l'utilisation de l'initialisation à double accolade), quand je peux facilement le remplacer par Collections.singletonMap().

hjk
la source