Advertisement
  1. Code
  2. PHP

SOLID: Часть 1 - Принцип Единственной Ответственности

Scroll to top
Read Time: 10 min
This post is part of a series called The SOLID Principles.
SOLID: Part 2 - The Open/Closed Principle

Russian (Pусский) translation by Sergey Zhuk (you can also view the original English article)

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

Определение

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

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

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

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

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

Аудитория

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

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

Актеры и роли

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

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

Таким образом ответственность - это набор функций, которые обслуживает одного актера. (Роберт C. Мартин)

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

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

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

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

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

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

1
class Book {
2
3
  function getTitle() {
4
		return "A Great Book";
5
	}
6
7
	function getAuthor() {
8
		return "John Doe";
9
	}
10
11
	function turnPage() {
12
		// pointer to next page

13
	}
14
15
	function printCurrentPage() {
16
		echo "current page content";
17
	}
18
}

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

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

1
class Book {
2
3
	function getTitle() {
4
		return "A Great Book";
5
	}
6
7
	function getAuthor() {
8
		return "John Doe";
9
	}
10
11
	function turnPage() {
12
		// pointer to next page

13
	}
14
15
	function getCurrentPage() {
16
		return "current page content";
17
	}
18
19
}
20
21
interface Printer {
22
23
	function printPage($page);
24
}
25
26
class PlainTextPrinter implements Printer {
27
28
	function printPage($page) {
29
		echo $page;
30
	}
31
32
}
33
34
class HtmlPrinter implements Printer {
35
36
	function printPage($page) {
37
		echo '<div style="single-page">' . $page . '</div>';
38
	}
39
40
}

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

Объекты, которые могут сохранять сами себя

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

1
class Book {
2
3
	function getTitle() {
4
		return "A Great Book";
5
	}
6
7
	function getAuthor() {
8
		return "John Doe";
9
	}
10
11
	function turnPage() {
12
		// pointer to next page

13
	}
14
15
	function getCurrentPage() {
16
		return "current page content";
17
	}
18
19
	function save() {
20
		$filename = '/documents/'. $this->getTitle(). ' - ' . $this->getAuthor();
21
		file_put_contents($filename, serialize($this));
22
	}
23
24
}

И снова мы можем выделить двух субъектов: Система управления книгами и Хранилище. Как только нам потребуется сменить хранилище, нам придется изменить и этот класс. Как только нам потребуется изменить то, каким образом мы переходим с одной страницы на другую, нам потребуется изменить этот класс. Здесь есть несколько причин для изменения.

1
class Book {
2
3
	function getTitle() {
4
		return "A Great Book";
5
	}
6
7
	function getAuthor() {
8
		return "John Doe";
9
	}
10
11
	function turnPage() {
12
		// pointer to next page

13
	}
14
15
	function getCurrentPage() {
16
		return "current page content";
17
	}
18
19
}
20
21
class SimpleFilePersistence {
22
23
	function save(Book $book) {
24
		$filename = '/documents/' . $book->getTitle() . ' - ' . $book->getAuthor();
25
		file_put_contents($filename, serialize($book));
26
	}
27
28
}

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

Представление высшего уровня

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

HighLevelDesignHighLevelDesignHighLevelDesign

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

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

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

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

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

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

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

  1. Найти и выделить актеров (субъектов).
  2. Определить обязанности, которые обслуживают этих актеров.
  3. Сгруппировать наши функции и классы таким образом, чтобы каждый из них имел только одну определенную ответственность. 

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

1
class Book {
2
3
	function getTitle() {
4
		return "A Great Book";
5
	}
6
7
	function getAuthor() {
8
		return "John Doe";
9
	}
10
11
	function turnPage() {
12
		// pointer to next page

13
	}
14
15
	function getCurrentPage() {
16
		return "current page content";
17
	}
18
19
	function getLocation() {
20
		// returns the position in the library

21
		// ie. shelf number & room number

22
	}
23
24
}

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

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

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

1
class Book {
2
3
	function getTitle() {
4
		return "A Great Book";
5
	}
6
7
	function getAuthor() {
8
		return "John Doe";
9
	}
10
11
	function turnPage() {
12
		// pointer to next page

13
	}
14
15
	function getCurrentPage() {
16
		return "current page content";
17
	}
18
19
}
20
21
class BookLocator {
22
23
	function locate(Book $book) {
24
		// returns the position in the library

25
		// ie. shelf number & room number

26
		$libraryMap->findBookBy($book->getTitle(), $book->getAuthor());
27
	}
28
29
}

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

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

Заключительные мысли

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

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

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.