Tirez-vous parti des avantages du principe ouvert-fermé?

12

Le principe ouvert-fermé (OCP) stipule qu'un objet doit être ouvert pour extension mais fermé pour modification. Je crois que je le comprends et l'utilise en conjonction avec SRP pour créer des classes qui ne font qu'une seule chose. Et, j'essaie de créer de nombreuses petites méthodes qui permettent d'extraire tous les contrôles de comportement dans des méthodes qui peuvent être étendues ou remplacées dans certaines sous-classes. Ainsi, je me retrouve avec des classes qui ont de nombreux points d'extension, que ce soit à travers: injection et composition de dépendances, événements, délégation, etc.

Considérez ce qui suit comme une classe simple et extensible:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Supposons maintenant, par exemple, que la OvertimeFactormodification passe à 1.5. Étant donné que la classe ci-dessus a été conçue pour être étendue, je peux facilement sous-classer et renvoyer un autre OvertimeFactor.

Mais ... en dépit du fait que la classe est conçue pour l'extension et adhère à l'OCP, je modifierai la méthode unique en question, plutôt que de sous-classer et de remplacer la méthode en question, puis de recâbler mes objets dans mon conteneur IoC.

En conséquence, j'ai violé une partie de ce que OCP tente d'accomplir. J'ai l'impression d'être juste paresseux parce que ce qui précède est un peu plus facile. Suis-je incompréhensible OCP? Dois-je vraiment faire quelque chose de différent? Tirez-vous parti des avantages de l'OCP différemment?

Mise à jour : sur la base des réponses, il semble que cet exemple artificiel soit médiocre pour un certain nombre de raisons différentes. L'objectif principal de l'exemple était de démontrer que la classe a été conçue pour être étendue en fournissant des méthodes qui, en cas de substitution, modifieraient le comportement des méthodes publiques sans qu'il soit nécessaire de modifier le code interne ou privé. Pourtant, j'ai certainement mal compris OCP.

Kaleb Pederson
la source

Réponses:

10

Si vous modifiez la classe de base, elle n'est pas vraiment fermée, n'est-ce pas!

Pensez à la situation où vous avez publié la bibliothèque dans le monde. Si vous allez changer le comportement de votre classe de base en modifiant le facteur d'heures supplémentaires à 1,5, vous avez violé toutes les personnes qui utilisent votre code en supposant que la classe était fermée.

Vraiment, pour rendre la classe fermée mais ouverte, vous devriez récupérer le facteur d'heures supplémentaires à partir d'une autre source (fichier de configuration peut-être) ou prouver une méthode virtuelle qui peut être remplacée?

Si le cours était vraiment fermé, après votre changement, aucun scénario de test n'échouerait (en supposant que vous avez une couverture à 100% avec tous vos scénarios de test) et je suppose qu'il existe un scénario de test qui vérifie GetOvertimeFactor() == 2.0M.

Ne pas trop ingénieur

Mais n'amenez pas ce principe d'ouverture-fermeture à la conclusion logique et ayez tout configurable dès le départ (c'est-à-dire au-delà de l'ingénierie). Définissez uniquement les bits dont vous avez actuellement besoin.

Le principe fermé ne vous empêche pas de réorganiser l'objet. Il vous empêche simplement de changer l'interface publique actuellement définie en votre objet ( les membres protégés font partie de l'interface publique). Vous pouvez toujours ajouter plus de fonctionnalités tant que l'ancienne fonctionnalité n'est pas rompue.

Martin York
la source
"Le principe fermé ne vous empêche pas de réorganiser l'objet." En fait, c'est le cas . Si vous lisez le livre où le principe ouvert-fermé a été proposé pour la première fois, ou l' article qui a introduit l'acronyme "OCP", vous verrez qu'il dit que "Personne n'est autorisé à y apporter des modifications de code source" (sauf pour le bogue correctifs).
Rogério
@ Rogério: C'est peut-être vrai (en 1988). Mais la définition actuelle (rendue populaire dans les années 1990 lorsque OO est devenue populaire) consiste à maintenir une interface publique cohérente. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York
Merci pour la référence Wikipedia. Mais je ne suis pas sûr que la définition "actuelle" soit vraiment différente, car elle repose toujours sur l'héritage de type (classe ou interface). Et cette citation «sans changement de code source» que j'ai mentionnée provient de l'article OCP 1996 de Robert Martin qui est (soi-disant) conforme à la «définition actuelle». Personnellement, je pense que le principe ouvert-fermé serait désormais oublié si Martin ne lui avait pas donné un acronyme qui, apparemment, a beaucoup de valeur marketing. Le principe lui-même est dépassé et nuisible, OMI.
Rogério
3

Le principe Open Closed est donc un piège ... surtout si vous essayez de l'appliquer en même temps que YAGNI . Comment adhérer aux deux en même temps? Appliquez la règle des trois . La première fois que vous effectuez un changement, faites-le directement. Et la deuxième fois aussi. La troisième fois, il est temps d'abstraire ce changement.

Une autre approche consiste à "me tromper une fois ...", lorsque vous devez effectuer un changement, appliquez l'OCP pour vous protéger contre ce changement à l'avenir . J'irais presque jusqu'à proposer que la modification du taux des heures supplémentaires soit une nouvelle histoire. "En tant qu'administrateur de la paie, je souhaite modifier le taux des heures supplémentaires afin d'être en conformité avec les lois du travail applicables". Vous avez maintenant une nouvelle interface utilisateur pour modifier le taux d'heures supplémentaires, un moyen de le stocker, et GetOvertimeFactor () demande simplement à son référentiel quel est le taux d'heures supplémentaires.

Michael Brown
la source
2

Dans l'exemple que vous avez publié, le facteur d'heures supplémentaires doit être une variable ou une constante. * (Exemple Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

OU

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Ensuite, lorsque vous étendez la classe, définissez ou remplacez le facteur. "Les nombres magiques" ne devraient apparaître qu'une seule fois. C'est beaucoup plus dans le style d'OCP et de DRY (Don't Repeat Yourself), car il n'est pas nécessaire de créer une toute nouvelle classe pour un facteur différent si vous utilisez la première méthode et que vous n'avez qu'à changer la constante dans un idiomatique placer dans le second.

J'utiliserais le premier dans les cas où il y aurait plusieurs types de calculatrice, chacune nécessitant des valeurs constantes différentes. Un exemple serait le modèle de chaîne de responsabilité, qui est généralement implémenté à l'aide de types hérités. Un objet qui ne peut voir que l'interface (c'est-à-dire getOvertimeFactor()) l'utilise pour obtenir toutes les informations dont il a besoin, tandis que les sous-types s'inquiètent des informations réelles à fournir.

La seconde est utile dans les cas où la constante n'est pas susceptible d'être modifiée, mais est utilisée à plusieurs endroits. Avoir une constante à changer (dans le cas peu probable où elle le fait) est beaucoup plus facile que de la définir partout ou de l'obtenir à partir d'un fichier de propriétés.

Le principe Open-closed est moins un appel à ne pas modifier un objet existant qu'une mise en garde de leur laisser l'interface inchangée. Si vous avez besoin d'un comportement légèrement différent d'une classe ou de fonctionnalités supplémentaires pour un cas spécifique, étendez et remplacez. Mais si les exigences de la classe elle-même changent (comme changer le facteur), vous devez changer la classe. Il n'y a aucun intérêt dans une énorme hiérarchie de classes, dont la plupart ne sont jamais utilisées.

Michael K
la source
Il s'agit d'un changement de données, pas d'un changement de code. Le taux des heures supplémentaires n'aurait pas dû être codé en dur.
Jim C
Vous semblez avoir votre Get et votre Set à l'envers.
Mason Wheeler
Oups! aurait dû tester ...
Michael K
2

Je ne vois pas vraiment votre exemple comme une excellente représentation d'OCP. Je pense que la règle veut vraiment dire ceci:

Lorsque vous souhaitez ajouter une fonctionnalité, vous ne devez ajouter qu'une seule classe et vous ne devez modifier aucune autre classe (mais éventuellement un fichier de configuration).

Une mauvaise mise en œuvre ci-dessous. Chaque fois que vous ajoutez un jeu, vous devez modifier la classe GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

La classe GamePlayer ne devrait jamais avoir besoin d'être modifiée

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Maintenant, en supposant que ma GameFactory respecte également OCP, lorsque je veux ajouter un autre jeu, il me suffirait de créer une nouvelle classe qui hérite de la Gameclasse et tout devrait simplement fonctionner.

Trop souvent, des classes comme la première se construisent après des années d '"extensions" et ne sont jamais correctement refactorisées à partir de la version originale (ou pire, ce qui devrait être plusieurs classes reste une grande classe).

L'exemple que vous fournissez est OCP-ish. À mon avis, la bonne façon de gérer les changements de taux d'heures supplémentaires serait dans une base de données avec des taux historiques conservés afin que les données puissent être retraitées. Le code doit toujours être fermé pour modification car il chargerait toujours la valeur appropriée à partir de la recherche.

Comme exemple du monde réel, j'ai utilisé une variante de mon exemple et le principe ouvert-fermé brille vraiment. La fonctionnalité est vraiment facile à ajouter car je dois juste dériver d'une classe de base abstraite et mon "usine" la récupère automatiquement et le "joueur" ne se soucie pas de l'implémentation concrète que l'usine renvoie.

Austin Salonen
la source
1

Dans cet exemple particulier, vous avez ce que l'on appelle une «valeur magique». Essentiellement une valeur codée en dur qui peut ou non changer avec le temps. Je vais essayer de résoudre l'énigme que vous exprimez de manière générique, mais c'est un exemple du type de chose où la création d'une sous-classe est plus de travail que de changer une valeur dans une classe.

Plus que probablement, vous avez spécifié un comportement trop tôt dans votre hiérarchie de classes.

Disons que nous avons le PaycheckCalculator. Ces informations OvertimeFactorseraient très probablement supprimées des informations sur l'employé. Un employé à l'heure peut bénéficier d'une prime pour heures supplémentaires, tandis qu'un employé ne touche rien. Pourtant, certains salariés auront droit à l'heure en raison du contrat sur lequel ils travaillaient. Vous pouvez décider qu'il existe certaines classes connues de scénarios de rémunération, et c'est ainsi que vous construirez votre logique.

Dans la PaycheckCalculatorclasse de base , vous la rendez abstraite et spécifiez les méthodes que vous attendez. Les calculs de base sont les mêmes, c'est juste que certains facteurs sont calculés différemment. Votre HourlyPaycheckCalculatorimplémenterait alors la getOvertimeFactorméthode et retournerait 1.5 ou 2.0 selon votre cas. Votre StraightTimePaycheckCalculatorimplémenterait le getOvertimeFactorpour retourner 1.0. Enfin une troisième implémentation serait NoOvertimePaycheckCalculatorcelle qui implémenterait le getOvertimeFactorpour retourner 0.

La clé est de décrire uniquement le comportement dans la classe de base qui doit être étendue. Les détails des parties de l'algorithme global ou des valeurs spécifiques seraient remplis par des sous-classes. Le fait que vous ayez inclus une valeur par défaut pour le getOvertimeFactorconduit à la "correction" rapide et facile de la ligne au lieu d'étendre la classe comme vous le souhaitiez. Il met également en évidence le fait que des efforts sont déployés pour étendre les classes. La compréhension de la hiérarchie des classes dans votre application nécessite également des efforts. Vous souhaitez concevoir vos classes de manière à minimiser le besoin de créer des sous-classes tout en offrant la flexibilité dont vous avez besoin.

Matière à réflexion: lorsque nos classes encapsulent certains facteurs de données comme le OvertimeFactordans votre exemple, vous pourriez avoir besoin d'un moyen d'extraire ces informations d'une autre source. Par exemple, un fichier de propriétés (car cela ressemble à Java) ou une base de données contiendrait la valeur, et votre PaycheckCalculatorutiliserait un objet d'accès aux données pour extraire vos valeurs. Cela permet aux bonnes personnes de changer le comportement du système sans nécessiter de réécriture de code.

Berin Loritsch
la source