As a preface, I want to say a few words about myself. I am a software engineer by training, I have been working in the IT industry for more than 10 years and recently I have been fond of writing thematic professional articles. Some of them were successful. Earlier, I published on another resource, unfortunately, inaccessible in Russia (greetings to Roskomnadzor). If someone wants to get to know them, you know what to do.
All code examples, as usual, are presented in the article with pseudo-code stylized as “hated php”.
Initial task
To make it faster and more understandable, we immediately turn to the example. From the sales department I wanted to see the metrics: how much money we earn monthly, daily, hourly.
We solve this problem with the help of three teams that are run periodically on a schedule:
- MonthlyReportCommand
- DailyReportCommand
- HourlyRerortCommand
We will need the 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); }
We are writing report teams (the last one is omitted, as a practical exercise for those who want to thoroughly understand and practice writing it yourself):
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 }
And we see that the code of the calculateTotals () method will be exactly the same in all cases. The first thing that comes to mind is to put duplicate code in a common abstract class. Like this:
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 }
The calculateTotals () method is part of the internal mechanisms of our class. We prudently close it, because it should not be called by external customers - we are not designing it for that. We declare this method protected, because We plan to call him in the heirs - this is our goal. Obviously, such an abstract class is very similar to something like a library - it just provides some methods (for php experts: that is, it works like Trait).
The secret of abstract classes
It is time to take a break from the example and recall the purpose of abstract classes:
An abstract class encapsulates general mechanisms, while at the same time allowing heirs to implement their own particular behavior.
Abstraction (lat. Abstractio - distraction) is a distraction from details and generalization. At the moment, the AbstractReportCommand class generalizes only counting money for all reports. But we can make our abstraction more efficient by using the Hollywood principle, which sounds like this:
“Do not call us, we will call you ourselves”
To see how this works, let's put in AbstractReportCommand a general reporting mechanism:
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 }
What did we do? None of the descendants of an abstract class apply to common mechanisms (do not call us). Instead, abstraction gives its heirs a general functioning scheme and requires them to implement particular behavioral features, using only the results (we will challenge you).
But what about the promised IoC, LSP, private vs protected?
So what does the Inversion of control have to do with it? Where does this name come from? Very simple: first we set the sequence of calls directly in the final implementations, controlling what will be done and when. And later on, we transferred this logic to a general abstraction. Now abstraction controls what and when will be called, and implementations simply obey this. That is, we inverted the control.
To fix this behavior and avoid problems with the Barbara Liskov substitution principle (LSP) , you can close the createReport () method by including final in the method declaration. After all, everyone knows that LSP is directly related to inheritance.
abstract class AbstractReportCommand implements ReportCommandInterface { final public function createReport(): Money { //bla-bla realization } ... }
Then all the descendants of the AbstractReportCommand class become rigidly subordinate to a single logic that cannot be redefined. Iron discipline, order, bright future.
For the same reason, the advantage of private over protected becomes apparent. Everything related to general functioning mechanisms should be wired in an abstract class and not accessible to redefinition - private. All that needs to be redefined / implemented in special cases is abstract protected. Any methods are designed for specific purposes. And if you don’t know what kind of scope to set for a method, it means that you don’t know why you are creating it. This design is worth revising.
conclusions
The construction of abstract classes is always preferable with the use of Inversion of control, since allows you to use the idea of abstraction to the fullest. But the use of abstract classes as libraries in some cases can also be justified.
If you look more broadly, then our small-town confrontation between the Hollywood principle and the abstract library class turns into a dispute: a framework (adult IoC) vs a library. There is no point in proving which of them is better - each is created for a specific purpose. The only important thing is the conscious creation of such structures.
Thanks to everyone who carefully read from start to finish - you are my favorite readers.