Comprendre la «programmation vers une interface»

30

J'ai souvent rencontré le terme "programmation vers une interface au lieu d'une implémentation", et je pense que je comprends un peu ce que cela signifie. Mais je veux m'assurer de bien comprendre ses avantages et ses implémentations possibles.

«Programmation vers une interface» signifie que, lorsque cela est possible, il convient de se référer à un niveau plus abstrait d'une classe (une interface, une classe abstraite ou parfois une superclasse quelconque), au lieu de se référer à une implémentation concrète.

Un exemple courant en Java est d'utiliser:

List myList = new ArrayList();au lieu de ArrayList myList = new ArrayList();.

J'ai deux questions à ce sujet:

  1. Je veux m'assurer de bien comprendre les principaux avantages de cette approche. Je pense que les avantages sont surtout la flexibilité. Déclarer un objet comme une référence de plus haut niveau, plutôt qu'une implémentation concrète, permet plus de flexibilité et de maintenabilité tout au long du cycle de développement et tout au long du code. Est-ce correct? La flexibilité est-elle le principal avantage?

  2. Existe-t-il d'autres moyens de «programmer vers une interface»? Ou est-ce que «déclarer une variable comme une interface plutôt qu'une implémentation concrète» est la seule implémentation de ce concept?

Je ne parle pas de l'interface de construction Java . Je parle du principe OO "programmation vers une interface, pas une implémentation". Dans ce principe, l '"interface" mondiale fait référence à tout "supertype" d'une classe - une interface, une classe abstraite ou une superclasse simple qui est plus abstraite et moins concrète que ses sous-classes plus concrètes.

Aviv Cohn
la source
doublon possible de Pourquoi les interfaces sont-elles utiles?
moucher
1
Cette réponse vous donne un exemple facile à comprendre programmers.stackexchange.com/a/314084/61852
Tulains Córdova

Réponses:

46

«Programmation vers une interface» signifie que, lorsque cela est possible, il convient de se référer à un niveau plus abstrait d'une classe (une interface, une classe abstraite ou parfois une superclasse quelconque), au lieu de se référer à une implémentation concrète.

Ce n'est pas correct . Ou du moins, ce n'est pas tout à fait correct.

Le point le plus important vient du point de vue de la conception du programme. Ici, «programmer vers une interface» signifie concentrer votre conception sur ce que fait le code, pas sur la façon dont il le fait. Il s'agit d'une distinction essentielle qui pousse votre conception vers l'exactitude et la flexibilité.

L'idée principale est que les domaines changent beaucoup plus lentement que les logiciels. Disons que vous disposez d'un logiciel pour garder une trace de votre liste d'épicerie. Dans les années 80, ce logiciel fonctionnait avec une ligne de commande et certains fichiers plats sur disquette. Ensuite, vous avez une interface utilisateur. Ensuite, vous pouvez peut-être mettre la liste dans la base de données. Plus tard, il peut être déplacé vers le cloud, les téléphones mobiles ou l'intégration Facebook.

Si vous avez conçu votre code spécifiquement autour de l'implémentation (disquettes et lignes de commande), vous seriez mal préparé aux changements. Si vous avez conçu votre code autour de l'interface (en manipulant une liste d'épicerie), l'implémentation est libre de changer.

Telastyn
la source
Merci d'avoir répondu. À en juger par ce que vous avez écrit, je pense que je comprends ce que signifie "programmer vers une interface" et quels sont ses avantages. Mais j'ai une question - L'exemple concret le plus courant pour ce concept est le suivant: lors de la création d'une référence à un objet, définissez le type de référence comme le type d'interface que cet objet implémente (ou la superclasse dont cet objet hérite), au lieu de créer le type de référence le type d'objet. (Aka, List myList = new ArrayList()au lieu de ArrayList myList = new ArrayList(). (La question est dans le commentaire suivant)
Aviv Cohn
Ma question est: pouvez-vous me donner plus d'exemples d'endroits dans le code du monde réel où le principe de "programmation vers une interface" a lieu? Autre que l'exemple commun que j'ai décrit dans le dernier commentaire?
Aviv Cohn
6
NON . List/ ce ArrayListn'est pas du tout ce dont je parle. Cela ressemble plus à la fourniture d'un Employeeobjet plutôt qu'à l'ensemble de tables liées que vous utilisez pour stocker un enregistrement Employé. Ou fournir une interface pour parcourir les chansons, et ne pas se soucier si ces chansons sont mélangées, ou sur un CD, ou en streaming à partir d'Internet. Ce ne sont que des séquences de chansons.
Telastyn
1
L'existence du «bouton turbo»: en.wikipedia.org/wiki/Turbo_button est un exemple réel de l'analogie de votre disquette.
JimmyJames
1
@AvivCohn Je suggère SpringFramework comme un bon exemple. Tout dans Spring peut être amélioré ou personnalisé en faisant votre propre implémentation de leurs interfaces et du comportement principal de Springs et les fonctionnalités continueront de fonctionner comme prévu ... Ou en cas de personnalisations, comme vous vous attendiez. La programmation vers une interface est également la meilleure stratégie pour concevoir des cadres d' intégration . Encore une fois, Spring fait son intégration au printemps. Au moment de la conception, la programmation vers une interface est ce que nous faisons avec UML. Ou c'est ce que je ferais. Abstenez-vous de la façon dont cela fonctionne et concentrez-vous sur ce que vous devez faire.
Laiv
19

Ma compréhension de la «programmation vers une interface» est différente de ce que la question ou les autres réponses suggèrent. Ce qui ne veut pas dire que ma compréhension est correcte, ou que les choses dans les autres réponses ne sont pas de bonnes idées, juste qu'elles ne sont pas ce à quoi je pense quand j'entends ce terme.

La programmation vers une interface signifie que lorsque vous êtes présenté avec une interface de programmation (que ce soit une bibliothèque de classes, un ensemble de fonctions, un protocole réseau ou autre), vous continuez à n'utiliser que des choses garanties par l'interface. Vous pouvez avoir des connaissances sur l'implémentation sous-jacente (vous l'avez peut-être écrite), mais vous ne devriez jamais utiliser ces connaissances.

Par exemple, supposons que l'API vous présente une valeur opaque qui est une "poignée" vers quelque chose interne. Vos connaissances peuvent vous dire que ce descripteur est vraiment un pointeur, et vous pouvez le déréférencer et accéder à une valeur, ce qui pourrait vous permettre d'accomplir facilement une tâche que vous souhaitez effectuer. Mais l'interface ne vous offre pas cette option; c'est votre connaissance de l'implémentation particulière qui le fait.

Le problème avec cela est qu'il crée un couplage fort entre votre code et l'implémentation, exactement ce que l'interface était censée empêcher. Selon la politique, cela peut signifier que l'implémentation ne peut plus être modifiée, car cela casserait votre code, ou que votre code est très fragile et continue de casser à chaque mise à niveau ou changement de l'implémentation sous-jacente.

Un grand exemple de ceci est les programmes écrits pour Windows. WinAPI est une interface, mais de nombreuses personnes ont utilisé des astuces qui fonctionnaient en raison de l'implémentation particulière de Windows 95, par exemple. Ces astuces ont peut-être rendu leurs programmes plus rapides ou leur ont permis de faire des choses avec moins de code que cela n'aurait été nécessaire autrement. Mais ces astuces signifiaient également que le programme plantait sur Windows 2000, car l'API y était implémentée différemment. Si le programme était suffisamment important, Microsoft pourrait en fait aller de l'avant et ajouter un peu de hack à leur implémentation pour que le programme continue de fonctionner, mais le coût de cela est une complexité accrue (avec tous les problèmes qui en découlent) du code Windows. Cela rend également la vie très difficile pour les gens de Wine, car ils essaient également d'implémenter WinAPI, mais ils ne peuvent se référer qu'à la documentation pour savoir comment le faire,

Sebastian Redl
la source
C'est un bon point, et j'entends souvent cela dans certains contextes.
Telastyn
Je vois ce que tu veux dire. Alors laissez-moi voir si je peux adapter ce que vous dites à la programmation générale: Disons que j'ai une classe (classe A) qui utilise les fonctionnalités de la classe abstraite B. Les classes C et D héritent de la classe B - elles fournissent une implémentation concrète pour ce que cela la classe est censée faire. Si la classe A utilise directement la classe C ou D, cela s'appelle «programmer pour une implémentation», ce qui n'est pas une solution très flexible. Mais si la classe A utilise une référence à la classe B, qui peut ensuite être définie sur l'implémentation C ou l'implémentation D, cela rend les choses plus flexibles et maintenables. Est-ce correct?
Aviv Cohn
Si cela est correct, ma question est la suivante: existe-t-il des exemples plus concrets de «programmation vers une interface», autres que l'exemple commun «utilisation d'une référence d'interface plutôt qu'une référence de classe concrète»?
Aviv Cohn
2
@AvivCohn Un peu tard dans cette réponse, mais un exemple concret est le World Wide Web. Pendant la guerre des navigateurs (ère IE 4), les sites Web ont été écrits non pas dans le sens des spécifications, mais aux caprices de certains navigateurs (Netscape ou IE). C'était essentiellement la programmation de l'implémentation au lieu de l'interface.
Sebastian Redl
9

Je ne peux que parler de mon expérience personnelle, car cela ne m'a jamais été formellement enseigné non plus.

Votre premier point est correct. La flexibilité acquise vient du fait de ne pas pouvoir appeler accidentellement les détails d'implémentation de la classe concrète où ils ne devraient pas être appelés.

Par exemple, considérons une ILoggerinterface qui est actuellement implémentée en tant que LogToEmailLoggerclasse concrète . La LogToEmailLoggerclasse expose toutes les ILoggerméthodes et propriétés, mais se trouve également avoir une propriété spécifique à l'implémentation sysAdminEmail.

Lorsque votre enregistreur est utilisé dans votre application, il ne devrait pas être du ressort du code consommateur de définir le sysAdminEmail. Cette propriété doit être définie lors de la configuration de l'enregistreur et doit être cachée au monde.

Si vous codiez par rapport à l'implémentation concrète, vous pourriez accidentellement définir la propriété d'implémentation lors de l'utilisation de l'enregistreur. Maintenant, votre code d'application est étroitement couplé à votre enregistreur, et le passage à un autre enregistreur nécessitera d'abord de découpler votre code de celui d'origine.

En ce sens, le codage sur une interface desserre le couplage .

Concernant votre deuxième point: Une autre raison que j'ai vue pour le codage sur une interface est de réduire la complexité du code.

Par exemple, imaginez que j'ai un jeu avec les interfaces suivantes I2DRenderable, I3DRenderable, IUpdateable. Il n'est pas rare qu'un seul composant de jeu ait à la fois du contenu de rendu 2D et 3D. D'autres composants peuvent être uniquement 2D et d'autres uniquement 3D.

Si le rendu 2D est effectué par un module, il est alors logique qu'il conserve une collection de I2DRenderables. Peu importe si les objets de sa collection sont également I3DRenderableou IUpdateblecomme d'autres modules seront chargés de traiter ces aspects des objets.

Le stockage des objets pouvant être rendus sous forme de liste I2DRenderableréduit la complexité de la classe de rendu. La logique de rendu et de mise à jour 3D n'est pas son souci et donc ces aspects de ses objets enfants peuvent et doivent être ignorés.

En ce sens, le codage sur une interface maintient une complexité faible en isolant les problèmes .

MetaFight
la source
4

Il y a peut-être deux utilisations du mot interface utilisé ici. L'interface à laquelle vous faites principalement référence dans votre question est une interface Java . C'est spécifiquement un concept Java, plus généralement c'est une interface de langage de programmation.

Je dirais que la programmation vers une interface est un concept plus large. Les API REST désormais populaires qui sont disponibles pour de nombreux sites Web sont un autre exemple du concept plus large de programmation d'une interface à un niveau supérieur. En créant une couche entre le fonctionnement interne de votre code et le monde extérieur (personnes sur Internet, autres programmes, voire d'autres parties du même programme), vous pouvez changer quoi que ce soit à l'intérieur de votre code tant que vous ne changez pas ce que le monde extérieur attend, lorsque cela est défini par une interface ou un contrat que vous avez l'intention d'honorer.

Cela vous donne alors la flexibilité de refactoriser votre code interne sans avoir à dire toutes les autres choses qui dépendent de son interface.

Cela signifie également que votre code devrait être plus stable. En vous en tenant à l'interface, vous ne devriez pas casser le code des autres. Lorsque vous devez vraiment changer l'interface, vous pouvez publier une nouvelle version majeure (1.abc à 2.xyz) de l'API qui signale qu'il y a des changements de rupture dans l'interface de la nouvelle version.

Comme le souligne @Doval dans les commentaires sur cette réponse, il existe également une localité d'erreurs. Je pense que tout cela se résume à l'encapsulation. Tout comme vous l'utiliseriez pour des objets dans une conception orientée objet, ce concept est également utile à un niveau supérieur.

Encaitar
la source
1
Un avantage généralement négligé est la localisation des erreurs. Supposons que vous ayez besoin d'une carte et que vous l'implémentiez à l'aide d'un arbre binaire. Pour que cela fonctionne, les clés doivent avoir un certain ordre, et vous devez conserver l'invariant que les clés qui sont "inférieures à" la clé du nœud actuel sont dans le sous-arbre gauche, tandis que celles qui sont "supérieures à" sont activées. le sous-arbre droit. Lorsque vous masquez l'implémentation de la carte derrière une interface, si une recherche de carte échoue, vous savez que le bogue doit être dans le module de carte. S'il est exposé, le bogue peut se trouver n'importe où dans le programme. Pour moi, c'est le principal avantage.
Doval
4

Une analogie dans le monde réel pourrait aider:

Une prise électrique secteur dans une interface.
Oui; cette chose à trois broches à l'extrémité du cordon d'alimentation de votre téléviseur, radio, aspirateur, lave-linge, etc.

Tout appareil qui a une prise principale (c'est-à-dire qui met en œuvre l'interface "a une prise secteur") peut être traité exactement de la même manière; ils peuvent tous être branchés sur une prise murale et peuvent tirer de l'énergie de cette prise.

Ce que chaque appareil n'est tout à fait différent. Vous n'allez pas aller très loin en nettoyant vos tapis avec le téléviseur et la plupart des gens ne regardent pas leur machine à laver pour se divertir. Mais tous ces appareils partagent le même comportement de pouvoir être branchés sur une prise murale.

C'est ce que les interfaces vous procurent. Comportements
unifiés qui peuvent être effectués par de nombreuses classes d'objets différentes, sans avoir besoin des complications de l'héritage.

Phill W.
la source
1

Le terme «programmation vers une interface» est ouvert à de nombreuses interprétations. Interface dans le développement de logiciels est un mot très courant. Voici comment j'explique le concept aux développeurs juniors que j'ai formés au fil des ans.

Dans l'architecture logicielle, il existe un large éventail de frontières naturelles. Les exemples courants incluent

  • La frontière réseau entre les processus client et serveur
  • Limite de l'API entre une application et une bibliothèque tierce
  • Limite de code interne entre différents domaines d'activité au sein du programme

Ce qui importe, c'est que lorsque ces frontières naturelles existent, elles sont identifiées et le contrat de comportement de cette frontière est spécifié. Vous testez votre logiciel non pas par le comportement de "l'autre côté", mais par la conformité de vos interactions avec les spécifications.

Les conséquences de ceci sont:

  • Les composants externes peuvent être échangés tant qu'ils mettent en œuvre la spécification
  • Point naturel pour les tests unitaires pour valider le comportement correct
  • Les tests d'intégration deviennent importants - la spécification était-elle ambiguë?
  • En tant que développeur, vous avez un monde de préoccupation plus petit lorsque vous travaillez sur une tâche particulière

Bien qu'une grande partie de cela puisse concerner les classes et les interfaces, il est tout aussi important de réaliser que cela concerne également les modèles de données, les protocoles réseau et, de manière plus générale, le travail avec plusieurs développeurs.

Michael Shaw
la source