web-dev-qa-db-fra.com

Qu'est-ce que cela signifie quand on dit «Encapsuler ce qui varie»?

L'un des principes OOP que j'ai rencontrés est: -Encapsuler ce qui varie.

Je comprends le sens littéral de l'expression, c'est-à-dire cacher ce qui varie. Cependant, je ne sais pas exactement comment cela contribuerait à une meilleure conception. Quelqu'un peut-il l'expliquer en utilisant un bon exemple?

25
Haris Ghauri

Vous pouvez écrire du code qui ressemble à ceci:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

ou vous pouvez écrire du code qui ressemble à ceci:

pet.speak();

Si ce qui varie est encapsulé, vous n'avez pas à vous en préoccuper. Vous vous inquiétez simplement de ce dont vous avez besoin et de tout ce que vous utilisez. Découvrez comment faire ce dont vous avez vraiment besoin en fonction de ce qui varie.

Encapsulez ce qui varie et vous n'avez pas à diffuser de code qui se soucie de ce qui varie. Vous définissez simplement l'animal comme étant un certain type qui sait parler comme ce type et après cela, vous pouvez oublier quel type et le traiter comme un animal. Vous n'avez pas à demander quel type.

Vous pourriez penser que le type est encapsulé car un getter est nécessaire pour y accéder. Je ne. Getter's n'encapsule pas vraiment. Ils bavent juste quand quelqu'un rompt votre encapsulation. C'est un joli crochet orienté décorateur qui est le plus souvent utilisé comme code de débogage. Peu importe comment vous le coupez, vous exposez toujours le type.

Vous pourriez regarder cet exemple et penser que j'associe le polymorphisme et l'encapsulation. Je ne suis pas. J'associe "ce qui varie" et "détails".

Le fait que votre animal soit un chien est un détail. Celui qui pourrait varier pour vous. Un qui pourrait ne pas l'être. Mais certainement celui qui peut varier d'une personne à l'autre. Sauf si nous pensons que ce logiciel ne sera utilisé que par les amoureux des chiens, il est judicieux de traiter le chien comme un détail et de l'encapsuler. De cette façon, certaines parties du système ignorent parfaitement le chien et ne seront pas affectées lorsque nous fusionnerons avec "les perroquets sont nous".

Découplez, séparez et masquez les détails du reste du code. Ne laissez pas la connaissance des détails se propager à travers votre système et vous suivrez très bien "encapsuler ce qui varie".

32
candied_orange

"Varie" signifie ici "peut changer au fil du temps en raison de l'évolution des exigences". Il s'agit d'un principe de conception de base: séparer et isoler des éléments de code ou de données qui devront peut-être changer séparément à l'avenir. Si une seule exigence change, elle devrait idéalement nous obliger uniquement à changer le code associé en un seul endroit. Mais si la base de code est mal conçue, c'est-à-dire hautement interconnectée et la logique de l'exigence répartie dans de nombreux endroits, alors le changement sera difficile et aura un risque élevé de provoquer des effets inattendus.

Supposons que vous ayez une application qui utilise le calcul de la taxe de vente dans de nombreux endroits. Si le taux de la taxe de vente change, que préféreriez-vous:

  • le taux de la taxe de vente est un littéral codé en dur partout dans l'application où la taxe de vente est calculée.

  • le taux de la taxe de vente est une constante globale, qui est utilisée partout dans l'application où la taxe de vente est calculée.

  • il existe une seule méthode appelée calculateSalesTax(product) qui est le seul endroit où le taux de taxe de vente est utilisé.

  • le taux de la taxe de vente est spécifié dans un fichier de configuration ou un champ de base de données.

Étant donné que le taux de la taxe de vente peut changer en raison d'une décision politique indépendante d'une autre exigence, nous préférons l'avoir isolé dans une configuration, afin qu'il puisse être modifié sans affecter aucun code. Mais il est également concevable que la logique de calcul de la taxe de vente puisse changer, par exemple différents taux pour différents produits, nous aimons donc également que la logique de calcul soit encapsulée. La constante globale peut sembler une bonne idée, mais elle est en fait mauvaise, car elle pourrait encourager l'utilisation de la taxe de vente à différents endroits du programme plutôt qu'à un seul endroit.

Considérons maintenant une autre constante, Pi, qui est également utilisée à de nombreux endroits dans le code. Le même principe de conception est-il valable? Non, car Pi ne va pas changer. Son extraction dans un fichier de configuration ou un champ de base de données ne fait qu'introduire une complexité inutile (et toutes choses étant égales par ailleurs, nous préférons le code le plus simple). Il est logique d'en faire une constante globale plutôt que de la coder en dur à plusieurs endroits pour éviter les incohérences et améliorer la lisibilité.

Le fait est que si nous ne regardons que le fonctionnement du programme maintenant, le taux de taxe de vente et Pi sont équivalents, les deux sont des constantes. Ce n'est que lorsque nous considérons ce qui peut varier à l'avenir, que nous réalisons que nous devons les traiter différemment dans la conception.

Ce principe est en fait assez profond, car cela signifie que vous devez regarder au-delà de ce que la base de code est censée faire aujourd'hui, et également considérer les forces externes qui peuvent la faire changer, et même comprendre la différentes parties prenantes derrière les exigences.

17
JacquesB

Les deux réponses actuelles semblent ne frapper que partiellement la cible, et elles se concentrent sur des exemples qui obscurcissent l'idée de base. Ce n'est pas non plus (uniquement) un principe OOP) mais un principe de conception logicielle en général.

La chose qui "varie" dans cette phrase est le code. Christophe a raison de dire que c'est généralement quelque chose que peut varier, c'est-à-dire que vous anticipez souvent ceci. Le but est de vous protéger des modifications futures du code. Ceci est étroitement lié à programmation contre une interface . Cependant, Christophe a tort de limiter cela aux "détails d'implémentation". En fait, la valeur de ces conseils est souvent due à des changements dans les exigences .

Ceci n'est qu'indirectement lié à l'encapsulation de l'état, ce à quoi je pense que David Arno pense. Ce conseil ne suggère pas toujours (mais souvent) un état d'encapsulation, et ce conseil s'applique également aux objets immuables. En fait, le simple fait de nommer des constantes est une forme (très basique) d'encapsuler ce qui varie.

CandiedOrange confond explicitement "ce qui varie" avec "détails". Ce n'est que partiellement correct. Je suis d'accord que tout code qui varie est des "détails" dans un certain sens, mais un "détail" ne peut pas varier (sauf si vous définissez des "détails" pour rendre cela tautologique). Il peut y avoir des raisons d'encapsuler des détails non variables, mais ce dicton n'en est pas un. En gros, si vous étiez très confiant que "chien", "chat" et "canard" seraient les seuls types avec lesquels vous auriez besoin de traiter, alors ce dicton ne suggère pas le refactoring effectué par CandiedOrange.

Casting l'exemple de CandiedOrange dans un contexte différent, supposons que nous avons un langage procédural comme C. Si j'ai du code qui contient:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

Je peux raisonnablement m'attendre à ce que ce morceau de code change à l'avenir. Je peux "l'encapsuler" simplement en définissant une nouvelle procédure:

void speak(pet) {
  if (pet.type() == dog) {
    pet.bark();
  } else if (pet.type() == cat) {
    pet.meow();
  } else if (pet.type() == duck) {
    pet.quack()
  }
}

et en utilisant cette nouvelle procédure au lieu du bloc de code (c'est-à-dire une refactorisation de "méthode d'extraction"). À ce stade, l'ajout d'un type "vache" ou autre ne nécessite que la mise à jour de la procédure speak. Bien sûr, dans une langue OO vous pouvez à la place tirer parti de la répartition dynamique comme le mentionne la réponse de CandiedOrange. Cela se produira naturellement si vous accédez à pet via une interface. Élimination de la logique conditionnelle via la répartition dynamique est une préoccupation orthogonale qui faisait partie de la raison pour laquelle j'ai fait ce rendu procédural. Je tiens également à souligner que cela ne nécessite pas de fonctionnalités particulières à la POO. Même dans un langage OO, encapsulant ce qui varie ne signifie pas nécessairement qu'une nouvelle classe ou interface doit être créée.

À titre d'exemple plus archétypal (qui est plus proche mais pas tout à fait OO), disons que nous voulons supprimer les doublons d'une liste. Disons que nous l'implémentons en itérant sur la liste en gardant une trace des éléments que nous avons vus jusqu'à présent dans une autre liste et en supprimant tous les éléments que nous avons vus. Il est raisonnable de supposer que nous pouvons vouloir changer la façon dont nous gardons la trace des éléments vus pour, au moins, pour des raisons de performances. Le dicton pour encapsuler ce qui varie suggère que nous devrions construire un type de données abstrait pour représenter l'ensemble des éléments vus. Notre algorithme est maintenant défini par rapport à ce type de données Set abstrait, et si nous décidons de passer à un arbre de recherche binaire, notre algorithme n'a pas besoin de changer ou de prendre soin. Dans un langage OO, nous pouvons utiliser une classe ou une interface pour capturer ce type de données abstrait. Dans un langage comme SML/O'Caml, vous pouvez plutôt capturer le type de données abstrait Set en tant que module.

Pour un exemple axé sur les exigences, disons que vous devez valider un champ par rapport à une logique métier. Bien que vous puissiez avoir des exigences spécifiques maintenant, vous soupçonnez fortement qu'elles évolueront. Vous pouvez encapsuler la logique actuelle dans sa propre procédure/fonction/règle/classe.

Bien qu'il s'agisse d'une préoccupation orthogonale qui ne fait pas partie de "l'encapsulation de ce qui varie", il est souvent naturel d'abstraire, c'est-à-dire de paramétrer, la logique désormais encapsulée. Cela conduit généralement à un code plus flexible et permet de changer la logique en remplaçant dans une implémentation alternative plutôt qu'en modifiant la logique encapsulée.

16

"Encapsuler ce qui varie" fait référence au masquage des détails de mise en œuvre qui peuvent changer et évoluer.

Exemple:

Par exemple, supposons que la classe Course garde une trace de Students qui peut enregistrer (). Vous pouvez l'implémenter avec un LinkedList et exposer le conteneur pour permettre son itération:

class Course { 
    public LinkedList<Student> Atendees; 
    public bool register (Student s);  
    ...
}

Mais ce n'est pas une bonne idée:

  • Premièrement, les gens peuvent manquer de bon comportement et l'utiliser comme libre-service, ajoutant directement des étudiants à la liste, sans passer par la méthode register ().
  • Mais encore plus ennuyeux: cela crée une dépendance du "code d'utilisation" avec les détails d'implémentation internes de la classe utilisée. Cela pourrait empêcher de futures évolutions de la classe, par exemple si vous préférez utiliser un tableau, un vecteur, une carte avec le numéro de siège ou votre propre structure de données persistante.

Si vous encapsulez ce qui varie (ou plutôt dites ce qui pourrait varier), vous gardez la liberté pour le code utilisateur et la classe encapsulée d'évoluer les uns les autres. C'est pourquoi c'est un principe important en POO.

Lecture supplémentaire:

11
Christophe