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 OvertimeFactor
modification 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.
la source
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_principleLe 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.
la source
Dans l'exemple que vous avez publié, le facteur d'heures supplémentaires doit être une variable ou une constante. * (Exemple Java)
OU
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.
la source
Je ne vois pas vraiment votre exemple comme une excellente représentation d'OCP. Je pense que la règle veut vraiment dire ceci:
Une mauvaise mise en œuvre ci-dessous. Chaque fois que vous ajoutez un jeu, vous devez modifier la classe GamePlayer.
La classe GamePlayer ne devrait jamais avoir besoin d'être modifiée
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
Game
classe 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.
la source
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.
Disons que nous avons le
PaycheckCalculator
. Ces informationsOvertimeFactor
seraient 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
PaycheckCalculator
classe 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. VotreHourlyPaycheckCalculator
implémenterait alors lagetOvertimeFactor
méthode et retournerait 1.5 ou 2.0 selon votre cas. VotreStraightTimePaycheckCalculator
implémenterait legetOvertimeFactor
pour retourner 1.0. Enfin une troisième implémentation seraitNoOvertimePaycheckCalculator
celle qui implémenterait legetOvertimeFactor
pour 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
getOvertimeFactor
conduit à 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
OvertimeFactor
dans 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 votrePaycheckCalculator
utiliserait 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.la source