Skip to content

atlance/solid-principles

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

SOLID «S»: Принцип единственной обязанности (single responsibility principle, SPR)

Принцип единственной обязанности - один из принципов, которого следует придерживаться при написании кода. Он декларирует, что каждый объект должен иметь одну единственную обязанность и эта обязанность должна быть инкапсулирована в класс.

Термин «SOLID» представляет собой аббревиатуру пяти важнейших принципов работы с классами в объектно-ориентированном проектировании:

  1. Принципа единственной обязанности
  2. Принципа открытости/закрытости
  3. Принципа подстановки Барбары Лисков
  4. Принципа разделения интерфейса
  5. Принципа инверсии зависимостей

Этими пятью гибкими принципами следует руководствоваться при написании кода.

Определение

Класс должен иметь одну и только одну причину для изменений.

Это один из 5 гибких принципов SOLID, определенных в книге «Быстрая разработка программ. Принципы, примеры, практика» Робертом С. Мартином. Затем эта книга была переиздана в версии для C# «Принципы, паттерны и методики гибкой разработки на языке C#». То, что декларирует данный принцип, вполне легко понять, однако не так легко  реализовать на практике.

Класс должен иметь одну единственную причину для изменений. Но чем обусловлена подобная необходимость? В компилируемых статически типизированных языках программирования существование нескольких причин может повлечь за собой ряд нежелательных изменений.

Две различные причины для изменений подобны двум различным командам, которые могут работать с одним кодом, и каждая из них будет разворачивать свое собственное решение, которое в случае C++, C# Java или других компилируемых языков, может привести к несовместимости между различными частями приложения.

Даже если вы не используете компилируемый язык, вам, возможно, потребуется повторное тестирование определенного класса или модуля по различным причинам. Это означает, что вы потратите больше времени и усилий.

Аудитория

Определение одной единственной ответственности, которую должен иметь класс или модуль, является гораздо более сложной задачей, чем простое просматривание списка.

Например, один из способов поиска причин изменения - анализ аудитории класса. Пользователи приложения, которые обслуживаются определенным модулем, будут являться теми лицами, которые требуют изменений класса. Вот несколько модулей и их возможная аудитория.

  1. Модуль сохраняемости - аудитория включает администраторов баз данных и проектировщиков ПО.
  2. Модуль отчетности - аудитория включает клерков, бухгалтеров и операционистов.
  3. Модуль расчета платежей для системы расчета заработной платы - аудитория может включать юристов, менеджеров и бухгалтеров.
  4. Модуль поиска книг в библиотеке - аудитория может включать библиотекаря и читателей.

Роли и актеры

Ассоциирование конкретных лиц с ролями – весьма непростая задача. В небольшой фирме одно единственное лицо может играть сразу несколько ролей, в то время как в крупной организации может быть привлечено несколько исполнителей для выполнения одной функции. Поэтому, пожалуй, будет немаловажно подумать об этих ролях. Однако определить сами роли бывает достаточно сложно. Гораздо легче обозначить актёров, играющих эти роли и связать актёров и нашу аудиторию.

Таким образом, если аудитория обусловливает причины изменения, актёры определяют аудиторию. Это позволит нам свести понятия конкретных лиц вроде «Архитектора Джона» или «Секретаря Марии» к операциям.

Таким образом, согласно Роберту С. Мартину, ответственность - это определенный набор функций, которые выполняет один взятый актёр.

Источник изменений

Рассуждая таким образом, можно сделать вывод о том, что актёры представляют собой источник изменений для набора функций, которые удовлетворяют потребности самих актёров. Наряду с изменением потребностей актёров, этот специфичный набор функций тоже должен измениться и приспособиться под новые потребности актеров.

Актер для ответственности - единственный источник изменений этой ответственности. (Роберт С. Мартин)

Классические примеры

Объекты, которые могут распечатывать сами себя

Предположим, что у нас есть класс Book, который инкапсулирует в себе книгу вместе с её функциональностью.

<?php

class Book
{
    public function getTitle()
    {
        return "A Great Book";
    }
 
    public function getAuthor()
    {
        return "John Doe";
    }
 
    public function turnPage()
    {
        // pointer to next page
    }
 
    public function printCurrentPage()
    {
        echo "current page content";
    }
}

Это может выглядеть как целесообразный класс. У нас есть книга, которая может предоставлять информацию о своем названии, авторе и способна перелистывать страницы. Также этот класс может отображать на экране текущую страницу книгу. Однако существует маленькая проблема в определении актёров, которые могли бы быть вовлечены в управление объектом Book. Сходу можно назвать двух различных актёров: Управление книгой (к примеру, библиотекарь) и Механизм представления данных (например, способ, с помощью которого мы планируем выводить содержимое пользователю - на экран, в графическом виде, только текст или же распечатывать). Существует значительное различие между этими двумя актерами.

Разделяй и властвуй

Совмещение бизнес-логики с представлением является крайне нежелательным, т.к. это будет противоречить принципу единой ответственности (ПЕО). Рассмотрим код ниже:

<?php

class Book
{
    public function getTitle()
    {
        return "A Great Book";
    }

    public function getAuthor()
    {
        return "John Doe";
    }

    public function turnPage()
    {
        // pointer to next page
    }

    public function getCurrentPage()
    {
        return "current page content";
    }

}

interface Printer
{
    public function printPage($page);
}

class PlainTextPrinter implements Printer
{
    public function printPage($page)
    {
        echo $page;
    }
}

class HtmlPrinter implements Printer
{
    public function printPage($page)
    {
        echo $page;
    }
}

Даже по такому простейшему примеру можно сделать вывод о том, как правильно разграничивать бизнес-логику и представление. Придерживаясь ПЕО, можно построить гораздо более гибкую архитектуру.

Объекты, которые способны «сохранять» себя

Еще один похожий на предыдущий вариант пример того, как объект может сам себя сохранять и восстанавливаться из представления.

<?php

class Book
{
    public function getTitle()
    {
        return "A Great Book";
    }

    public function getAuthor()
    {
        return "John Doe";
    }

    public function turnPage()
    {
        // pointer to next page
    }

    public function getCurrentPage()
    {
        return "current page content";
    }

    public function save()
    {
        $filename = '/documents/'. $this->getTitle(). ' - ' . $this->getAuthor();
        file_put_contents($filename, serialize($this));
    }
}

Мы снова можем определить несколько актёров, например, Систему управления книгой и Сохраняемость. Этот класс подлежит модификации каждый раз при измении сохраняемости или способа перелистывания страницы. Можно отметить несколько вариантов изменения данных.

<?php

class Book
{
    public function getTitle()
    {
        return "A Great Book";
    }

    public function getAuthor()
    {
        return "John Doe";
    }

    public function turnPage()
    {
        // pointer to next page
    }

    public function getCurrentPage()
    {
        return "current page content";
    }

}

class SimpleFilePersistence
{
    public function save(Book $book)
    {
        $filename = '/documents/' . $book->getTitle() . ' - ' . $book->getAuthor();
        file_put_contents($filename, serialize($book));
    }
}

Переместив метод сохранения объекта в другой класс, мы сможем явно разделить ответственность и легко изменить данный метод сохраняемости, никак не влияя на класс Book. Так, внедрение класса DatabasePersistence будет абсолютно тривиальным, и наша бизнес-логика, выстроенная вокруг действий с книгой никак не изменится.

Высокоуровневое представление

В своих предыдущих статьях я не раз упоминал и предлагал Вашему вниманию схему высокоуровневой архитектуры, которая приведена ниже.

Если мы проанализируем данную схему, то сразу поймем, как соблюдается ПЕО. Создание нового объекта обозначено с правой стороны схемы с помощью «Фабрик» (Factories) и единой точки входа нашего приложения (Main). Один актёр - одна ответственность. О сохраняемости (Persistence) также позаботились, расположив ее внизу. Отдельный модуль предназначается для отдельной ответственности. И наконец, с левой стороны мы разместили представление, или механизм доставки, в виде MVC или каком-либо другом типе пользовательского интерфейса. И вновь соблюден принцип единой ответственности. Все, что нам остается выяснить, - это что делать с самой бизнес-логикой.

Вопросы проектирования программного обеспечения

Когда мы обдумываем, как лучше написать программное обеспечение, нам обычно приходится принимать во внимание множество различных аспектов. Например, несколько требований, предъявляемых к классу и оказывающих на него влияние, могут быть представлены в виде оси изменений. Эти оси изменений могут играть роль ключа к единой ответственности. Вполне вероятно, что группы требований, влияющих на ту же группу функций, будут иметь причины для изменений или могут быть выдвинуты на первый план.

Главное достоинство ПО - простота внесения изменений. Следующее важное качество - функциональность с точки зрения способности ПО удовлетворять как можно более широкий круг потребностей пользователей. Однако для достижения высокого значения на втором уровне, сначала нужно обязательно выяснить значение первого критерия. Чтобы поднять значение первичного критерия на должный уровень, мы должны, соблюдая ПЕО, построить такую архитектуру, которую можно легко модифицировать, расширять и в которую можно быстро добавлять новую функциональность.

Изложим по шагам:

Значение первого критерия должно быть установлено до определения значения второго критерия.

  1. Второй критерий отвечает за удовлетворение потребностей пользователей.
  2. Потребности пользователей – это потребности актёров.
  3. Потребности актёров определяют необходимость изменения этих актёров.
  4. Потребности в изменениях актёров, в свою очередь, определяют нашу ответственность.

Таким образом, в процессе разработки архитектуры нашего программного обеспечения, мы должны:

  1. Определить актёров.
  2. Выявить область ответственности каждого актёра.
  3. Сгруппировать классы и функции так, чтобы каждый из них отвечал только за свою часть.

Менее очевидный пример

<?php

class Book
{
    public function getTitle()
    {
        return "A Great Book";
    }

    public function getAuthor()
    {
        return "John Doe";
    }

    public function turnPage()
    {
        // pointer to next page
    }

    public function getCurrentPage()
    {
        return "current page content";
    }

    public function getLocation()
    {
        // returns the position in the library
        // ie. shelf number & room number
    }
}

Теперь это выглядит вполне очевидно и рационально. У нас нет метода, который относился бы к сохранению или отображению данных. Мы располагаем функционалом метода turnPage() и еще несколькими методами, позволяющими предоставить необходимые сведения о книге. Однако мы можем столкнуться с некоторой проблемой. Чтобы ее установить, давайте проанализируем наше приложение. Проблема может быть в функции getLocation().

Всем методы нашего класса Book реализуют задуманную бизнес-логику. Поэтому мы должны рассматривать свои перспективы с точки зрения задачи. Если наше приложение написано специально для использования библиотекарями, которые ищут и выдают нам реальные книги, то ПЕО может быть нарушен.

Варианты интерпретации

Мы можем сделать вывод, что методы getAuthor(), getTitle() и getLocation() могут быть нужны только для выполнения операций актеров. Посетители также могут иметь доступ к приложению для выбора книги и чтения нескольких первых ее страниц, которые могут помочь им решить, нужна ли им эта книга или нет. Следовательно, для таких актёров как читатели могут быть полезны все имеющиеся методы, кроме getLocation(), т.к. читателей не волнует, где в библиотеке хранятся нужные книги. Книгу найдет и отдаст в руки посетителя библиотекарь. Таким образом, мы действительно имеем нарушение принципа единой ответственности.

<?php

class Book
{
    public function getTitle()
    {
        return "A Great Book";
    }

    public function getAuthor()
    {
        return "John Doe";
    }

    public function turnPage()
    {
        // pointer to next page
    }

    public function getCurrentPage()
    {
        return "current page content";
    }

}

class BookLocator
{
    public function locate(Book $book)
    {
        // returns the position in the library
        // ie. shelf number & room number
        $repository->findBookBy($book->getTitle(), $book->getAuthor());
    }

}

Для того чтобы найти нужную книгу, библиотекарь должен будет применить класс BookLocator. Посетителю же потребуется только класс Book. Конечно же, BookLocator можно реализовать несколькими разными способами. Так, можно использовать автора книги и ее название, чтобы найти информацию из объекта Book. Это всегда зависит от нашей задачи. Важно то, что при переезде библиотеки в другое помещение организация хранения книг, скорее всего, изменится, и библиотекарю придется искать книги в новой библиотеке, но при этом объект Book затронут не будет. Точно также, если мы позволим читателям просматривать только аннотации книг, закрыв доступ к их страницам – мы никак не повлияем ни на библиотекаря, ни на собственно процесс поиска полок, на которых находятся книги.

Однако если наша задача – исключить библиотекаря и разработать механизм самообслуживания в нашей библиотеке, то мы можем считать, что ПЕО был соблюден в нашем первом примере. Читатели в этом случае сами станут выступать в роли библиотекарей, и им придется идти искать книги самостоятельно, после чего подтверждать получение нужной книги в автоматизированной системе. Существует и такая возможность. Главное, что здесь нужно запомнить, - это то, что Вы всегда должны обдумывать свои задачи очень тщательно.

Выводы

Принцип единcтвенной ответственности должен соблюдаться каждый раз, когда вы пишите код. Построение классов и модулей во многом определяется ПЕО, который позволяет сокращать зависимость между ними. Но, как и каждая медаль, ПЕО имеет две противоположные стороны. Очень удобно планировать архитектуру приложения, учитывая ПЕО, с самого начала разработки. Также удобно сразу выделить столько актеров, сколько нам понадобится. Однако, с точки зрения архитектуры, крайне опасно пытаться продумать все составляющие части приложения с самого начала. Излишнее соблюдение ПЕО может с легкостью привести к чрезмерной оптимизации, и вместо хорошей архитектуры, мы получим архитектуру, в которой будет очень сложно разобраться, какой класс или модуль за что отвечает.

Таким образом, каждый раз, когда вы обнаруживаете, что класс или модуль может вскоре измениться по разным причинам, постарайтесь сделать необходимые шаги для соблюдения ПЕО, при этом не нужно сильно переусердствовать, так как излишняя оптимизация может, наоборот, нести за собой дополнительные проблемы.

SOLID «O»: Принцип открытости/закрытости (open/closed principle, OCP)

Принцип открытости/закрытости легко нарушить, но и написать код, который соответствует этому принципу, не так уж и сложно.

Сущность программного обеспечения (классы, модули и функции) должна быть открыта для усовершенствования, но закрыта для различных изменений.

Принцип открытости-закрытости (OCP) был сформулирован французским программистом Бертраном Майером и впервые вышел в мир в его книге «Object-Oriented Software Construction» в 1988 году. Популярность к этому принципу пришла в начале 2000-х годов, когда его включили в SOLID.

Здесь же речь идет о разработке модулей, классов и функций таким образом, чтобы в ситуации, когда понадобится новый функционал, не пришлось менять уже существующий код. Решение – в написании нового кода, который будет использовать существующий. Это может вызвать недоумение у разработчиков, которые пишут на Java, C, C++ или C#, так как затрагивается не только исходный код, но и двоичный. Имеется в виду создание новых возможностей таким образом, чтобы не пришлось заново распределять двоичные файлы, файлы с расширением «exe» и DLL-библиотеки.

SRP в контексте SOLID

Если двигаться дальше, получится, что каждый новый принцип можно рассматривать в контексте уже рассмотренных ранее. Так, принцип единственной обязанности (SRP) гласит, что на одном объекте может лежать только одна обязанность. Сравнивая OCP и SRP, можно отметить их комплементарность, взаимодополняемость. Код, разработанный с учетом SRP, визуально будет близок к такому же коду, но учитывающему OCP. Когда у нас есть код, каждый объект которого имеет одну обязанность, введение новой функции создаст вторую обязанность, второй повод для изменения. Это может нарушить оба принципа.

Точно также, если у нас есть код, который должен меняться только тогда, когда его основные функции меняются или, наоборот, должны оставаться неизменными при добавлении новой функции, то в этом коде будут соблюдены оба принципа. Но это не значит, что SRP-принцип всегда приводит к OCP, или, наоборот, но в преобладающем большинстве случаев, если соблюден один принцип, привести код к соблюдению второго не составит большого труда.

Очевидный пример нарушения принципа OCP

С исключительно технической точки зрения принцип открытости-закрытости очень прост: между двумя классами есть простые связи, но один из классов нарушает принцип OCP.

Класс User напрямую использует класс Logic. Если второй класс Logic нужно реализовать таким образом, чтобы можно было использовать и старые наработки, и новые, существующий класс Logic придется несколько изменить. User напрямую связан с классом Logic, а значит, способа изменить его так, чтобы не пришлось менять и User, попросту не существует. Когда речь идет о языках со статической типизацией, то класс User, скорее всего, тоже придется изменить.

Когда же речь идет о компилируемых языках, то исполняемые файлы User и Logic и динамические библиотеки потребуют перекомпиляции клиентов, а это – крайне нежелательный процесс.

Смотрим код

Основываясь на размещенной выше схеме, можно сделать вывод о том, что если любой один класс использует другой класс, то принцип открытости-закрытости будет нарушаться. Строго говоря, это – верно. Очень интересно найти тот самый предел, ту черту, за которой приходит понимание: соответствовать принципу OCP гораздо сложнее, чем изменить уже существующий код, или же затраты на изменение кода будут слишком большими.

К примеру, нужно написать класс, который показывает прогресс закачки файла через некое приложение в процентах. Использоваться будет два основных класса - Progress и File.

    public function testItCanGetTheProgressOfAFileAsAPercent()
    {
        $file = new File();
        $file->length = 200;
        $file->sent = 100;
     
        $progress = new Progress($file);
     
        $this->assertEquals(50, $progress->getAsPercent());
    }

В примере был использован Progress. В качестве результата нужно получить значение в процентах, независимо от фактического размера файла. Класс File был использован в качестве источника информации для класса Progress. У файла есть определенная длина в байтах и поле под названием sent, которое предоставляет объем данных, переданных загрузчику. В данный момент не важно, как именно эти значения будут обновляться в приложении. Можно предположить, что существует некая волшебная логика, которая это и делает, поэтому в примере их можно просто установить.

<?php

class File
{
    public $length;
    public $sent;
}

Класс File – это простой объект данных, содержащий 2 поля. В реальной жизни, конечно, у него может быть другое содержимое и поведение, например, имя файла, путь и относительный путь, директория, тип, разрешение и так далее.

<?php

class Progress
{
    private $file;
 
    public function __construct(File $file)
    {
        $this->file = $file;
    }
 
    public function getAsPercent()
    {
        return $this->file->sent * 100 / $this->file->length;
    }
}

Проще говоря, Progress – это класс, который принимает File в конструкторе. Для ясности тип переменной был определен в параметрах конструктора. Существует единственный полезный метод в Progress, это - getAsPercent(), который принимает отправляемые значения и длину из File и переводит все в проценты. Просто, понятно и работает.

Testing started at 5:39 PM ...
PHPUnit 3.7.28 by Sebastian Bergmann.
.
Time: 15 ms, Memory: 2.50Mb
OK (1 test, 1 assertion)

Код выглядит правильно, но он все равно нарушает принцип открытости-закрытости. Но, как и почему?

Изменение требований

Вполне ожидаемо, что каждое приложение будет эволюционировать по мере того, как появится нужда в новых функциях. Одной из новых возможностей некоего нашего приложения может стать не только закачка музыки, но и ее прослушивание. Длина File представлена в байтах, а продолжительность музыки – в секундах. Слушателям нужно предложить хороший показатель прогресса, но нельзя ли использовать для этого уже существующий?

Оказывается, нет, нельзя, так как наш прогресс уже связан с File и понимает только файлы. Это не изменится даже тогда, когда мы сможем переделать его для распознавания музыки. Но для того, чтобы появилось распознавание музыкальных файлов, нужно, чтобы Progress имел представление о Music и о File. Если бы решение соответствовало принципу OCP, то File или Progress не пришлось бы менять, а существующий показатель прогресса можно было бы легко адаптировать к музыке.

Решение 1: используем динамическую природу PHP

У динамически типизированных языков есть преимущество: они могут распознавать тип объекта в процессе исполнения. Это позволяет не использовать отдельное определение типа в конструкторе Progress с вполне работоспособным кодом.

<?php

class Progress
{
    private $file;
 
    public function __construct($file)
    {
        $this->file = $file;
    }
 
    public function getAsPercent()
    {
        return $this->file->sent * 100 / $this->file->length;
    }
}

Теперь в Progress можно добавить все, что только угодно.

<?php

class Music
{
    public $length;
    public $sent;
 
    public $artist;
    public $album;
    public $releaseDate;
 
    public function getAlbumCoverFile()
    {
        return 'Images/Covers/' . $this->artist . '/' . $this->album . '.png';
    }
}

И класс Music будет работать отлично. Проверить это можно на простом примере:

    public function testItCanGetTheProgressOfAMusicStreamAsAPercent()
    {
        $music = new Music();
        $music->length = 200;
        $music->sent = 100;
     
        $progress = new Progress($music);
     
        $this->assertEquals(50, $progress->getAsPercent());
    }

В общем, любое измеримое содержание можно использовать вместе с классом Progress. Можно выразить это в переменной, изменив ее имя:

<?php

class Progress
{
    private $measurableContent;
 
    public function __construct($measurableContent)
    {
        $this->measurableContent = $measurableContent;
    }
 
    public function getAsPercent()
    {
        return $this->measurableContent->sent * 100 / $this->measurableContent->length;
    }
 
}

Все, кажется, отлично, но в этом подходе есть одна громадная проблема. Когда File был указан в роли определителя типа, уверенность в том, что класс будет отменно работать, только крепчала. Это было вполне очевидно, а о каких-либо неточностях могла бы сообщить простая ошибка.

Argument 1 passed to Progress::__construct()
must be an instance of File,
instance of Music given.

Конечный результат был бы одинаковым в обоих случаях, но в первом мы получили бы сообщение. Правда, очень неясное. Нет способа узнать, что переменной (в нашем случае это - строка) не хватает каких-либо свойств или они просто не были найдены. Отладка и дебагинг в этом случае становятся проблемой: программисту приходится открывать класс Progress и перечитывать его для того, чтобы найти и понять проблему. Это можно уточнить по поведению Progress, а если точнее, то благодаря доступу к полям sent и length в методе getAsPercent(). Но в реальной жизни все может быть гораздо сложнее.

Подобное решение нужно применять только в том случае, если ни одно из предложенных ниже решений нельзя реализовать с минимальными затратами (трудность реализации или слишком большие архитектурные изменения, которые не оправдают усилий).

Решение 2: Стратегия паттернов проектирования

Это – наиболее распространенное и наиболее доступное решение для соответствия OCP, простой и эффективный метод.

Шаблон Стратегия отлично показывает используемый интерфейс. Интерфейс – это особый тип организации в объектно-ориентированном программировании, который определяет отношения между клиентом и классом сервера. Оба класса будут вести себя так, чтобы достигнуть желаемого.

<?php

interface Measurable
{
    public function getLength();
    public function getSent();
}

В интерфейсе мы можем определить только поведение, поэтому вместо прямого использования общедоступных переменных следует подумать о сеттерах и геттерах. А адаптировать другие классы будет просто, ведь об этом практически полностью позаботится IDE.

    public function testItCanGetTheProgressOfAFileAsAPercent()
    {
        $file = new File();
        $file->setLength(200);
        $file->setSent(100);
     
        $progress = new Progress($file);
     
        $this->assertEquals(50, $progress->getAsPercent());
    }

Как обычно, сначала пойдут примеры, где мы будем пользоваться сеттерами для установки значений. Кстати, сеттеры также можно определить в нашем измерительном интерфейсе (Measurable), но будьте внимательны с тем, что вы туда прописываете. Интерфейс для определения контракта между клиентским классом Progress и различными серверными классами File и Music.

Нужно ли устанавливать значения для Progress? Вероятно, нет. Вряд ли вам придется определять сеттеры в интерфейсе, но если вы все же решите это сделать, то заставите все серверные классы работать с сеттерами. Одним из них вполне могут быть необходимы сеттеры, но другие могут вести себя неожиданно непредсказуемо. Что нужно сделать для того, чтобы Progress показывал температуру печи? Класс OvenTemperature можно инициализировать со значениями в конструкторе или же получить информацию от третьего класса. Но иметь здесь сеттеры – это странно.

<?php

class File implements Measurable
{
    private $length;
    private $sent;
 
    public $filename;
    public $owner;
 
    public function setLength($length)
    {
        $this->length = $length;
    }
 
    public function getLength()
    {
        return $this->length;
    }
 
    public function setSent($sent)
    {
        $this->sent = $sent;
    }
 
    public function getSent()
    {
        return $this->sent;
    }
 
    public function getRelativePath()
    {
        return dirname($this->filename);
    }
 
    public function getFullPath()
    {
        return realpath($this->getRelativePath());
    }
}

Класс File был слегка изменен для того, чтобы соответствовать требованиям выше, и теперь он реализует интерфейс Measurable; в нем есть сеттеры и геттеры для полей, которые нам нужны. А класс Music на него очень похож.

<?php

class Progress
{
    private $measurableContent;
 
    public function __construct(Measurable $measurableContent)
    {
        $this->measurableContent = $measurableContent;
    }
 
    public function getAsPercent()
    {
        return $this->measurableContent->getSent() * 100 / $this->measurableContent->getLength();
    }
 
}

Класс Progress тоже нужно немножко обновить, указав в конструкторе тип. Наш тип – это Measurable (измерительный). После этого у нас появляется явный контракт. В Progress всегда будут методы доступа – мы определили их в интерфейсе измерения. Классы File и Music всегда смогут сделать все, что нужно классу Progress, путем простого исполнения методов интерфейса: это необходимо, когда класс реализует интерфейс.

Заметка об имени интерфейса

Люди часто называют интерфейс с заглавной буквы или добавляют слово «Interface» в конце, например, IFile или FileInterface. Это обозначение старого образца и работало оно со старыми стандартами. Имя переменной или файла должно четко и ясно давать понять суть его содержимого. IDE определяет что-либо со скоростью в долю секунды и именно это позволяет нам сконцентрироваться на работе.

Интерфейсы принадлежат их клиентам и поэтому, когда мы ходим дать имя интерфейсу, нам нужно полностью забыть о реализации и думать только о клиенте. Когда мы назвали интерфейс измеримым (Measurable), то думали о Progress. Если бы мы были прогрессом, что нам нужно было бы для того, чтобы переводить что-либо в проценты? Ответ более, чем прост: что-то, что можно посчитать. Поэтому мы и присвоили название Measurable.

Не стоит забывать о том, что реализация может быть из разных областей. В нашем случае, это – музыка и файлы. Но готовый прогресс мы легко можем заново использовать в гоночном симуляторе, и тогда нашими измеряемыми классами станут скорость, количество топлива и так далее.

Решение 3: используем шаблонный метод

Шаблонный метод очень смахивает на стратегию, но с одним отличием: вместо интерфейса он использует абстрактные классы. Шаблонный метод рекомендуется использовать в том случае, если клиент для нашего приложения очень специфический, с небольшой возможностью повторного использования и в том случае, если у серверных классов общее поведение.

Просмотр высокого уровня

Итак, как же все это влияет на нашу архитектуру высокого уровня? 

Если изображение выше представляет текущую архитектуру нашего приложения, то добавление нового модуля с пятью классами (синий цвет) должно вполне ожидаемо повлиять на всю расстановку (красный цвет).

В большинстве систем невозможно не повлиять на код при вводе новых классов. Но соответствие принципу открытости-закрытости может значительно сократить классы и модули, требующие постоянного изменения.

Изучая очередной новый принцип, не пытайтесь удержать в голове одновременно с ним и все остальное, иначе вы просто запутаетесь в интерфейсах для каждого класса. Такую конструкцию будет тяжело понимать и поддерживать. Наиболее оптимальным решением в этом случае станет учет возможностей и определение, будут ли здесь другие типы и серверные классы.

В общем-то, можно легко представить себе новую функцию или найти ее в логах другого серверного класса. В таких случаях, интерфейс нужно добавить с самого начала. Если же вы не уверены или не можете разобраться – просто пропустите эту часть. Пусть добавлением интерфейса занимается другой программист или даже вы, но в будущем.

Если вы будете все продумывать при добавлении интерфейсов, то модификаций будет мало, они будут быстрыми и легкими. Помните, если код пришлось изменить один раз, скорее всего, это нужно будет делать снова и здесь принцип открытости-закрытости может здорово помочь.

SOLID «L»: Принцип подстановки Барбары Лисков (Liskov substitution principle, LSP)

Подклассы не могут замещать поведение базовых классов.

Концепция принципа подстановки была предложена Барбарой Лисков в ее докладе на конференции 1987 года, а спустя 7 лет – опубликована в соавторстве с Джаннет Вин. Оригинальное определение принципа, предложенное Барбарой, следующее:

«В том случае, если q(x) – свойство, верное по отношению к объектам х некого типа T, то свойство q(y) тоже будет верным относительно ряда объектов y, которые относятся к типу S, при этом S – подтип некого типа T.»

Некоторое время спустя, после публикации Робертом С. Мартином всей пятерки принципов SOLID в книге о быстрой разработке программ, а затем и после публикации версии книги о быстрой разработке для языка программирования C#, принцип  стал называться принципом подстановки Барбары Лисков.

Это приводит нас к определению, которое дал сам Роберт С. Мартин:

Подтипы должны дополнять базовые типы.

Если это разъяснить, то получится, что подклассы должны переопределять методы базового класса так, чтобы не нарушалась функциональность с точки зрения клиента. Подробно это можно рассмотреть на простом примере:

Есть существующий класс Vehicle, который может быть и абстрактным в том числе, и две реализации:

<?php

class Vehicle
{
    public function startEngine()
    {
        // Default engine start public functionality
    }
 
    public function accelerate()
    {
        // Default acceleration public functionality
    }
}

class Car extends Vehicle
{
    public function startEngine()
    {
        $this->engageIgnition();
        parent::startEngine();
    }
 
    private function engageIgnition()
    {
        // Ignition procedure
    }
}
 
class ElectricBus extends Vehicle
{
    public function accelerate()
    {
        $this->increaseVoltage();
        $this->connectIndividualEngines();
    }
 
    private function increaseVoltage()
    {
        // Electric logic
    }
 
    private function connectIndividualEngines()
    {
        // Connection logic
    }
 
}

Клиентский класс должен иметь возможность использовать любой из них, если он может использовать Vehicle.

<?php

class Driver
{
    public function go(Vehicle $v)
    {
        $v->startEngine();
        $v->accelerate();
    }
}

А это уже приводит нас к простой реализации шаблонного метода проектирования так же, как он использовался и с принципом открытости-закрытости.

Основываясь на предыдущем опыте с принципом открытости-закрытости, можно сделать вывод, что принцип Барбары Лисков сильно с ним связан. И в самом деле, как сказал Роберт Мартин, нарушение принципа LSP – это скрытое нарушение принципа OCP. Шаблонный метод проектирования – классический пример соблюдения и реализации принципа подстановки, который, в свою очередь, является одним из способов соблюдения OCP.

Классический пример нарушения принципа LSP

Чтобы показать нарушение как можно полнее и нагляднее, будет использован классический понятный пример.

<?php

class Rectangle
{
    private $topLeft;
    private $width;
    private $height;
 
    public function setHeight($height)
    {
        $this->height = $height;
    }
 
    public function getHeight()
    {
        return $this->height;
    }
 
    public function setWidth($width)
    {
        $this->width = $width;
    }
 
    public function getWidth()
    {
        return $this->width;
    }
 
}

Мы начнем с основной геометрической формы – прямоугольника (Rectangle). Это всего лишь простой объект данных с сеттерами и геттерами для ширины (width) и высоты (height). Если представить, что приложение уже работает и даже на нескольких клиентах, которым нужно управлять этим прямоугольником так, чтобы сделать из него квадрат, то придется ввести новые функции.

В реальной жизни, в геометрии, квадрат – это просто одна из форм прямоугольника. Поэтому нужно попробовать реализовать класс Square, расширяющий класс Rectangle. На первый взгляд, кажется, что подкласс – это базовый класс, а принцип подстановки не нарушается.

Но будет ли квадрат Square прямоугольником Rectangle уже в программировании?

<?php

class Square extends Rectangle
{
    public public function setHeight($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
 
    public public function setWidth($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
}

Квадрат – это прямоугольник с одинаковой шириной и высотой, а значит, реализация в примере выше была бы не совсем корректной. Можно было бы переписать сеттеры, чтобы установить ширина и высоту. Но как это повлияет на клиентский код?

<?php

class Client
{
    public function areaVerifier(Rectangle $rect)
    {
        $rect->setWidth(5);
        $rect->setHeight(4);
 
        if($rect->area() != 20) {
            throw new Exception('Bad area!');
        }
 
        return true;
    }
}

Реально получить клиентский класс, который проверяет площадь прямоугольника, и реагирует, если ее значение оказывается неправильным.

    public function area()
    {
        return $this->width * $this->height;
    }

Ну и конечно добавлен метод класса Rectangle.

<?php

class LspTest extends PHPUnit_Framework_TestCase
{
    public function testRectangleArea()
    {
        $rect = new Rectangle();
        $client = new Client();
        $this->assertTrue($client->areaVerifier($rect));
    }
 
}

С помощью простого теста можно проверить работу: отправим пустой прямоугольный объект для определения его площади. Работает. Если наш класс Square определяется корректно, то его отправка на клиентский areaVerifier() не повредит функциональности. В конце концов, в математическом понимании Square – это все тот же Rectangle. Но наш ли это класс?

    public function testSquareArea()
    {
        $rect = new Square();
        $client = new Client();
        $this->assertTrue($client->areaVerifier($rect));
    }

Тестирование проходит легко и много времени не занимает. А уведомление появляется при запуске теста выше.

PHPUnit 3.7.28 by Sebastian Bergmann.
 
Exception : Bad area!
#0 /paht/: /.../.../LspTest.php(18): Client->areaVerifier(Object(Square))
#1 [internal public function]: LspTest->testSquareArea()

Итак, в нашем «программном» смысле Square класс - это не Rectangle, иначе бы законы геометрии и принцип подстановки Барбары Лисков нарушались.

Этот пример особенно хорош тем, что он показывает и нарушение LSP, и то, что объектно-ориентированное программирование не может применить правила реальной жизни к объектам. Каждый объект здесь должен быть абстракцией над концепцией. А если мы попытаемся сопоставить реальный объект и программный объект, то у нас никогда это не получится.

SOLID «I»: Принцип разделения интерфейса (interface segregation principle, ISP)

Суть принципа разделения интерфейса – в бизнес-логике и клиентском общении. Во всех модульных приложениях должен быть интерфейс, которым может воспользоваться клиент. Это может быть классический объект, реализуемый в шаблонах проектирования вроде Facades. Не важно, применяется то или иное решение. Суть всегда остается той же: объяснить клиентскому коду как правильно использовать модуль. Эти интерфейсы могут находиться между различными модулями в одном приложении или проекте, или между одним проектом в качестве сторонней библиотеки, служащей для подачи еще одного проекта.

Хороший способ на старте определить, что именно мы хотим реализовать в нашем модуле. Подобное начало может привести к одной из реализаций:

  1. большой класс Car или Bus реализует методы интерфейса Vehicle. Одни размеры таких классов советуют избегать их любой ценой;
  2. маленькие классы вроде LightsControl, SpeedControl или RadioCD реализуют весь интерфейс, но делают что-то полезное только для реализуемых ими частей;

Очевидно, что ни одно из этих решений не подходит для реализации нашей бизнес-логики.

Мы могли бы попробовать еще один подход: разбить интерфейс на куски, каждый из которых займется своей реализацией. Для маленьких классов – идеально. Объекты, реализующие интерфейсы, будут использовать другой тип транспорта, например, машину (car) на картинке выше.

Но это может фундаментально изменить наше восприятие архитектуры. Вместо реализации Car становится клиентом. А мы хотим дать возможность клиенту использовать весь модуль.

Предположим, что проблема реализации уже решена, а бизнес-логика – стабильна. Прежде всего, нужно обеспечить единый интерфейс со всеми реализациями и пусть клиенты BusStation, HighWay, Driver используют все, что угодно из реализации. Это перекладывает ответственность за поведение на клиентов (подобный метод часто применялся в старых приложениях). Принцип разделения интерфейса утверждает, что ни один клиент не должен зависеть от неиспользуемых методов.

Но есть одна проблема: все клиенты зависят от всех методов. Разве BusStation должен зависеть от радиостанции, которую выбрал водитель, или от фар автобуса? Нет. Но что, если так оно и будет? Нужно вспомнить принцип единой обязанности. Если BusStation зависит от многих реализаций (и даже не используемых), то он может потребовать изменений, если изменится одна из реализаций. Такого быть не должно.

Интерфейсы относятся к клиентам, а не к реализациям, поэтому и создавать их нужно в лучших отношениях с клиентом. Мы должны разбить интерфейсы на кусочки так, чтобы они лучше работали с клиентами.

Конечно, могут появиться дубляжи, но помните, что интерфейсы – это простые определения имен функций и логика в них не реализуется, а значит, и проблем с дублированием не будет.

Здесь есть преимущество перед клиентами, которые зависят только от того, что им нужно и что они используют сами по себе. В некоторых случаях клиенты могут использовать и нуждаться в нескольких интерфейсах, и это совершенно нормально, пока они используют все методы всех интерфейсов, от которых зависят.

Еще один приятный момент заключается в том, что в нашей бизнес-логике один класс может реализовать несколько интерфейсов, если ему это понадобится. Так что мы можем обеспечить единую реализацию для всех общих методов между интерфейсами. Интерфейсы также заставляют нас думать о нашем коде больше с точки зрения клиента, что, в свою очередь, приводит нас к уже более легкому тестированию. Так что мы не только сделали наш код лучше для наших клиентов, мы также сделали его проще для себя, чтобы легче понимать, тестировать и реализовывать.

Резюме

Принцип подстановки Барбары Лисков демонстрирует, почему реальные объекты нельзя сопоставлять один к одному с объектами программирования, и учит писать код таким образом, чтоб подтипы хорошо уживались с базовыми типами. Принцип подстановки дополняет другие принципы SOLID и проще интерпретируется в их контексте.

Принцип разделения интерфейса учит нас уважать клиентов еще сильнее, чем мы это делали раньше. Внимание к их потребностям может сделать наш код намного лучше, а нашу работу – проще и интереснее.

SOLID «D»: Принцип инверсии зависимостей (dependency inversion principle, DIP)

Принцип единственной обязанностиоткрытости-закрытостиподстановкиразделения интерфейсов и инверсии зависимостей – пятерка принципов, на которые следует ориентироваться при написании кода.

Хотя говорить о преобладающей важности одного из принципов будет не верно, но отметить влияние принципа инверсии зависимостей на код нужно обязательно. Если вы заметили, что другие принципы трудно понять или применить, начните с этого, а остальные применяйте уже к коду, соответствующему DIP

Определение

  1. Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Этот принцип выделил Роберт С. Мартин в своей книге о быстрой разработке программ, а затем переиздал в версии книги для языка C#. Принцип инверсии зависимостей - последний из пятерки принципов SOLID.

DIP в реальном мире

Если вы программируете не очень аккуратно, не совсем разбираетесь в правилах программирования и не соблюдаете ряд принципов SOLID, в своей работе вам, возможно, придется пройти все 7 кругов ада, ощутить все на собственном опыте, прежде чем код станет действительно хорошим.

Принципы SOLID - это исключительно архитектурные принципы Роберта С. Мартина, которые полностью меняют правила игры, весь ход программирования. Далее мы проиллюстрируем влияние нескольких архитектурных решений, которые появились благодаря принципу DIP, и серьезно повлияли на один из наших проектов.

Большинство веб-проектов включают в себя три основных технологии: HTML, PHP и SQL. Определенная версия каждого из этих приложений, о которых мы говорим, или то, какой тип реализаций SQL вы используете – все это не имеет абсолютно никакого значения в нашем случае. Дело в том, что информация из HTML формы должна заканчиваться в базе данных. Остальное обеспечит РНР.

Из этого следует уяснить одну важную вещь - хороший способ, которым эти три технологии представляют три различных архитектурных слоя:

  1. пользовательский интерфейс,
  2. бизнес-логику,
  3. долговременное сохранение данных.

Скоро мы поговорим об этих слоях. А сейчас давайте сосредоточимся на некоторых странных, но часто встречающихся решениях, применяемых для того, чтобы заставить технологии работать вместе.

Есть много проектов, которые используют SQL код в тегах PHP внутри HTML-файла. Или PHP-код отражает и страницы HTML, и непосредственно интерпретирует глобальные переменные $_GET или $_POST. Казалось бы, все хорошо. Но в чем же тогда проблема?

На изображении выше показана сырая версия того, что мы описывали в предыдущем абзаце. Стрелки указывают на различные зависимости, и мы можем сделать вывод, что все базируется на всем, все зависит от всего. Если нам придется изменить таблицу базы данных, скорее всего, в конечном итоге, мы закончим редактированием HTML-файла. Или если мы изменим поле в HTML, то закончим изменением столбца в операторе SQL. Или, как видно из второй схемы, нам действительно придется сильно изменить наш PHP, если изменится HTML. А в худшем случае, если мы генерируем весь HTML-контент из PHP-файла, скорее всего, нам придется менять PHP, чтобы изменить HTML-контент. Поэтому нет никаких сомнений в том, что зависимости просто лавируют между нашими классами и модулями. Но на этом все не заканчивается: вы можете хранить операции или код на PHP в таблицах SQL.

На схеме выше видно, что запросы к базе данных SQL возвращают PHP-код, сгенерированный с данными из таблиц. Эти PHP-е функции или классы создают другие SQL-запросы, которые возвращают уже другой PHP-код. Этот цикл будет продолжаться до тех пор, пока вся информация не будет получена и возвращена, вероятно, пользовательскому интерфейсу.

Многим это покажется полной чушью, но если вы еще не работали с проектом, созданным и реализованным по этому сценарию, то в будущем, скорее всего, вам придется с этим столкнуться. Большинство существующих проектов, независимо от используемых языков программирования, были написаны программистами старой закалки, которым было все равно или они не знали, как можно сделать код лучше. Если вы сейчас читаете этот мануал о принципах программирования, значит, вам нужно научиться программировать на порядок лучше, чем вы можете на сегодняшний день. Это значит, что вы уважаете или даже только начинаете уважать свою профессию, хотите понять свое ремесло и сделать его лучше.

Еще одна версия может рассказать нам об ошибках, сделанных нашими предшественниками, и последствиями от них. В некоторых проектах вы можете получить, в итоге, практически неподдерживаемое состояние из-за их старой и кросс-зависимой архитектуры. В итоге, вам придется просто отказаться от таких проектов навсегда, и тогда вы поймете, что больше не хотите повторять эти ошибки снова. Поэтому стремитесь делать чистую архитектуру, которая будет соответствовать принципам SOLID, во-первых, а во-вторых, принципу инверсии зависимостей.

В этой архитектуре есть несколько интересных моментов:

  1. пользовательский интерфейс (в большинстве случаев, веб-фреймворк MVC) или любой другой механизм доставки в нашем проекте будет зависеть от бизнес-логики. Бизнес-логика сама по себе довольно абстрактна, а пользовательский интерфейс – само воплощение конкретики. Он представляет собой одну из деталей проекта, и к тому же, очень нестабильную. Ничто не должно зависеть от пользовательского интерфейса, ничто не должно зависеть от веб-фреймворка MVC;
  2. еще одно интересное наблюдение, сделанное нами, говорит, что долговременное сохранение, база данных, наш MySQL или PostgreSQL базируются на бизнес-логике. Это позволяет менять сохраняемость так, как это нужно нам. Если завтра нам понадобится изменить MySQL вместе с PostgreSQL или просто текстовые файлы, мы легко сможем это сделать. Нам, конечно, придется реализовать определенный уровень сохраняемости для новых методов сохранения состояния, но для этого не придется менять отдельные строки кода в нашей бизнес-логике;
  3. В конце концов, в правой части нашей бизнес-логики, вне ее, у нас есть все классы, которые создают классы бизнес-логики. Эти классы созданы как точки входа в наши приложения. Многие люди склонны думать, что они принадлежат к бизнес-логике, но делают они это только для того, чтобы создать бизнес-объекты. Они – просто классы, которые помогают нам создавать другие классы. Бизнес-объекты и логика, которую они обеспечивают, не зависят от них. Мы могли бы использовать различные модели или создать простой объект, чтобы обеспечить бизнес-логику. Это не имеет значения. После того, как бизнес-объекты созданы, они начинают выполнять свою работу.

Приступим к коду

Соблюдать принцип инверсии зависимостей (DIP) на архитектурном уровне довольно легко, если вы соблюдаете классические шаблоны проектирования. Использовать и показать это внутри бизнес-логики достаточно просто, а местами - даже весело. Представим себе приложение для чтения электронных книг.

<?php

class Test extends PHPUnit_Framework_TestCase
{
    public function testItCanReadAPDFBook()
    {
        $book = new PDFBook();
        $reader = new PDFReader($book);
 
        $this->assertRegExp('/pdf book/', $reader->read());
    }
}
 
class PDFReader
{
    private $book;
 
    public function __construct(PDFBook $book)
    {
        $this->book = $book;
    }
 
    public function read()
    {
        return $this->book->read();
    }
 
}
 
class PDFBook
{
    public function read()
    {
        return "reading a pdf book.";
    }
}

Мы начали разработку электронной читалки как читалки PDF. Пока не возникло никаких проблем. У нас есть класс PDFReader, который использует PDFBook. Функция read() в читалке относится к методу read(). В этом мы убедимся путем регулярной проверки выражений после ключевого элемента строки, которые возвращает метод PDFBook's reader().

Не забывайте о том, что это всего лишь пример. Мы не станем реализовывать логику чтения PDF-файлов или любых других форматов файлов. Наши тесты – проверки на некоторых базовых строках. Если бы мы писали настоящее приложение, единственная разница заключалась бы в способе тестирования разных файловых форматов. Структура зависимости была бы очень похожа на предложенную в примере.

Использование читалки формата PDF, которая использует книгу в формате PDF, может быть вполне здравым решением в ряде ограниченных случаев. Если наша задача заключалась в том, чтобы написать только читалку формата PDF, то такое решение фактически соответствует задаче. Но мы хотели написать универсальную читалку, которая использовала бы несколько разных форматов, в том числе, и уже реализованную PDF-версию. Переименуем класс нашей читалки.

<?php

class Test extends PHPUnit_Framework_TestCase
{
    public function testItCanReadAPDFBook()
    {
        $b = new PDFBook();
        $r = new EBookReader($b);
 
        $this->assertRegExp('/pdf book/', $r->read());
    }
}
 
class EBookReader
{
    private $book;
 
    public function __construct(PDFBook $book)
    {
        $this->book = $book;
    }
 
    public function read()
    {
        return $this->book->read();
    }
}
 
class PDFBook
{
    public function read()
    {
        return "reading a pdf book.";
    }
}

Переименование не дало каких-либо функциональных эффектов. Тест пройден на «отлично».

Testing started at 1:04 PM ...
PHPUnit 3.7.28
Time: 13 ms, Memory: 2.50Mb
OK (1 test, 1 assertion)
Process finished with exit code 0

Наша читалка уже стала гораздо абстрактнее. Намного универсальнее. Сейчас у нас есть универсальный EBookReader, который может читать довольно специфический формат книг – PDFBook. Наша абстракция зависит от деталей. То, что наша книга находится в формате PDF – все лишь деталь, от которой ничего не должно зависеть.

<?php

class Test extends PHPUnit_Framework_TestCase
{
    public function testItCanReadAPDFBook()
    {
        $book = new PDFBook();
        $reader = new EBookReader($book);
 
        $this->assertRegExp('/pdf book/', $reader->read());
    }
 
}
 
interface EBook
{
    public function read();
}
 
class EBookReader
{
    private $book;
 
    public function __construct(EBook $book)
    {
        $this->book = $book;
    }
 
    public function read()
    {
        return $this->book->read();
    }
 
}
 
class PDFBook implements EBook
{
    public function read()
    {
        return "reading a pdf book.";
    }
}

Наиболее популярным и часто используемым решением для того, чтобы инвертировать зависимость, является введение более абстрактного модуля в наш проект. Наиболее абстрактным элементом в ООП является интерфейс. Таким образом, любой другой класс может зависеть от интерфейса, и все еще соблюдать DIP.

Мы создали интерфейс для нашей читалки, который назвали EBookReader, и который отражает все потребности EBook. Это – прямой результат соблюдения принципа разделения интерфейса, который основывается на идее, что интерфейсы должны отражать потребности клиентов. Интерфейсы относятся к клиентам и называются таким образом, чтобы отражать типы и объекты, которые необходимы клиентам. Интерфейсы должны содержать методы, которые клиенты хотят использовать. Для EBookReader вполне естественно использовать EBook и содержать метод read().

Теперь вместо единственной зависимости, у нас их становится две.

  1. Первая зависимость указывает от EBookReader к интерфейсу EBook и на используемый тип. EBookReader использует EBook.
  2. Вторая зависимость уже несколько другого рода. Она указывает от PDFBook к тому же интерфейсу EBook, но на реализуемый тип. PDFBook – это просто особая форма EBook, которая реализует интерфейс для того, чтобы удовлетворить потребности клиентов.

Неудивительно, ведь такое решение позволит нам просматривать в нашей читалке различные типы электронных книг. Единственное условие для всех этих книг – они должны соответствовать интерфейсу EBook и реализовывать его.

<?php

class Test extends PHPUnit_Framework_TestCase
{
    public function testItCanReadAPDFBook()
    {
        $book = new PDFBook();
        $reader = new EBookReader($book);
 
        $this->assertRegExp('/pdf book/', $reader->read());
    }
 
    public function testItCanReadAMobiBook()
    {
        $book = new MobiBook();
        $reader = new EBookReader($book);
 
        $this->assertRegExp('/mobi book/', $reader->read());
    }
 
}
 
interface EBook
{
    public function read();
}
 
class EBookReader
{
    private $book;
 
    public function __construct(EBook $book)
    {
        $this->book = $book;
    }
 
    public function read()
    {
        return $this->book->read();
    }
 
}
 
class PDFBook implements EBook
{
    public function read()
    {
        return "reading a pdf book.";
    }
}
 
class MobiBook implements EBook
{
    public function read()
    {
        return "reading a mobi book.";
    }
}

Все это приводит нас к принципу открытости-закрытости, и круг замыкается. Принцип инверсии зависимостей помогает нам соблюдать все остальные принципы из пятерки SOLID. Соблюдая принцип DIP, мы практически начинаем соблюдать OCP, можем разделять обязанности, правильно используем подтипы и можем разделять интерфейсы.

Финальные мысли

Ну, вот и все, мы наконец-то закончили и полностью разобрали все мануалы о принципах SOLID. Это должно полностью изменить ваши представления об архитектуре и сделать вашу работу проще и интереснее. Мы должны стремиться делать наш код лучше, используя эту пятерку.

В объектно-ориентированном программировании пятерка принципов SOLID – один из важнейших стержней, которые должны делать код лучше, а жизнь программистов - проще.

About

SOLID principles of Object Oriented Design (Ru)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published