Comment adhérer au principe ouvert-fermé dans la pratique

14

Je comprends l'intention du principe ouvert-fermé. Il est destiné à réduire le risque de casser quelque chose qui fonctionne déjà en le modifiant, en vous disant d'essayer de l'étendre sans le modifier.

Cependant, j'ai eu du mal à comprendre comment ce principe est appliqué dans la pratique. À ma connaissance, il existe deux façons de l'appliquer. Avant et après un éventuel changement:

  1. Avant: programmez les abstractions et «prédisez l'avenir» autant que vous le pouvez. Par exemple, une méthode drive(Car car)devra changer si des Motorcycles sont ajoutés au système à l'avenir, donc elle viole probablement OCP. Mais la méthode drive(MotorVehicle vehicle)est moins susceptible de devoir changer à l'avenir, elle adhère donc à l'OCP.

    Cependant, il est assez difficile de prédire l'avenir et de savoir à l'avance quels changements vont être apportés au système.

  2. Après: lorsqu'une modification est nécessaire, étendez une classe au lieu de modifier son code actuel.

La pratique n ° 1 n'est pas difficile à comprendre. Cependant, c'est la pratique n ° 2 que j'ai du mal à comprendre comment postuler.

Par exemple (je l'ai pris à partir d' une vidéo sur YouTube): disons que nous avons une méthode dans une classe qui accepte des CreditCardobjets: makePayment(CraditCard card). Un jour Vouchers est ajouté au système. Cette méthode ne les prend pas en charge, elle doit donc être modifiée.

Lors de la mise en œuvre de la méthode, nous n'avons pas réussi à prédire l'avenir et à programmer en termes plus abstraits (par exemple makePayment(Payment pay), nous devons donc maintenant modifier le code existant.

La pratique # 2 dit que nous devrions ajouter la fonctionnalité en étendant au lieu de modifier. Qu'est-ce que ça veut dire? Dois-je sous-classer la classe existante au lieu de simplement changer son code existant? Dois-je faire une sorte de wrapper juste pour éviter de réécrire du code?

Ou le principe ne fait-il même pas référence à «comment modifier / ajouter correctement des fonctionnalités», mais plutôt à «comment éviter d'avoir à apporter des modifications en premier lieu (c'est-à-dire programmer des abstractions)?

Aviv Cohn
la source
1
Le principe ouvert / fermé ne dicte pas le mécanisme que vous utilisez. L'héritage n'est généralement pas le bon choix. De plus, il est impossible de se prémunir contre tous les changements futurs. Il est préférable de ne pas essayer de prédire l'avenir, mais une fois qu'un changement est nécessaire, modifiez la conception afin que de futurs changements du même type puissent être pris en compte.
Doval

Réponses:

14

Les principes de conception doivent toujours être mis en balance les uns avec les autres. Vous ne pouvez pas prédire l'avenir, et la plupart des programmeurs le font horriblement lorsqu'ils essaient. C'est pourquoi nous avons la règle de trois , qui concerne principalement la duplication, mais s'applique également à la refactorisation pour tout autre principe de conception.

Lorsque vous n'avez qu'une seule implémentation d'une interface, vous n'avez pas besoin de vous soucier beaucoup d'OCP, à moins qu'il ne soit clair où les extensions auraient lieu. En fait, vous perdez souvent la clarté lorsque vous essayez de sur-concevoir dans cette situation. Lorsque vous le prolongez une fois, vous le refactorisez pour le rendre compatible OCP si c'est la façon la plus simple et la plus claire de le faire. Lorsque vous l'étendez à une troisième implémentation, vous vous assurez de la refactoriser en tenant compte d'OCP, même si cela nécessite un peu plus d'efforts.

En pratique, lorsque vous n'avez que deux implémentations, la refactorisation lorsque vous en ajoutez une troisième n'est généralement pas trop difficile. C'est lorsque vous le laissez dépasser ce point qu'il devient difficile à entretenir.

Karl Bielefeldt
la source
1
Merci de répondre. Laissez-moi voir si je comprends ce que vous dites: ce que vous dites, c'est que je devrais me soucier de l'OCP principalement après avoir été contraint d'apporter des modifications à une classe. Signification: lors de l'implémentation d'une classe pour la première fois, je ne devrais pas trop m'inquiéter pour OCP, car il est difficile de prédire l'avenir de toute façon. Lorsque j'ai besoin de l'étendre / le modifier pour la première fois, c'est peut-être une bonne idée de le refactoriser un peu pour être plus flexible à l'avenir (plus d'OCP). Et dans la troisième fois, j'ai besoin d'étendre / modifier la classe, il est temps de refactoriser pour la rendre plus adhérente à l'OCP. C'est ce que tu veux dire?
Aviv Cohn
1
C'est l'idée.
Karl Bielefeldt
2

Je pense que vous regardez trop loin dans le futur. Résoudre le problème actuel d'une manière flexible qui adhère à ouvert / fermé.

Disons que vous devez implémenter une drive(Car car)méthode. Selon votre langue, vous avez deux options.

  • Pour les langages qui prennent en charge la surcharge (C ++), utilisez simplement drive(const Car& car)

    À un moment donné, vous en aurez peut-être besoin drive(const Motorcycle& motorcycle), mais cela ne gênera pas drive(const Car& car). Aucun problème!

  • Pour les langues qui ne prennent pas en charge la surcharge (Objective C), incluez ensuite le nom du type dans la méthode -driveCar:(Car *)car.

    À un moment donné, vous en aurez peut-être besoin -driveMotorcycle:(Motorcycle *)motorcycle, mais encore une fois, cela n'interfèrera pas.

Cela permet drive(Car car)d'être fermé à la modification, mais est ouvert à l'extension à d'autres types de véhicules. Cette planification future minimaliste qui vous permet de faire le travail aujourd'hui, mais vous empêche de vous bloquer à l'avenir.

Essayer d'imaginer les types les plus élémentaires dont vous avez besoin peut conduire à une régression infinie. Que se passe-t-il lorsque vous souhaitez conduire un Segue, un vélo ou un Jumbo jet. Comment construisez-vous un seul type abstrait générique qui peut rendre compte de tous les appareils que les gens utilisent et utilisent pour la mobilité?

Jeffery Thomas
la source
La modification d'une classe pour ajouter votre nouvelle méthode viole le principe ouvert-fermé. Votre suggestion élimine également la possibilité d'appliquer le principe de substitution Liskov à tous les véhicules qui peuvent conduire, ce qui élimine essentiellement l'une des parties les plus solides de l'OO.
Dunk
@Dunk J'ai basé ma réponse sur le principe ouvert / fermé polymorphe, pas sur le principe ouvert / fermé strict de Meyer. Il est possible de mettre à jour les classes pour prendre en charge de nouvelles interfaces. Dans cet exemple, l'interface de la voiture est séparée de l'interface de la moto. Celles-ci pourraient être formalisées sous forme de classes abstraites de conduite distinctes pour les voitures et les motos que la classe d'implémentation pourrait prendre en charge.
Jeffery Thomas
@Dunk Le principe de substitution de Liskov est utile, mais n'est pas gratuit. Si la spécification d'origine ne nécessite qu'une voiture, la création d'un véhicule plus générique peut ne pas valoir le coût supplémentaire, le temps et la complexité. De plus, il est peu probable qu'un véhicule plus générique soit parfaitement adapté pour gérer des sous-classes non planifiées. Soit l'interface pour une moto devra être intégrée à l'interface du véhicule (qui a été conçue pour gérer uniquement la voiture), soit vous devrez modifier le véhicule pour gérer la moto (une véritable violation de l'ouverture / fermeture).
Jeffery Thomas
Le principe de substitution de Liskov n'est pas gratuit, mais il n'est pas non plus très coûteux. Et généralement, cela rapporte beaucoup plus que ce qu'il en coûte de nombreuses fois, même si une autre sous-classe n'en est jamais héritée dans l'application principale. L'application de LSP facilite considérablement les tests automatisés, ce qui est déjà une victoire. De plus, bien que vous ne deviez certainement pas vous déchaîner et supposer que tout va avoir besoin de LSP, si vous créez une application et que vous n'avez pas une bonne idée de ce qui est susceptible d'en avoir besoin dans une future version, vous n'avez pas en savoir assez sur votre application ou son domaine.
Dunk
1
En ce qui concerne la définition de l'OCP. Ce sont peut-être les industries dans lesquelles j'ai travaillé, qui ont tendance à exiger des niveaux de vérification plus élevés qu'une simple entreprise commerciale ordinaire, mais de manière générale, si un fichier / classe change, vous devez non seulement retester le fichier / classe, mais tout ce qui utilise ce fichier / classe dans vos tests de régression. Donc, peu importe si quelqu'un dit que l'ouverture / la fermeture polymorphe est correcte, le changement d'interface a de vastes conséquences, donc ce n'est pas si bien.
Dunk
2

Je comprends l'intention du principe ouvert-fermé. Il est destiné à réduire le risque de casser quelque chose qui fonctionne déjà en le modifiant, en vous disant d'essayer de l'étendre sans le modifier.

Il s'agit également de ne pas casser tous les objets qui s'appuient sur cette méthode en ne modifiant pas le comportement des objets déjà existants. Une fois qu'un objet a annoncé un changement de comportement, il est risqué car vous modifiez le comportement connu et attendu de l'objet sans savoir exactement ce que les autres objets attendent de ce comportement.

Qu'est-ce que ça veut dire? Dois-je sous-classer la classe existante au lieu de simplement changer son code existant?

Ouaip.

"N'accepte que les cartes de crédit" est défini comme faisant partie du comportement de cette classe, via son interface publique. Le programmeur a déclaré au monde que la méthode de cet objet ne prend que les cartes de crédit. Elle l'a fait en utilisant un nom de méthode peu clair, mais c'est fait. Le reste du système s'appuie sur cela.

Cela avait peut-être du sens à l'époque, mais maintenant, si cela doit changer maintenant, vous devriez créer une nouvelle classe qui accepte des choses autres que les cartes de crédit.

Nouveau comportement = nouvelle classe

En aparté - Une bonne façon de prédire l'avenir est de penser au nom que vous avez donné à une méthode. Avez-vous donné un nom de méthode de sondage vraiment général comme makePayment à une méthode avec des règles spécifiques dans la méthode quant au paiement exact qu'elle peut effectuer? C'est une odeur de code. Si vous avez des règles spécifiques, celles-ci doivent être clarifiées par le nom de la méthode - makePayment doit être makeCreditCardPayment. Faites-le lorsque vous écrivez l'objet la première fois et d'autres programmeurs vous en remercieront.

Cormac Mulhall
la source