Где-то в последние несколько лет я прочитал и получил огромное удовольствие от Философии проектирования программного обеспечения Джона Оустерхаута. Автор старается не преувеличивать свои утверждения и всегда предоставляет ограничения и контраргументы для каждого, но один пункт, в частности, все же нанес легкую пощечину.

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

Я был шокирован (или эквивалент в программировании 😅)! Я никогда раньше не слышал этого совета, но с минимальными размышлениями смог определить, где я попал в этот антишаблон и в результате чего получилась слабая и извилистая архитектура.

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

Слои

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

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

Программисты всегда работают в каком-то среднем слое. Уровни ниже могут быть «системой», платформой, оборудованием или сервисным API. Код, который они создают, предоставляет что-то слоям выше в виде библиотеки, API, пользовательского интерфейса или приложения.

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

Кроме того, одной из моих самых больших проблем со многими объектно-ориентированными кодовыми базами является то, как имена классов часто просачиваются через все границы. Аккуратно организованные папки проекта и привередливые полные имена пакетов обратного DNS выглядят хорошо, но мало что значат, если любой класс может ссылаться на любой другой по имени 🤦🏻‍♂️

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

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

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

Пример чередования оставшихся и непосредственных слоев

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

  • Слои сохраненного режима поддерживают какое-то постоянное состояние. Они носят декларативный характер, например. в этой сцене есть 3 объекта, один находится здесь, один находится там, а другой находится там .
  • Слои непосредственного режима предоставляют команды, которые можно вызывать без контекста. Они носят процедурный характер, например. сделай это, сделай это.

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

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

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

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

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

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

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

Этот виртуальный DOM затем применяется к фактическому DOM браузера, который сохраняется и сохраняется по своей природе. Отдельный процесс обеспечивает применение только минимально необходимых обновлений, чтобы привести состояние DOM браузера в соответствие с новой виртуальной версией.

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

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