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>Первый порыв у многих разработчиков - ввести два класса: Under18 и Above18. Дальше в каждом из классов добавить по методу getMessage(). В итоге мы получили полиморфизм подтипов:</p>
5
<p>Первый порыв у многих разработчиков - ввести два класса: Under18 и Above18. Дальше в каждом из классов добавить по методу getMessage(). В итоге мы получили полиморфизм подтипов:</p>
6
<p>Это решение хоть и работает, но ведет не по тому пути. Сегодня у нас до 18 и после, потом появится отдельное поведение для тех кто старше 65. Все станет еще хуже, когда кроме этих разделений появится дополнительное разделение по полу. В таком случае мы получим большое число комбинаций, под каждую из которых придется создать отдельный класс пользователя:</p>
6
<p>Это решение хоть и работает, но ведет не по тому пути. Сегодня у нас до 18 и после, потом появится отдельное поведение для тех кто старше 65. Все станет еще хуже, когда кроме этих разделений появится дополнительное разделение по полу. В таком случае мы получим большое число комбинаций, под каждую из которых придется создать отдельный класс пользователя:</p>
7
<ul><li>девушки старше 18</li>
7
<ul><li>девушки старше 18</li>
8
<li>девушки младше 18</li>
8
<li>девушки младше 18</li>
9
<li>парни старше 18</li>
9
<li>парни старше 18</li>
10
<li>парни младше 18</li>
10
<li>парни младше 18</li>
11
<li>...</li>
11
<li>...</li>
12
</ul><p>В книжках по паттернам любят приводить пример с разделением средств передвижения по типам: плавающие, летающие и ездящие. А потом внезапно оказывается, что некоторые одновременно и плавают, и ездят.</p>
12
</ul><p>В книжках по паттернам любят приводить пример с разделением средств передвижения по типам: плавающие, летающие и ездящие. А потом внезапно оказывается, что некоторые одновременно и плавают, и ездят.</p>
13
<p>Теперь попробуем ответить на вопрос, почему эту задачу не надо решать подтипами в любом случае. Сам по себе пользователь - это сущность, взятая из нашей предметной области. Предметная область и вывод текста на экран - это совершенно разные вещи. Второе относится к логике приложения, но не бизнес-логике. Если об этом не задумываться, то в конце концов настанет момент, когда внутри пользователя окажется вообще все, что только происходит на сайте, ведь оно все так или иначе связано с самим пользователем. И мы получим<a>божественный объект</a>.</p>
13
<p>Теперь попробуем ответить на вопрос, почему эту задачу не надо решать подтипами в любом случае. Сам по себе пользователь - это сущность, взятая из нашей предметной области. Предметная область и вывод текста на экран - это совершенно разные вещи. Второе относится к логике приложения, но не бизнес-логике. Если об этом не задумываться, то в конце концов настанет момент, когда внутри пользователя окажется вообще все, что только происходит на сайте, ведь оно все так или иначе связано с самим пользователем. И мы получим<a>божественный объект</a>.</p>
14
<p>Правильное решение в таких ситуациях построено на композиции - подходе, основанном на взаимодействии объектов, а не на иерархии классов. Начнем сначала. В нашей задаче есть две ситуации: пользователи до 18 лет и пользователи старше. Это два разных варианта поведения, которые будут описываться двумя разными классами. Назовем их:<em>GreetingForAbove18</em>и<em>GreetingForUnder18</em>. В каждом из классов реализуем метод<em>getMessage</em>. В каждом из классов этот метод будет возвращать именно то приветствие, которое требуется для этой категории пользователей.</p>
14
<p>Правильное решение в таких ситуациях построено на композиции - подходе, основанном на взаимодействии объектов, а не на иерархии классов. Начнем сначала. В нашей задаче есть две ситуации: пользователи до 18 лет и пользователи старше. Это два разных варианта поведения, которые будут описываться двумя разными классами. Назовем их:<em>GreetingForAbove18</em>и<em>GreetingForUnder18</em>. В каждом из классов реализуем метод<em>getMessage</em>. В каждом из классов этот метод будет возвращать именно то приветствие, которое требуется для этой категории пользователей.</p>
15
<p>Как пользователь будет взаимодействовать с объектами этих классов? Варианта два: либо мы передаем его в конструктор, либо в сам метод getMessage(user). Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем все понятно. Пользователь - это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения - это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:</p>
15
<p>Как пользователь будет взаимодействовать с объектами этих классов? Варианта два: либо мы передаем его в конструктор, либо в сам метод getMessage(user). Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем все понятно. Пользователь - это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения - это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:</p>
16
<p>За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.</p>
16
<p>За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.</p>
17
<p>Главное в этой схеме то, что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.</p>
17
<p>Главное в этой схеме то, что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.</p>
18
<p>Что еще более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.</p>
18
<p>Что еще более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.</p>
19
<p><em>Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.</em></p>
19
<p><em>Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.</em></p>
20
<p>В итоге, в коде появляется большое количество небольших классов. Количество этих классов равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.</p>
20
<p>В итоге, в коде появляется большое количество небольших классов. Количество этих классов равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.</p>