Приложения взаимодействуют со своей средой, но чем больше они масштабируются, тем важнее четко отделить бизнес-логику от внешнего мира.
Фундаментальное правило программного обеспечения: масштабирование выявляет слабые стороны. Многие архитектуры прекрасно работают для небольших приложений, но рушатся под их тяжестью при добавлении дополнительных функций. Разработчик приложения, которому посчастливилось нести ответственность за большую кодовую базу, должен понимать, как его проект может дать сбой при масштабировании, и предпринимать соответствующие действия для рефакторинга, пока не стало слишком поздно.
«Как я уже говорил, масштабируемость вашего приложения зависит от того, насколько хорошо вы отделяете бизнес-логику от состояния, но в этой статье я хотел бы обсудить еще один не менее важный фактор: входы и выходы (I/O). Нам нужно отделить внутреннее от внешнего.
Ввод-вывод зависит от контекста, поэтому его сложнее обобщить, чем бизнес-логику. Я сосредоточусь на знакомой мне области — видеоплеерах — потому что я пытаюсь заполнить пробел в документации по этому типу приложений. Однако эти идеи могут найти широкое применение.
Видеоплееры масштабируются за счет добавления функций (и других функций) поверх одного или нескольких основных механизмов воспроизведения. Каждая функция добавляет новый поток перекрывающейся бизнес-логики, каждый со своими входами и выходами. Эта сложность требует различных типов взаимодействия ввода-вывода сверх того, что вы найдете в большинстве веб-приложений.
Четыре аспекта
У ввода-вывода есть два аспекта: направление и специфичность.
Направление достаточно прямолинейное. У вас есть данные, поступающие в приложение извне; это ввод. Вы преобразовали данные, выходящие из приложения, что может привести к побочным эффектам; это вывод.
Другое измерение, специфичность, немного более тонкое. Ввод-вывод является рассеянным, если вашему приложению все равно, откуда поступают входные данные или куда направляются выходные данные. Ввод/вывод является конкретным, если вам нужно знать точный интерфейс, с которым вы взаимодействуете.
1. Диффузный ввод: вызовы функций API
В любом приложении вы должны позволить внешнему контексту запускать внутреннюю бизнес-логику.
Возьмем функцию паузы видеоплеера. Игроку все равно, кто хочет поставить медиа на паузу; его можно вызвать нажатием кнопки или каким-либо процессом в самом контексте. Любой код, который должен приостановить воспроизведение мультимедиа, вызывает эту функцию, а проигрыватель позаботится обо всем остальном.
Базовым требованием здесь является то, что API создается вашим приложением, а затем становится доступным в контексте вашего приложения для доступа к внешнему коду.
Но будь осторожен; здесь будут драконы. Я видел слишком много объектов «Приложение», которые содержат функции API и создают зоопарк, полный различных менеджеров и контроллеров. Эти классы бога приложений почти всегда становятся мусорными свалками для всего кода, который больше нигде не помещается.
Это зловещее бьющееся сердце неразберихи диспетчера контроллеров. Это первородный грех каждого неудачного приложения.
Но какова масштабируемая альтернатива? Проблема здесь не столько в API, сколько в том, что его экземпляр используется как класс бога.
Каждая часть бизнес-логики должна быть отделена от других и самого API. Создание экземпляра бизнес-логики отличается от создания вашего API.
Класс API не должен ничего делать, кроме как направлять вызовы на правильную бизнес-логику. Для тех, чей язык включает аннотации, вы можете даже создать API во время компиляции, избавив себя от искушения загружать туда случайную бизнес-логику. К сожалению, экосистема JavaScript не потворствует аннотациям, поэтому все делается вручную — просто старайтесь не усложнять.
2. Диффузный вывод: события
Часто в момент создания или изменения состояния в вашем приложении вы хотите уведомить заинтересованные внешние приложения, чтобы они могли использовать данные в качестве входных данных для своей собственной бизнес-логики. Мероприятие подходит для этого. События могут потребляться в десяти местах или ни в одном. Вашему приложению все равно.
Таким образом, события — это основной способ разделения приложений, и даже разработчики начального уровня должны быть знакомы с ними, особенно в разработке пользовательского интерфейса.
Но не ошибитесь; события часто используются неправильно. Они могут уничтожить ваше приложение.
Проблема возникает, когда вы используете события для передачи данных в приложении между двумя фрагментами кода, которые иначе не знают друг о друге. Конечно, это лучше, чем вводить друг в друга всех этих случайных контроллеров и менеджеров, верно?
Не тот случай. Это на самом деле хуже. Намного хуже. Жесткие вызовы функций для других контроллеров становятся безумно беспорядочными, но, по крайней мере, вы можете отследить, что делает ваш код, если у вас достаточно стойкости. Однако, если ваша внутренняя бизнес-логика зависит от событий, вы напрашиваетесь на неприятности.
Я видел этот паттерн (и его еще более агрессивного родственника, общий изменяемый объект), поэтому часто меня немного тошнит, когда я сталкиваюсь с ним в кодовой базе.
Вот в чем проблема: по определению, события потребляются неопределенным кодом и запускаются в неопределенном порядке. По мере увеличения сложности с масштабированием две или более строк выполнения кода, запускаемых одним и тем же событием, в конечном итоге будут непредсказуемо преобразовывать один и тот же фрагмент состояния, вызывая кошмарные ошибки.
Ваш внутренний порядок исполнения раскрывается, как пиньята на дне рождения.
Как только вы используете события внутри своей бизнес-логики, нет хорошего способа засунуть конфету обратно в пиньяту. Вы вынуждены добавить состояние специального использования, чтобы устранить неоднозначность возникающих маленьких серых областей перекрывающихся исполнений. Один, потом другой, потом еще.
Говорят, что благими намерениями вымощена дорога в ад.
3. Конкретные входы: плагины
Часто необходимо и желательно позволить внешнему коду участвовать в бизнес-логике вашего приложения. Это может повысить расширяемость вашего кода, позволяя заменять функциональные возможности во время выполнения.
Но этот внешний код не должен реагировать на событие и отправлять обратно свои данные через вызов функции API. Это еще один шаблон, который я видел (и построил, к сожалению), и, очевидно, он сталкивается с теми же проблемами, что и внутренние события, описанные выше.
Вместо этого вы хотите найти место в своем коде, где вы можете инкапсулировать выполнение этой внешней бизнес-логики, а затем создать интерфейс, специально предназначенный для обработки обмена.
Преимущество здесь в том, что ваше внутреннее приложение сохраняет полный контроль над порядком выполнения. Внешний код, часто подключаемый модуль, который загружается во время выполнения, получает соответствующие данные через обратный вызов, выполняет свою работу, а затем возвращает преобразованные данные.
А дискретный интерфейс легко документировать и понимать. Не недооценивайте важность предоставления другим командам возможности взаимодействовать с вашим кодом предопределенным, автономным способом. Плагины — это легкая победа в плане расширяемости. Хорошая настройка плагина позволяет использовать ваше приложение так, как вы никогда не ожидали, и это хорошо!
4. Конкретные результаты: интерфейсы приложений
В крупномасштабных приложениях мы часто создаем экосистему одноранговых подприложений, каждое из которых имеет свою сложную бизнес-логику и многогранный API. Эти подприложения должны быть построены вдоль плоскостей разлома в вашей системе, что позволит нескольким командам независимо работать над вашим приложением.
Например, есть тонкие (и не очень) различия в низкоуровневом воспроизведении видео между разными браузерами и устройствами. Любой достойный видеоплеер должен абстрагировать эти детали от бизнес-логики более высокого уровня, скрывая их за общим интерфейсом.
Эти интерфейсы, однако, должны быть тщательно сконструированы, иначе они станут источником проблем и проблем.
Самая большая проблема — утечка общих состояний через интерфейс. Часто я видел команды, которые не зашли достаточно далеко, чтобы изолировать одну кодовую базу от другой.
Что делать, если вам нужно изменить способ работы состояния воспроизведения? Теперь необходимо провести рефакторинг по обеим сторонам интерфейса, включая каждую реализацию — отследить это непросто. Этот тип связи может очень затруднить рефакторинг вашей системы, поскольку изменения на одной стороне интерфейса могут привести к непредвиденным результатам на другой стороне.
Лучше держать эти интерфейсы непроницаемыми. Превратите их в формализованный API. Никогда не перемещайте состояние напрямую с одной стороны на другую. Разместите код в разных репозиториях. По возможности держите команды раздельно. Никогда не позволяйте деталям реализации с одной стороны API изменять другую без явного изменения официальной документации.
Когда вы сделаете это, вы часто обнаружите, что вам нужно отточить API до чего-то достаточно общего, чтобы работать для каждого возможного варианта использования. Это хорошая вещь. Это заставляет вас серьезно подумать, где провести границу между подприложениями.
Принцип разделения интерфейса
API-интерфейсы для любого обычного приложения предназначены для обработки широкого спектра функций. Возьмите API для видеоэлемента HTML. Он полон функций и свойств для решения самых разных задач бизнес-логики: запуск мультимедиа, воспроизведение, звук, подписи, макет и т. д.
Почти каждый видеопроигрыватель, с которым я когда-либо имел дело, рассматривает эту мешанину случайностей как монолитную сущность, создавая чудовищные классы из нескольких тысяч строк для обработки всех этих взаимодействий в одном месте, независимо от того, насколько они не связаны между собой.
Эм, мы можем не делать этого, пожалуйста? Есть отличный способ справиться с этой проблемой. А именно Я в SOLID — Принцип интеграции-сегрегации.
Я видел, как команды практически поклоняются SOLID на словах, но далеки от идеала на практике. Они ЛЮБЯТ D (Инверсия зависимости), злоупотребляют L (Замена Лисков), неправильно понимают O (Открыто-закрыто), открыто нарушают S (Единственная ответственность) , и относитесь к я так, как будто его не существует.
Но в крупномасштабном внешнем приложении, взаимодействующем с API других приложений, разделение интерфейса, возможно, является наиболее важным и полезным из принципов SOLID. Это принцип, соблюдение которого позволяет правильно выполнять все остальные.
Когда API разбивается на разные интерфейсы, каждый интерфейс может быть сосредоточен исключительно на том фрагменте функциональности, который требуется определенной части бизнес-логики. Это имеет основополагающее значение для перехода вашего приложения от путаницы диспетчера контроллеров к расширяемой модели, ориентированной на бизнес-логику.
Это последнее из моих архитектурных очерков за пока. Спасибо всем за то, что дочитали до этого места, и я надеюсь, что вы нашли для себя что-то полезное.
О реактивном программировании можно сказать еще много, и я намерен вернуться к этой теме, но на данный момент я заинтересован в решении еще одного насущного вопроса.
А именно, как нам создать эффективный процесс создания программного обеспечения?
А пока удачного кодирования!