Создание действительно модульного кода без зависимостей

Опубликовано: 2022-03-11

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

Перенесемся на несколько месяцев вперед, и скорость вашего развития снизится. Это потому, что вы не работаете так усердно, как раньше? Не совсем. Давайте перенесемся еще на несколько месяцев вперед, и ваша скорость разработки снизится еще больше. Работа над этим проектом больше не доставляет удовольствия и стала обузой.

Становится хуже. Вы начинаете обнаруживать множество ошибок в своем приложении. Часто устранение одной ошибки порождает две новые. В этот момент вы можете начать петь:

99 маленьких ошибок в коде. 99 маленьких жуков. Снимите один, залатайте его,

…127 маленьких ошибок в коде.

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

Этот опыт распространен в мире программного обеспечения и может объяснить, почему так много программистов хотят выбросить исходный код и все переписать.

Причины, по которым разработка программного обеспечения со временем замедляется

Так в чем причина этой проблемы?

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

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

Большой ком грязи и как его уменьшить

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

Так что же это за анти-шаблон? Проще говоря, вы получаете большой ком грязи, когда каждый элемент имеет зависимость с другими элементами. Ниже вы можете увидеть график зависимостей от известного проекта с открытым исходным кодом Apache Hadoop. Чтобы визуализировать большой клубок грязи (точнее, большой клубок пряжи), вы рисуете круг и равномерно распределяете по нему классы из проекта. Просто нарисуйте линию между каждой парой классов, которые зависят друг от друга. Теперь вы видите источник своих проблем.

Визуализация «большого комка грязи» Apache Hadoop с несколькими десятками узлов и сотнями линий, соединяющих их друг с другом.

«Большой ком грязи» Apache Hadoop

Решение с модульным кодом

Поэтому я задал себе вопрос: можно ли уменьшить сложность и по-прежнему получать удовольствие, как в начале проекта? По правде говоря, вы не можете устранить все сложности. Если вы хотите добавить новые функции, вам всегда придется повышать сложность кода. Тем не менее, сложность можно перемещать и отделять.

Как другие отрасли решают эту проблему

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

Техническая схема физического механизма и то, как его части соединяются друг с другом. Части пронумерованы в том порядке, в котором они будут присоединяться следующими, но этот порядок слева направо соответствует 5, 3, 4, 1, 2.

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

Можем ли мы повторить это в индустрии программного обеспечения?

Мы можем точно! Используя интерфейсы и инверсию принципа управления; Самое приятное то, что этот подход можно использовать на любом объектно-ориентированном языке: Java, C#, Swift, TypeScript, JavaScript, PHP — список можно продолжать и продолжать. Вам не нужна какая-то причудливая структура, чтобы применить этот метод. Вам просто нужно придерживаться нескольких простых правил и оставаться дисциплинированным.

Инверсия контроля — ваш друг

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

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

Проблема

Предположим, что у нас есть очень простое приложение, состоящее только из класса Main , трех служб и одного класса Util . Эти элементы зависят друг от друга множеством способов. Ниже вы можете увидеть реализацию с использованием подхода «большой ком грязи». Классы просто вызывают друг друга. Они тесно связаны, и вы не можете просто вынуть один элемент, не касаясь других. Приложения, созданные с использованием этого стиля, позволяют изначально быстро расти. Я считаю, что этот стиль подходит для проектов проверки концепции, поскольку вы можете легко экспериментировать с вещами. Тем не менее, это не подходит для готовых к производству решений, потому что даже обслуживание может быть опасным, а любое отдельное изменение может привести к непредсказуемым ошибкам. На приведенной ниже диаграмме показан этот большой шар глиняной архитектуры.

Main использует сервисы A, B и C, каждый из которых использует Util. Служба C также использует службу A.

Почему инъекция зависимостей не удалась

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

Предыдущая архитектура, но с внедрением зависимостей. Теперь Main использует службы интерфейса A, B и C, которые реализуются соответствующими службами. Службы A и C используют интерфейсную службу B и интерфейсную утилиту, которая реализуется Util. Служба C также использует интерфейсную службу A. Каждая служба вместе со своим интерфейсом считается элементом.

Единственная разница между текущей ситуацией и большим комом грязи заключается в том, что теперь вместо того, чтобы вызывать классы напрямую, мы вызываем их через их интерфейсы. Это немного улучшает отделение элементов друг от друга. Если, например, вы хотите повторно использовать Service A в другом проекте, вы можете сделать это, удалив сам Service A вместе с Interface A , а также Interface B и Interface Util . Как видите, Service A по-прежнему зависит от других элементов. В результате мы по-прежнему получаем проблемы с изменением кода в одном месте и нарушением поведения в другом. Это по-прежнему создает проблему, заключающуюся в том, что если вы измените Service B и Interface B , вам нужно будет изменить все элементы, которые зависят от него. Этот подход ничего не решает; на мой взгляд, это просто добавляет слой интерфейса поверх элементов. Вы никогда не должны внедрять какие-либо зависимости, вместо этого вы должны избавиться от них раз и навсегда. Ура независимости!

Решение для модульного кода

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

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

Обратите внимание, что в этой архитектуре только класс Main имеет несколько зависимостей. Он связывает все элементы вместе и инкапсулирует бизнес-логику приложения.

Услуги, с другой стороны, являются полностью независимыми элементами. Теперь вы можете удалить каждую службу из этого приложения и повторно использовать их в другом месте. Они не зависят ни от чего другого. Но подождите, становится лучше: вам больше не нужно изменять эти службы, пока вы не измените их поведение. Пока эти сервисы делают то, что должны делать, их можно оставить нетронутыми до скончания века. Они могут быть созданы профессиональным инженером-программистом или программистом, впервые скомпрометировавшим худший спагетти-код, который кто-либо когда-либо готовил, с примешанными goto . Это не имеет значения, потому что их логика инкапсулирована. Каким бы ужасным это ни было, оно никогда не распространится на другие классы. Это также дает вам возможность разделить работу над проектом между несколькими разработчиками, где каждый разработчик может работать над своим компонентом независимо, не прерывая другого или даже не зная о существовании других разработчиков.

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

Шаблон элемента

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

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

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

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

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

Простая схема более сложного элемента. Здесь в широком смысле слово «элемент» состоит из шести частей: вид; Логики А, В и С; Элемент; и прослушиватель элементов. Отношения между последними двумя и приложением такие же, как и раньше, но внутренний элемент также использует логику A и C. Логика C использует логику A и B. Логика A использует логику B и представление.

Давайте также взглянем на простой пример «Hello World», созданный на Java.

 public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }

Сначала мы определяем ElementListener , чтобы указать метод, выводящий вывод. Сам элемент определен ниже. При вызове sayHello для элемента он просто печатает сообщение с помощью ElementListener . Обратите внимание, что элемент полностью независим от реализации метода printOutput . Его можно распечатать в консоли, на физическом принтере или в причудливом пользовательском интерфейсе. Элемент не зависит от этой реализации. Из-за этой абстракции этот элемент можно легко повторно использовать в различных приложениях.

Теперь взгляните на основной класс App . Он реализует слушателя и собирает элемент вместе с конкретной реализацией. Теперь мы можем начать использовать его.

Вы также можете запустить этот пример на JavaScript здесь

Элементная архитектура

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

Структура полнофункционального веб-приложения, которое мне нравится использовать, выглядит следующим образом:

 src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements

В папке с исходным кодом мы изначально разделили файлы клиента и сервера. Это разумно, поскольку они работают в двух разных средах: в браузере и на внутреннем сервере.

Затем мы разделяем код каждого слоя на папки с именами app и elements. Элементы состоят из папок с независимыми компонентами, а папка приложения связывает все элементы вместе и хранит всю бизнес-логику.

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

Практический пример

Полагая, что практика всегда важнее теории, давайте взглянем на реальный пример, созданный в Node.js и TypeScript.

Пример из реальной жизни

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

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

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

Разрабатывайте быстрее, используйте чаще!

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

  • Многие проблемы в программном обеспечении возникают из-за зависимостей между несколькими компонентами.

  • Внося изменения в одном месте, вы можете вызвать непредсказуемое поведение где-то еще.

Три общих архитектурных подхода:

  • Большой шар грязи. Он отлично подходит для быстрой разработки, но не так хорош для стабильных производственных целей.

  • Внедрение зависимости. Это половинчатое решение, которого следует избегать.

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

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

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

Теперь вы можете пойти и начать создавать и делиться своими собственными элементами. В долгосрочной перспективе это поможет вам создавать простые в обслуживании продукты. Удачи и дайте мне знать, что вы создали!

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

Связанный: Лучшие практики JS: создание бота Discord с помощью TypeScript и внедрения зависимостей