0 added
0 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>В этом уроке описывается система, которая помогает правильно организовывать код, построенный на классах.</p>
1
<p>В этом уроке описывается система, которая помогает правильно организовывать код, построенный на классах.</p>
2
<p><em>В языках, где ООП построено без инкапсуляции, подобные проблемы решаются проще и возникают реже. Если хочется узнать как это бывает, попробуйте пописать код на Clojure или Elixir</em>.</p>
2
<p><em>В языках, где ООП построено без инкапсуляции, подобные проблемы решаются проще и возникают реже. Если хочется узнать как это бывает, попробуйте пописать код на Clojure или Elixir</em>.</p>
3
<p>Предположим, что мы делаем сайт имеющий механизм аутентификации. После её выполнения, пользователю выводится приветствие, которое строится по-разному в зависимости от возраста пользователя. Если пользователю не исполнилось 18, то пишется одно, всем остальным - другое.</p>
3
<p>Предположим, что мы делаем сайт имеющий механизм аутентификации. После её выполнения, пользователю выводится приветствие, которое строится по-разному в зависимости от возраста пользователя. Если пользователю не исполнилось 18, то пишется одно, всем остальным - другое.</p>
4
<p>В данном случае реализация в лоб через if будет лучшим решением задачи. Но в этом уроке мы отрабатываем использование полиморфизма в рамках классовой модели, поэтому пойдём другим путём. Сама задача специально упрощена, чтобы не тратить время на её анализ.</p>
4
<p>В данном случае реализация в лоб через if будет лучшим решением задачи. Но в этом уроке мы отрабатываем использование полиморфизма в рамках классовой модели, поэтому пойдём другим путём. Сама задача специально упрощена, чтобы не тратить время на её анализ.</p>
5
<p>Первый порыв у многих разработчиков - ввести два класса:</p>
5
<p>Первый порыв у многих разработчиков - ввести два класса:</p>
6
<ul><li>Under18</li>
6
<ul><li>Under18</li>
7
<li>Above18</li>
7
<li>Above18</li>
8
</ul><p>Эти классы реализуют один интерфейс - UserInterface. Дальше в каждом из классов добавляем по методу getGreetingMessage(). В итоге мы получаем полиморфизм подтипов:</p>
8
</ul><p>Эти классы реализуют один интерфейс - UserInterface. Дальше в каждом из классов добавляем по методу getGreetingMessage(). В итоге мы получаем полиморфизм подтипов:</p>
9
<p>Это решение хоть и работает, но ведёт не по тому пути. Сегодня у нас до 18 и после, потом появится отдельное поведение для тех кто старше 65. Всё станет ещё хуже, когда кроме этих разделений, появится дополнительное разделение на девушек и парней. В таком случае мы получим большое число комбинаций, под каждую из которых придётся создать отдельный класс пользователя:</p>
9
<p>Это решение хоть и работает, но ведёт не по тому пути. Сегодня у нас до 18 и после, потом появится отдельное поведение для тех кто старше 65. Всё станет ещё хуже, когда кроме этих разделений, появится дополнительное разделение на девушек и парней. В таком случае мы получим большое число комбинаций, под каждую из которых придётся создать отдельный класс пользователя:</p>
10
<ul><li>девушки старше 18</li>
10
<ul><li>девушки старше 18</li>
11
<li>девушки младше 18</li>
11
<li>девушки младше 18</li>
12
<li>парни старше 18</li>
12
<li>парни старше 18</li>
13
<li>парни младше 18</li>
13
<li>парни младше 18</li>
14
<li>...</li>
14
<li>...</li>
15
</ul><p>В книжках по паттернам любят приводить пример с разделением средств передвижения по типам: плавающие, летающие и ездящие. А потом, внезапно оказывается, что некоторые одновременно и плавают и ездят.</p>
15
</ul><p>В книжках по паттернам любят приводить пример с разделением средств передвижения по типам: плавающие, летающие и ездящие. А потом, внезапно оказывается, что некоторые одновременно и плавают и ездят.</p>
16
<p>Теперь попробуем ответить на вопрос, почему эту задачу не надо решать подтипами в любом случае. Сам по себе, пользователь - это сущность взятая из нашей предметной области. Предметная область и вывод текста на экран, это совершенно разные вещи. Второе относится к логике приложения, но не бизнес-логике. Если об этом не задумываться, то в конце концов настанет момент, когда внутри пользователя окажется вообще всё что только происходит на сайте, ведь оно всё так или иначе связано с самим пользователем. И мы получим<a>божественный объект</a>.</p>
16
<p>Теперь попробуем ответить на вопрос, почему эту задачу не надо решать подтипами в любом случае. Сам по себе, пользователь - это сущность взятая из нашей предметной области. Предметная область и вывод текста на экран, это совершенно разные вещи. Второе относится к логике приложения, но не бизнес-логике. Если об этом не задумываться, то в конце концов настанет момент, когда внутри пользователя окажется вообще всё что только происходит на сайте, ведь оно всё так или иначе связано с самим пользователем. И мы получим<a>божественный объект</a>.</p>
17
<p>Правильное решение основано на композиции, подходе при котором создаются классы под конкретные задачи. Начнём сначала. В нашей задаче есть две ситуации: пользователи до 18 лет и пользователи старше. Создадим интерфейс<em>GreetingMessage</em>с методом getGreetingMessage и реализуем его в двух классах, один<em>GreetingForUnder18</em>и другой<em>GreetingForAbove18</em>. В каждом из них, будет тот вывод, который нужен для конкретного пользователя.</p>
17
<p>Правильное решение основано на композиции, подходе при котором создаются классы под конкретные задачи. Начнём сначала. В нашей задаче есть две ситуации: пользователи до 18 лет и пользователи старше. Создадим интерфейс<em>GreetingMessage</em>с методом getGreetingMessage и реализуем его в двух классах, один<em>GreetingForUnder18</em>и другой<em>GreetingForAbove18</em>. В каждом из них, будет тот вывод, который нужен для конкретного пользователя.</p>
18
<p>Как пользователь будет взаимодействовать с объектами этих классов? Варианта два, либо мы передаём его в конструктор, либо в сам метод getGreetingMessage. Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем всё понятно. Пользователь это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения, это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:</p>
18
<p>Как пользователь будет взаимодействовать с объектами этих классов? Варианта два, либо мы передаём его в конструктор, либо в сам метод getGreetingMessage. Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем всё понятно. Пользователь это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения, это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:</p>
19
<p>За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.</p>
19
<p>За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.</p>
20
<p>Главное в этой схеме, то что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.</p>
20
<p>Главное в этой схеме, то что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.</p>
21
<p>Что ещё более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов, реализующих интерфейс<em>RegistrationEmailText</em>. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.</p>
21
<p>Что ещё более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов, реализующих интерфейс<em>RegistrationEmailText</em>. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.</p>
22
<p><em>Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.</em></p>
22
<p><em>Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.</em></p>
23
<p>В итоге, в коде появляется большое количество небольших интерфейсов (типов) и множество классов их реализующих. Количество классов, реализующих конкретный интерфейс, равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.</p>
23
<p>В итоге, в коде появляется большое количество небольших интерфейсов (типов) и множество классов их реализующих. Количество классов, реализующих конкретный интерфейс, равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.</p>
24
<p>Стоит ли так писать код? Иногда да, но чаще нет. Слепое следование ООП, делает код сложнее и тяжелее, там где подходит простая функция или условная конструкция, начинают вырастать параллельные иерархии классов. В примерах выше это хорошо прослеживается. Задача, которая может быть реализована десятью строчками, решается многими десятками строчек и четырьмя файлами (фабрика, классы и интерфейс). А программист знакомый с абстрактными классами и наследованием, наворотит ещё больше файлов.</p>
24
<p>Стоит ли так писать код? Иногда да, но чаще нет. Слепое следование ООП, делает код сложнее и тяжелее, там где подходит простая функция или условная конструкция, начинают вырастать параллельные иерархии классов. В примерах выше это хорошо прослеживается. Задача, которая может быть реализована десятью строчками, решается многими десятками строчек и четырьмя файлами (фабрика, классы и интерфейс). А программист знакомый с абстрактными классами и наследованием, наворотит ещё больше файлов.</p>
25
<p>Обычно получаемая сложность оправдывается расширяемостью, но это так не работает. Расширяемость нужно добавлять только в тех случаях, когда это необходимо. Другой вопрос, что сам способ организации кода через композицию объектов, является краеугольным камнем при организации кода построенного на классах. При этом надо чётко отслеживать, где у нас абстракция данных, а где действия без состояния, представленные объектами.</p>
25
<p>Обычно получаемая сложность оправдывается расширяемостью, но это так не работает. Расширяемость нужно добавлять только в тех случаях, когда это необходимо. Другой вопрос, что сам способ организации кода через композицию объектов, является краеугольным камнем при организации кода построенного на классах. При этом надо чётко отслеживать, где у нас абстракция данных, а где действия без состояния, представленные объектами.</p>