SOLID «S»: Принцип единственной обязанности
Принцип единственной обязанности, ПЕО (англ. Single responsibility principle, SPR) - один из принципов, которого следует придерживаться при написании кода. Он декларирует, что каждый объект должен иметь одну единственную обязанность и эта обязанность должна быть инкапсулирована в класс.
Термин «SOLID» представляет собой аббревиатуру пяти важнейших принципов работы с классами в объектно-ориентированном проектировании:
- Принципа единственной обязанности;
- Принципа открытости/закрытости;
- Принципа подстановки Барбары Лисков;
- Принципа разделения интерфейса;
- Принципа инверсии зависимостей.
Этими пятью гибкими принципами следует руководствоваться при написании кода.
Определение
Класс должен иметь одну и только одну причину для изменений.
Это один из 5 гибких принципов SOLID, определенных в книге «Быстрая разработка программ. Принципы, примеры, практика» Робертом С. Мартином. Затем эта книга была переиздана в версии для C# «Принципы, паттерны и методики гибкой разработки на языке C#». То, что декларирует данный принцип, вполне легко понять, однако не так легко реализовать на практике.
Класс должен иметь одну единственную причину для изменений. Но чем обусловлена подобная необходимость? В компилируемых статически типизированных языках программирования существование нескольких причин может повлечь за собой ряд нежелательных изменений.
Две различные причины для изменений подобны двум различным командам, которые могут работать с одним кодом, и каждая из них будет разворачивать свое собственное решение, которое в случае C++, C# Java или других компилируемых языков, может привести к несовместимости между различными частями приложения.
Даже если вы не используете компилируемый язык, вам, возможно, потребуется повторное тестирование определенного класса или модуля по различным причинам. Это означает, что вы потратите больше времени и усилий.
Аудитория
Определение одной единственной ответственности, которую должен иметь класс или модуль, является гораздо более сложной задачей, чем простое просматривание списка.
Например, один из способов поиска причин изменения - анализ аудитории класса. Пользователи приложения, которые обслуживаются определенным модулем, будут являться теми лицами, которые требуют изменений класса. Вот несколько модулей и их возможная аудитория.
- Модуль сохраняемости - аудитория включает администраторов баз данных и проектировщиков ПО.
- Модуль отчетности - аудитория включает клерков, бухгалтеров и операционистов.
- Модуль расчета платежей для системы расчета заработной платы - аудитория может включать юристов, менеджеров и бухгалтеров.
- Модуль поиска книг в библиотеке - аудитория может включать библиотекаря и читателей.
Роли и актеры
Ассоциирование конкретных лиц с ролями – весьма непростая задача. В небольшой фирме одно единственное лицо может играть сразу несколько ролей, в то время как в крупной организации может быть привлечено несколько исполнителей для выполнения одной функции. Поэтому, пожалуй, будет немаловажно подумать об этих ролях. Однако определить сами роли бывает достаточно сложно. Гораздо легче обозначить актёров, играющих эти роли и связать актёров и нашу аудиторию.
Таким образом, если аудитория обусловливает причины изменения, актёры определяют аудиторию. Это позволит нам свести понятия конкретных лиц вроде «Архитектора Джона» или «Секретаря Марии» к операциям.
Таким образом, согласно Роберту С. Мартину, ответственность - это определенный набор функций, которые выполняет один взятый актёр.
Источник изменений
Рассуждая таким образом, можно сделать вывод о том, что актёры представляют собой источник изменений для набора функций, которые удовлетворяют потребности самих актёров. Наряду с изменением потребностей актёров, этот специфичный набор функций тоже должен измениться и приспособиться под новые потребности актеров.
Актер для ответственности - единственный источник изменений этой ответственности. (Роберт С. Мартин)
Классические примеры
Объекты, которые могут распечатывать сами себя
Предположим, что у нас есть класс Book, который инкапсулирует в себе книгу вместе с её функциональностью.
class Book {
function getTitle() {
return "A Great Book";
}
function getAuthor() {
return "John Doe";
}
function turnPage() {
// pointer to next page
}
function printCurrentPage() {
echo "current page content";
}
}
Это может выглядеть как целесообразный класс. У нас есть книга, которая может предоставлять информацию о своем названии, авторе и способна перелистывать страницы. Также этот класс может отображать на экране текущую страницу книгу. Однако существует маленькая проблема в определении актёров, которые могли бы быть вовлечены в управление объектом Book. Сходу можно назвать двух различных актёров: Управление книгой (к примеру, библиотекарь) и Механизм представления данных (например, способ, с помощью которого мы планируем выводить содержимое пользователю - на экран, в графическом виде, только текст или же распечатывать). Существует значительное различие между этими двумя актерами.
Разделяй и властвуй
Совмещение бизнес-логики с представлением является крайне нежелательным, т.к. это будет противоречить принципу единой ответственности (ПЕО). Рассмотрим код ниже:
class Book {
function getTitle() {
return "A Great Book";
}
function getAuthor() {
return "John Doe";
}
function turnPage() {
// pointer to next page
}
function getCurrentPage() {
return "current page content";
}
}
interface Printer {
function printPage($page);
}
class PlainTextPrinter implements Printer {
function printPage($page) {
echo $page;
}
}
class HtmlPrinter implements Printer {
function printPage($page) {
echo $page;
}
}
Даже по такому простейшему примеру можно сделать вывод о том, как правильно разграничивать бизнес-логику и представление. Придерживаясь ПЕО, можно построить гораздо более гибкую архитектуру.
Объекты, которые способны «сохранять» себя
Еще один похожий на предыдущий вариант пример того, как объект может сам себя сохранять и восстанавливаться из представления.
class Book {
function getTitle() {
return "A Great Book";
}
function getAuthor() {
return "John Doe";
}
function turnPage() {
// pointer to next page
}
function getCurrentPage() {
return "current page content";
}
function save() {
$filename = '/documents/'. $this->getTitle(). ' - ' . $this->getAuthor();
file_put_contents($filename, serialize($this));
}
}
Мы снова можем определить несколько актёров, например, Систему управления книгой и Сохраняемость. Этот класс подлежит модификации каждый раз при измении сохраняемости или способа перелистывания страницы. Можно отметить несколько вариантов изменения данных.
class Book {
function getTitle() {
return "A Great Book";
}
function getAuthor() {
return "John Doe";
}
function turnPage() {
// pointer to next page
}
function getCurrentPage() {
return "current page content";
}
}
class SimpleFilePersistence {
function save(Book $book) {
$filename = '/documents/' . $book->getTitle() . ' - ' . $book->getAuthor();
file_put_contents($filename, serialize($book));
}
}
Переместив метод сохранения объекта в другой класс, мы сможем явно разделить ответственность и легко изменить данный метод сохраняемости, никак не влияя на класс Book. Так, внедрение класса DatabasePersistence будет абсолютно тривиальным, и наша бизнес-логика, выстроенная вокруг действий с книгой никак не изменится.
Высокоуровневое представление
В своих предыдущих статьях я не раз упоминал и предлагал Вашему вниманию схему высокоуровневой архитектуры, которая приведена ниже.
![](/static/storage/4101972236588369075870765334228981529.png)
Если мы проанализируем данную схему, то сразу поймем, как соблюдается ПЕО. Создание нового объекта обозначено с правой стороны схемы с помощью «Фабрик» (Factories) и единой точки входа нашего приложения (Main). Один актёр - одна ответственность. О сохраняемости (Persistence) также позаботились, расположив ее внизу. Отдельный модуль предназначается для отдельной ответственности. И наконец, с левой стороны мы разместили представление, или механизм доставки, в виде MVC или каком-либо другом типе пользовательского интерфейса. И вновь соблюден принцип единой ответственности. Все, что нам остается выяснить, - это что делать с самой бизнес-логикой.
Вопросы проектирования программного обеспечения
Когда мы обдумываем, как лучше написать программное обеспечение, нам обычно приходится принимать во внимание множество различных аспектов. Например, несколько требований, предъявляемых к классу и оказывающих на него влияние, могут быть представлены в виде оси изменений. Эти оси изменений могут играть роль ключа к единой ответственности. Вполне вероятно, что группы требований, влияющих на ту же группу функций, будут иметь причины для изменений или могут быть выдвинуты на первый план.
Главное достоинство ПО - простота внесения изменений. Следующее важное качество - функциональность с точки зрения способности ПО удовлетворять как можно более широкий круг потребностей пользователей. Однако для достижения высокого значения на втором уровне, сначала нужно обязательно выяснить значение первого критерия. Чтобы поднять значение первичного критерия на должный уровень, мы должны, соблюдая ПЕО, построить такую архитектуру, которую можно легко модифицировать, расширять и в которую можно быстро добавлять новую функциональность.
Изложим по шагам:
Значение первого критерия должно быть установлено до определения значения второго критерия.
- Второй критерий отвечает за удовлетворение потребностей пользователей.
- Потребности пользователей – это потребности актёров.
- Потребности актёров определяют необходимость изменения этих актёров.
- Потребности в изменениях актёров, в свою очередь, определяют нашу ответственность.
Таким образом, в процессе разработки архитектуры нашего программного обеспечения, мы должны:
- Определить актёров.
- Выявить область ответственности каждого актёра.
- Сгруппировать классы и функции так, чтобы каждый из них отвечал только за свою часть.
Менее очевидный пример
class Book {
function getTitle() {
return "A Great Book";
}
function getAuthor() {
return "John Doe";
}
function turnPage() {
// pointer to next page
}
function getCurrentPage() {
return "current page content";
}
function getLocation() {
// returns the position in the library
// ie. shelf number & room number
}
}
Теперь это выглядит вполне очевидно и рационально. У нас нет метода, который относился бы к сохранению или отображению данных. Мы располагаем функционалом метода turnPage() и еще несколькими методами, позволяющими предоставить необходимые сведения о книге. Однако мы можем столкнуться с некоторой проблемой. Чтобы ее установить, давайте проанализируем наше приложение. Проблема может быть в функции getLocation().
Всем методы нашего класса Book реализуют задуманную бизнес-логику. Поэтому мы должны рассматривать свои перспективы с точки зрения задачи. Если наше приложение написано специально для использования библиотекарями, которые ищут и выдают нам реальные книги, то ПЕО может быть нарушен.
Варианты интерпретации
Мы можем сделать вывод, что методы getAuthor(), getTitle()и getLocation() могут быть нужны только для выполнения операций актеров. Посетители также могут иметь доступ к приложению для выбора книги и чтения нескольких первых ее страниц, которые могут помочь им решить, нужна ли им эта книга или нет. Следовательно, для таких актёров как читатели могут быть полезны все имеющиеся методы, кроме getLocation(), т.к. читателей не волнует, где в библиотеке хранятся нужные книги. Книгу найдет и отдаст в руки посетителя библиотекарь. Таким образом, мы действительно имеем нарушение принципа единой ответственности.
class Book {
function getTitle() {
return "A Great Book";
}
function getAuthor() {
return "John Doe";
}
function turnPage() {
// pointer to next page
}
function getCurrentPage() {
return "current page content";
}
}
class BookLocator {
function locate(Book $book) {
// returns the position in the library
// ie. shelf number & room number
$libraryMap->findBookBy($book->getTitle(), $book->getAuthor());
}
}
Для того чтобы найти нужную книгу, библиотекарь должен будет применить класс BookLocator. Посетителю же потребуется только класс Book. Конечно же, BookLocator можно реализовать несколькими разными способами. Так, можно использовать автора книги и ее название, чтобы найти информацию из объекта Book. Это всегда зависит от нашей задачи. Важно то, что при переезде библиотеки в другое помещение организация хранения книг, скорее всего, изменится, и библиотекарю придется искать книги в новой библиотеке, но при этом объект Book затронут не будет. Точно также, если мы позволим читателям просматривать только аннотации книг, закрыв доступ к их страницам – мы никак не повлияем ни на библиотекаря, ни на собственно процесс поиска полок, на которых находятся книги.
Однако если наша задача – исключить библиотекаря и разработать механизм самообслуживания в нашей библиотеке, то мы можем считать, что ПЕО был соблюден в нашем первом примере. Читатели в этом случае сами станут выступать в роли библиотекарей, и им придется идти искать книги самостоятельно, после чего подтверждать получение нужной книги в автоматизированной системе. Существует и такая возможность. Главное, что здесь нужно запомнить, - это то, что Вы всегда должны обдумывать свои задачи очень тщательно.
Выводы
Принцип единcтвенной ответственности должен соблюдаться каждый раз, когда вы пишите код. Построение классов и модулей во многом определяется ПЕО, который позволяет сокращать зависимость между ними. Но, как и каждая медаль, ПЕО имеет две противоположные стороны. Очень удобно планировать архитектуру приложения, учитывая ПЕО, с самого начала разработки. Также удобно сразу выделить столько актеров, сколько нам понадобится. Однако, с точки зрения архитектуры, крайне опасно пытаться продумать все составляющие части приложения с самого начала. Излишнее соблюдение ПЕО может с легкостью привести к чрезмерной оптимизации, и вместо хорошей архитектуры, мы получим архитектуру, в которой будет очень сложно разобраться, какой класс или модуль за что отвечает.
Таким образом, каждый раз, когда вы обнаруживаете, что класс или модуль может вскоре измениться по разным причинам, постарайтесь сделать необходимые шаги для соблюдения ПЕО, при этом не нужно сильно переусердствовать, так как излишняя оптимизация может, наоборот, нести за собой дополнительные проблемы.
Источник: code.tutsplus.com