Principe d'Hollywood (IoC)

Dans cet article, je vais essayer de parler du principe de conception appelé Inversion of Control / IoC, également appelé principe Hollywood. Je montrerai comment cela se rapporte au principe de substitution de Barbara Liskovo (LSP) , ainsi que de contribution à la guerre sainte privée vs protégée.



En préface, je veux dire quelques mots sur moi. Je suis ingénieur logiciel de formation, je travaille dans l'industrie informatique depuis plus de 10 ans et j'ai récemment adoré écrire des sujets professionnels. Certains ont réussi. Plus tôt, j'ai publié sur une autre ressource, malheureusement inaccessible en Russie (salutations à Roskomnadzor). Si quelqu'un veut les connaître, vous savez quoi faire.

Tous les exemples de code, comme d'habitude, sont présentés dans l'article avec un pseudo-code stylisé comme «php détesté».

Tâche initiale


Pour le rendre plus rapide et plus compréhensible, nous passons immédiatement à l'exemple. Du service des ventes, je voulais voir les métriques: combien d'argent nous gagnons mensuellement, quotidiennement, toutes les heures.

Nous résolvons ce problème avec l'aide de trois équipes qui sont exécutées périodiquement selon un calendrier:

  • MonthlyReportCommand
  • DailyReportCommand
  • HourlyRerortCommand

Nous aurons besoin des interfaces:

interface ReportCommandInterface { public function createReport(): Money; } interface MoneyRepositoryInterface { /** @return Money[] */ public function getMoney(Period $period): array; } interface MetricRepositoryInterface { public function saveMoneyMetric(Period $period, Money $amount, string $metricType); } 

Nous rédigeons des équipes de rapport (la dernière est omise, comme exercice pratique pour ceux qui veulent bien comprendre et bien pratiquer, écrivez-le vous-même):

 class MonthlyReportCommand implements ReportCommandInterface { //lets assume constructor is already here public function createReport(): Money { $period = new Period(new DateTime('first day of previous month'), new DateTime('last day of previous month')); $moneyRecords = $this->moneyRepository->getMoney($period); $amount = $this->calculateTotals($moneyRecords); $this->metricRepository->saveMoneyMetric($period, $amount, 'monthly income'); } /** @param Money[] $moneyRecords */ private function calculateTotals(array $moneyRecords): Money { //here is calculating sum of money records } } class DailyReportCommand implements ReportCommandInterface { //lets assume constructor is already here public function createReport(): Money { $period = new Period(new DateTime('yesterday'), new DateTime('today')); $moneyRecords = $this->moneyRepository->getMoney($period); $amount = $this->calculateTotals($moneyRecords); $this->metricRepository->saveMoneyMetric($period, $amount, 'daily income'); } /** @param Money[] $moneyRecords */ private function calculateTotals(array $moneyRecords): Money { //here calculates sum of money records } } class HourlyReportCommand ... { //the same as previous two but hourly } 

Et nous voyons que le code de la méthode CalculateTotals () sera exactement le même dans tous les cas. La première chose qui me vient à l'esprit est de mettre du code en double dans une classe abstraite commune. Comme ça:



 abstract class AbstractReportCommand { protected function calculateTotals(array $moneyRecords): Money { //here calculates sum of money records } } class MonthlyReportCommand extends AbstractReportCommand implements ReportCommandInterface { public function createReport(): Money { //realization is here, calls calculateTotals($moneyRecords) } } class DailyReportCommand extends AbstractReportCommand implements ReportCommandInterface { //the same as previous two but daily } class HourlyReportCommand ... { //the same as previous two but hourly } 

La méthode CalculateTotals () fait partie des mécanismes internes de notre classe. Nous le fermons prudemment, car il ne doit pas être appelé par des clients externes - nous ne le concevons pas pour cela. Nous déclarons cette méthode protégée, car Nous prévoyons de l'appeler parmi les héritiers - c'est notre objectif. De toute évidence, une telle classe abstraite est très similaire à quelque chose comme une bibliothèque - elle fournit simplement quelques méthodes (pour les experts php: c'est-à-dire qu'elle fonctionne comme Trait).

Le secret des classes abstraites


Il est temps de faire une pause dans l'exemple et de rappeler le but des classes abstraites:

Une classe abstraite encapsule les mécanismes généraux, tout en permettant aux héritiers de mettre en œuvre leur propre comportement particulier.

L'abstraction (lat. Abstractio - distraction) est une distraction des détails et de la généralisation. Pour le moment, la classe AbstractReportCommand ne généralise que l'argent comptant pour tous les rapports. Mais nous pouvons rendre notre abstraction plus efficace en utilisant le principe d'Hollywood, qui ressemble à ceci:

"Ne nous appelez pas, nous vous appellerons nous-mêmes"

Pour voir comment cela fonctionne, mettons dans AbstractReportCommand un mécanisme de rapport général:



 abstract class AbstractReportCommand implements ReportCommandInterface { /** @var MoneyRepositoryInterface */ private $moneyRepository; /** @var MetricRepositoryInterface */ private $metricRepository; //lets assume constructor is already here public function createReport(): Money { $period = $this->getPeriod(); $metricType = $this->getMetricType(); $moneyRecords = $this->moneyRepository->getMoney($period); $amount = $this->calculateTotals($moneyRecords); $this->metricRepository->saveMoneyMetric($period, $amount, $metricType); } abstract protected function getPeriod(): Period; abstract protected function getMetricType(): string; private function calculateTotals(array $moneyRecords): Money { //here calculates sum of money records } } class MonthlyReportCommand extends AbstractReportCommand { protected function getPeriod(): Period { return new Period(new DateTime('first day of previous month'), new DateTime('last day of previous month')); } protected function getMetricType(): string { return 'monthly income'; } } class DailyReportCommand extends AbstractReportCommand { protected function getPeriod(): Period { return new Period(new DateTime('yesterday'), new DateTime('today')); } protected function getMetricType(): string { return 'daily income'; } } class HourlyReportCommand ... { //the same as previous two but hourly } 

Qu'avons-nous fait? Aucun des descendants d'une classe abstraite ne s'applique aux mécanismes communs (ne nous appelez pas). Au lieu de cela, l'abstraction donne à ses héritiers un schéma de fonctionnement général et les oblige à implémenter des caractéristiques comportementales particulières, en utilisant uniquement les résultats (nous vous mettrons au défi).

Mais qu'en est-il de l'IoC, LSP, privé ou protégé promis?


Alors qu'est-ce que l'inversion de contrôle a à voir avec ça? D'où vient ce nom? Très simple: nous définissons d'abord la séquence d'appels directement dans les implémentations finales, en contrôlant ce qui sera fait et quand. Et plus tard, nous avons transféré cette logique à une abstraction générale. Maintenant, l'abstraction contrôle ce qui sera appelé et quand, et les implémentations obéiront simplement à cela. Autrement dit, nous avons inversé le contrôle.

Pour résoudre ce problème et éviter les problèmes avec le principe de substitution Barbara Liskov (LSP) , vous pouvez fermer la méthode createReport () en incluant final dans la déclaration de méthode. Après tout, tout le monde sait que le LSP est directement lié à l'héritage.

 abstract class AbstractReportCommand implements ReportCommandInterface { final public function createReport(): Money { //bla-bla realization } ... } 

Tous les descendants de la classe AbstractReportCommand deviennent alors subordonnés de manière rigide à une logique unique qui ne peut pas être redéfinie. Discipline de fer, ordre, avenir radieux.

Pour la même raison, l'avantage du privé sur le protégé devient apparent. Tout ce qui concerne les mécanismes de fonctionnement général doit être câblé dans une classe abstraite et non accessible à la redéfinition - privé. Tout ce qui doit être redéfini / implémenté dans des cas particuliers est protégé de manière abstraite. Toutes les méthodes sont conçues à des fins spécifiques. Et si vous ne savez pas quel type d’étendue définir pour une méthode, cela signifie que vous ne savez pas pourquoi vous la créez. Cette conception mérite d'être révisée.

Conclusions


La construction de classes abstraites est toujours préférable avec l'utilisation de l'inversion de contrôle, car vous permet d'utiliser au maximum l'idée de l'abstraction. Mais l'utilisation de classes abstraites comme bibliothèques dans certains cas peut également être justifiée.

Si vous regardez plus largement, alors notre confrontation dans une petite ville entre le principe d'Hollywood et la classe de bibliothèque abstraite se transforme en un différend: un cadre (IoC pour adultes) contre une bibliothèque. Il est inutile de prouver laquelle est la meilleure - chacune est créée dans un but précis. La seule chose importante est la création consciente de telles structures.

Merci à tous ceux qui ont lu attentivement du début à la fin - vous êtes mes lecteurs préférés.

Source: https://habr.com/ru/post/fr477700/


All Articles