HTML Diff
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>