Сделать его легко, сделать так, чтобы его можно было расширить, не стреляя себе в ногу? Это не так просто

Написание инструмента CLI (интерфейс командной строки) в 2022 году с использованием Node.js больше не является реальной проблемой, поскольку уже существует бесчисленное множество пакетов, которые помогут вам сделать именно это.

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

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

Что мы тогда делаем?

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

В частности, проект, который я создал, предоставляет 3 команды для работы со строками:

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

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

Вот скриншот работы нашего инструмента, отображающий экран справки по умолчанию:

Обратите внимание на 3 команды (split, upper и wc) и дополнительную команду «help», которая предоставит подробную информацию о каждой из остальных.

Инструменты, которые мы будем использовать

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

Конечно, это будет построено с использованием Node.js с простым JavaScript, но вы определенно можете добавить TS или любой другой дополнительный слой, который вам нравится. Я просто покажу вам основы.

Что заставило меня пойти с командиром? Две вещи:

  1. Его набор функций. Как мы увидим, добавление команд, атрибутов и их реализация довольно просты. Для этого требуется нулевой шаблон.
  2. Кажется, он довольно популярен и активно поддерживается. По данным NPM, его загружают более 86 миллионов раз в неделю, и он обновлялся за последние два месяца (на момент написания этой статьи). Так что он ставит все флажки, которые мне нужны, чтобы я мог ему доверять.

Наконец, последний «инструмент», который я собираюсь использовать, — это не обязательно кусок кода, а шаблон проектирования, один из моих любимых: шаблон команды, удачно названный им. Давайте быстро взглянем на него, чтобы вы могли понять, почему я его использую.

Понимание шаблона команды

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

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

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

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

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

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

  1. Все команды имеют одинаковую «форму» в том смысле, что все они реализуют один и тот же интерфейс (если у вас есть такая конструкция, как с TS) и расширяют один и тот же общий класс. Этот последний пункт касается повторного использования кода, все команды будут иметь одинаковое поведение в том смысле, что они могут быть выполнены, они, вероятно, будут генерировать какой-то вывод и структурно, они, скорее всего, будут иметь одни и те же основные характеристики (т.е. в нашем случае все они будут иметь имя, описание, набор атрибутов и опций).
  2. «Клиентскому» коду, выполняющему команду, не нужно точно знать, какую именно команду он выполняет. Все они должны вызываться из одного и того же места, и это место может быть где угодно внутри клиентского кода. Именно это изолирует эти «капсулы», о которых я упоминал ранее, от окружающего их контекста.

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

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

Если вам понравилось то, что вы прочитали до сих пор, рассмотрите возможность подписаться на мою БЕСПЛАТНУЮ рассылку Бессвязная болтовня старого разработчика и получайте регулярные советы об ИТ-отрасли прямо в свой почтовый ящик.

Внедрение клиентского кода в первую очередь

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

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

Итак, давайте посмотрим и посмотрим, что я имею в виду:

Это весь клиентский код, который нам понадобится. Итак, давайте разделим его на разделы:

  1. Первое, что мы делаем, это импортируем как пакет Commander, так и наш список команд (мы рассмотрим его через минуту).
  2. Затем мы перебираем наш список команд и для каждой определяем команду, которую должен понять Commander. Вот как работает библиотека: вы определяете команду, а затем анализируете аргументы командной строки, чтобы определить, какой из них необходимо выполнить.
  3. Затем мы анализируем ввод и решаем, какой метод action команды вызывать. Это делается в последней строке кода, вызывая метод parse командира.

Это шаблон команды во всей красе. Мы прошлись по списку команд, как если бы они были одним и тем же объектом (и это не так, как мы увидим через минуту). Для каждого из них мы просили одинаковые атрибуты, и никто из них не жалуется на это. Наконец, мы определили выполнение их бизнес-логики для каждого из них (строки 23–26), вызвав их метод action, и знаете что? Никто из них не сказал: «Подождите, нет, я не знаю, о каком методе вы говорите». Это потому, что все они имеют одну и ту же «форму», или, другими словами, все они расширяют один и тот же общий класс.

Теперь давайте взглянем на этот общий класс, который, как вы скоро увидите, не имеет внутри никакой магии:

По сути, это фиктивный класс. Он определяет, какие атрибуты будут иметь все команды, а затем определяет метод action, который все они должны реализовать. Если бы у нас был доступ к абстрактным методам или классам в JS, этот был бы идеальным кандидатом для него, поскольку вы никогда не создавали бы экземпляр класса Command напрямую, он нужен только для того, чтобы убедиться, что все команды соответствуют одним и тем же стандартам.

И под этим я подразумеваю, что все они должны, по крайней мере, реализовать метод action. Если им нужны дополнительные методы для их конкретной бизнес-логики, все они должны быть закрытыми. Но, увы, это JS, земля публики, по крайней мере, пока не выйдет частный оператор.

Таким образом, обычная команда будет выглядеть так:

Итак, основные места, на которые следует обратить внимание в первую очередь:

  1. Конструктор. Как видите, наша команда не принимает никаких параметров, а сама их заранее определяет. Это означает, что эта команда не зависит от своего контекста, и любой, кто ее вызовет, получит тот же результат, потому что она уже точно знает, как определить себя.
  2. Метод action. Он определяет бизнес-логику для нашей команды. Он предназначен для полной перезаписи того, что определено в классе Command, поэтому мы не вызываем другой через super или что-то в этом роде. И он всегда будет получать 2 атрибута: строку, с которой мы имеем дело, и параметры, указанные пользователем. Это стандарт и определяется Commander, поэтому мы можем ему доверять. Однако, если бы мы хотели быть еще более изолированными от внешнего мира, мы могли бы просто ожидать объект «полезной нагрузки», и каждая команда работала бы с ним так, как им нужно.
  3. Наконец, два вспомогательных метода, _countWords и _countSingleWord, здесь представляют «частные» методы. Часть команды, которая используется только внутри и в идеале, не будет доступна из внешнего мира.

Если вы хотите посмотреть, как были реализованы другие (более простые) команды, вот вам полный репозиторий, чтобы поиграть с ним.

Сделать его легко расширяемым

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

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

Однако что произойдет, если нам нужно ДОБАВИТЬ новую команду? Вернувшись к фрагменту клиентского кода, вы можете видеть, что мы импортируем список команд из папки «commands». Но как мы собираем этот список? Он жестко запрограммирован или динамичен?

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

Вот как выглядит файл index.js внутри папки commands:

Как видите, все, что мы делаем, — это просматриваем все файлы внутри папки (папка commands), и если они заканчиваются на «Command.js», то мы их импортируем и создаем экземпляры.

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

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

Магия!

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

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

Вы использовали шаблон команды в прошлом? А как насчет любого другого шаблона проектирования? Какой твой любимый? Поделись в комментариях, а я поделюсь своим!

Создавайте компонуемые веб-приложения

Не создавайте веб-монолиты. Используйте Bit для создания и компоновки несвязанных программных компонентов — в ваших любимых средах, таких как React или Node. Создавайте масштабируемые и модульные приложения с мощными и приятными возможностями разработки.

Перенесите свою команду в Bit Cloud, чтобы вместе размещать компоненты и совместно работать над ними, а также ускорять, масштабировать и стандартизировать разработку в команде. Попробуйте компонуемые внешние интерфейсы с помощью Design System или Микроинтерфейсы или изучите компонуемые внутренние интерфейсы с серверными компонентами. .

Попробуйте →

Узнать больше