Как правило, я из тех разработчиков, которые, когда я сталкиваюсь с такими аббревиатурами, как SOLID или DRY, не могут не думать: «О, отлично, вот мы снова с другим набором правил, которые я никогда не буду использовать в практика!».

Эти принципы всегда казались далекими от будничного кодирования.

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

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

Принцип открытости-закрытости — одна из тех концепций, на полное осмысление которых мне потребовалось некоторое время, но как только я уловил ее суть, мой подход к разработке кода существенно изменился.

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

Сначала мне было непонятно, как что-то может быть открыто для расширения, если оно не может быть изменено. Я связывал расширение кода с внесением изменений, но вскоре понял, что эта точка зрения не совсем точна.

Давайте рассмотрим пример, который мы разработали в Creator Now (оцените приложение — оно бесплатное!). Приложение — это опыт, который помогает создателям набирать обороты и развивать свой канал YouTube.

В качестве одного из стимулов нашего уровня геймификации мы внедрили функцию под названием Ежедневные задания.

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

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

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

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

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

Позвольте мне представить вам основу нашей модели.

На высоком уровне наш подход включает создание экземпляра (то есть записи в базе данных) для каждого ежедневного квеста, связанного с пользователем. Трекер отвечает за манипулирование и понимание displayMetadata и claimMetadata.

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

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

Как же тогда мы можем абстрагироваться от этого понятия?

Давайте рассмотрим больше диаграмм и концепций, чтобы улучшить наше понимание того, как это будет работать.

На данный момент мы стремимся создать общий интерфейс, который может работать со всеми типами трекеров. Таким образом, мы можем реализовать специализированные реализации для каждого квеста в нашем приложении.

На основе первоначальных спецификаций мы разработали следующую абстракцию:

Давайте рассмотрим некоторые из этих методов и их обоснование:

  • registerEventListeners(): Наш бэкенд использует механизм обмена данными между модулями, управляемый событиями. Думайте об этом как о канале, по которому отправляются сообщения всякий раз, когда в приложении происходят значимые события (это похоже на шаблон публикация-подписка, если вы с ним знакомы). Например, событие media_watched запускается, когда пользователь заканчивает просмотр видео. Этот основанный на событиях подход идеально сочетается с механизмом отслеживания квестов. Позволяя трекерам прослушивать соответствующие события, они могут регистрироваться на события, которые имеют для них смысл.
  • getWeightForUser(userUid): Этот метод определяет приоритет или вес квестов для конкретных пользователей. Некоторые квесты могут иметь большее значение для определенных пользователей в зависимости от их характеристик. Например, у недавно присоединившегося пользователя квест «Полный профиль» может иметь приоритет в верхней части списка.

Остальные методы должны быть понятными.

Следуя некоторым основным правилам модуляризации и слабой связи (вы можете найти мою статью об этих принципах здесь), мы можем дополнительно создать две диаграммы высокого уровня, которые помогут нам в реализация решения с использованием описанных нами сущностей.

Во-первых, давайте посмотрим, как конкретные трекеры регистрируются в системе:

Как вы можете сделать вывод, 'разрешить средствам отслеживания регистрироваться для прослушивателей событий', по сути, перебирает все эти экземпляры и вызывает registerEventListeners(). QuestTrackerRepository служит контейнером, в котором могут находиться все трекеры, выполняя свою исключительную ответственность.

А теперь самое интересное!

Здесь мы действительно используем созданную нами абстракцию. Он определяет внутренний поток для обработки начала нового цикла квеста (т. е. полночь). Давайте углубимся в это (нажмите на изображение, чтобы развернуть):

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

Это отличный пример принципа инверсии зависимостей, который в конечном итоге позволяет нам достичь принципа открытого-закрытого, как мы вскоре обнаружим!

С точки зрения theQuestService, он остается в неведении ни о специфике квестов, ни об их количестве. Он служит посредником, предназначенным для общего управления процессом создания квеста.

Возвращаясь к нашему исходному пункту: Принцип открытого-закрытого.

В текущем дизайне, что происходит, когда в систему необходимо добавить новый тип квеста (например, новый трекер квестов)? Какие части существующего кода потребуют модификации?

Прелесть нашего подхода в том, что все, что нам нужно сделать, это создать новую специализацию класса QuestTracker и зарегистрировать ее в нашем QuestTrackerRepository.

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

Но подождите, можем ли мы избежать даже этого?

Очень вероятно, что мы можем.

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

Бум.

Благодаря этому усовершенствованию ваш код становится на 100 % закрытым для модификации. Вам не нужно будет изменять какие-либо существующие классы, чтобы добавить новые квесты. Вместо этого, создавая новую конкретную систему отслеживания квестов, вы легко расширяете свой код.

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

Придерживаясь принципа «открыто-закрыто», вы создаете более гибкую и удобную в сопровождении кодовую базу, которая изящно приспосабливается к будущим усовершенствованиям и расширениям.

В следующий раз, когда будете разрабатывать код, подумайте:

Можете ли вы создать его таким образом, чтобы мы могли «присоединить» новую ожидаемую функциональность позже, без повторного посещения существующих шаблонов?