Статьи

SOLID: часть 1 — принцип единой ответственности

Одиночная ответственность (SRP), открытие / закрытие, подстановка Лискова, сегрегация интерфейса и инверсия зависимостей. Пять гибких принципов, которыми вы должны руководствоваться при написании кода.

У класса должна быть только одна причина для изменения.

Определенный Робертом К. Мартином в его книге « Гибкие разработки, принципы и практики», а затем переизданный в версии C # книги « Принципы, шаблоны и практики Agile в C #» , это один из пяти гибких принципов SOLID. То, что в нем говорится, очень просто, однако достижение этой простоты может быть очень сложным. У класса должна быть только одна причина для изменения.

Но почему? Почему так важно иметь только одну причину для перемен?

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

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

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

  • Модуль Постоянства — Аудитория включает администраторов баз данных и разработчиков программного обеспечения.
  • Модуль отчетности — Аудитория включает клерков, бухгалтеров и операций.
  • Модуль расчета платежей для системы начисления заработной платы. Аудитория может включать юристов, менеджеров и бухгалтеров.
  • Модуль поиска книг для системы управления библиотекой. Аудитория может включать библиотекаря и / или самих клиентов.

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

Поэтому, если наша аудитория определяет причины изменений, актеры определяют аудиторию. Это очень помогает нам свести концепцию конкретных людей, таких как «Иоанн-архитектор», к архитектуре или «Мэри-референт» к операциям.

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

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

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

Допустим, у нас есть класс Book инкапсулирующий концепцию книги и ее функциональные возможности.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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 , кто бы это мог быть? Здесь мы легко можем представить себе двух разных участников: управление книгами (например, библиотекарь) и механизм представления данных (например, способ доставки контента пользователю — экранный, графический пользовательский интерфейс, только текстовый пользовательский интерфейс, возможно печать) , Это два совершенно разных актера.

Смешивать бизнес-логику с презентацией плохо, потому что она противоречит принципу единой ответственности (SRP). Посмотрите на следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 ‘<div style=»single-page»>’ .
    }
 
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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/’.
        file_put_contents($filename, serialize($this));
    }
 
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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/’ .
        file_put_contents($filename, serialize($book));
    }
 
}

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

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

HighLevelDesign

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

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

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

Мы можем рассуждать шаг за шагом:

  1. Высокая первичная ценность приводит к высокой вторичной стоимости.
  2. Вторичная ценность означает потребности пользователей.
  3. Потребности пользователей означают потребности актеров.
  4. Потребности актеров определяют потребности изменений этих актеров.
  5. Необходимость смены актеров определяет наши обязанности.

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

  1. Найдите и определите актеров.
  2. Определите обязанности, которые служат этим субъектам.
  3. Сгруппируйте наши функции и классы так, чтобы у каждого была только одна распределенная ответственность.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.
    }
 
}

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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.
        $libraryMap->findBookBy($book->getTitle(), $book->getAuthor());
    }
 
}

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

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

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

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