Найти тему
Nuances of programming

Советы по разработке больших приложений JavaScript

Источник: Nuances of Programming

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

К примеру, запуск GED VIZ для фонда Bertelsmann произошел в 2012 году, выпуск — в 2013 году, а новые функции и данные добавлялись каждые несколько лет. В 2016 году мы провели значительную оптимизацию базовой визуализации, превратив ее в библиотеку многократного использования. Механизм визуализации потока данных до сих пор используется Европейским Центральным банком (ЕЦБ). Еще одним долгосрочным проектом является разработка внешнего портала данных ОЭСР: Реализация началась в 2014 году, а расширение кодовой базы продолжается до сих пор.

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

Замена крупномасштабного рерайта небольшими улучшениями

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

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

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

Обучение на основе долгосрочных проектов

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

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

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

Главный вопрос: какие изменения можно внести сейчас? Что необходимо улучшить? У каждого разработчика порой возникает желание уничтожить все, что есть, и начать с нуля.

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

Избегайте сложных структур

“Сложные” — не означает большие. В каждом нетривиальном проекте содержится много логики. Много случаев, требующих рассмотрения и проверки. Множество различных данных, требующих обработки.

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

Рассмотрим простые и сложные структуры данных в JavaScript.

Функции

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

Для написания хорошего кода на JavaScript не обязательно наличие высокоуровневых шаблонов проектирования. В первую очередь, необходимо умение использовать самую простую технику разумным и целесообразным способом: программу следует структурировать с помощью функций, правильно выполняющих одно определенное действие. Затем низкоуровневые функции необходимо сопоставить с высокоуровневыми.

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

Объекты

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

const cat = { name: 'Maru', meow() { window.alert(`${this.name} says MEOW`); }};cat.meow();

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

Классы

Самая сложная структура в JavaScript — это класс. Он представляет собой основу для объектов и, в то же время, производит эти самые объекты. Он представляет собой смесь прототипного наследования с созданием объектов. Он переплетает логику (функции) с данными (свойствами экземпляра). Иногда в функции конструктора содержатся свойства, называемые “статическими” свойствами. Такие шаблоны, как “singleton”, перегружают класс еще большим количеством логики.

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

Использование классов допустимо, если они имеют одну определенную цель. Согласно моему опыту, следует избегать добавления большого количества вопросов в класс. К примеру, компоненты в React, содержащие внутреннее состояние, обычно объявляются как классы. Это имеет смысл лишь для конкретной проблемной области. Они обладают одной определенной целью: Группировка данных props и state и пары функций, работающих с этими типами данных. В центре класса находится функция render.

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

Среди компонентов в Angular встречаются аналогичные проблемы: Поля метаданных применяются с использованием декоратора @Component(). Внедрение зависимостей через конструктор класса. Состояние как свойства экземпляра (входные и выходные данные, а также общедоступные пользовательские и частные свойства). Такие классы вовсе не являются простыми или одноцелевыми. Ими можно управлять только в том случае, если они содержат необходимую специфическую логику Angular.

Выбор структур

Несколько рекомендаций согласно моему многолетнему опыту:

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

Большинство фреймворков JavaScript имеют свой собственный способ структурирования кода. В основе компонентно-ориентированных UI фреймворков, таких как React и Angular, компоненты обычно представлены объектами или классами. Лучше отдать предпочтение композиции, а не наследованию: Чтобы разделить задачу на несколько частей, просто создайте новый облегченный класс компонентов.

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

Большое количество модулей

Раньше управление зависимостями между файлами JavaScript и внешними библиотеками доставляло множество неприятностей. Первое время мы применяли модули CommonJS или AMD. Позже сообщество остановилось на стандартных модулях ECMAScript 6.

Модули стали важной структурой кода в JavaScript. В зависимости от использования, модули могут упрощать или усложнять структуру.

Со временем я стал использовать модули совершенно по-другому. Раньше я создавал довольно большие файлы с несколькими экспортами. В противном случае, один экспорт представлял собой огромный объект группировки множества констант и функций. Сейчас я стремлюсь к созданию небольших плоских модулей с одним или несколькими экспортами. В результате, один файл приходится на одну функцию, один файл на один класс и так далее. Файл foo.js будет выглядеть так:

export default function foo(…) {…}

Или так, в случае указания имени экспорта:

export function foo(…) {…}

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

Избегайте создания нетипизированных объектов

Одной из лучших особенностей JavaScript является объектный литерал. Благодаря нему ускоряется создание объектов с произвольными свойствами.

const cat = { name: 'Maru', meow() { window.alert(`${this.name} says MEOW`); }};

Формат JSON настолько прост и выразителен, что со временем он превратился в независимый формат данных, повсеместно распространенный в наши дни. Однако с каждой новой версией ECMAScript, объектный литерал приобретал все больше и больше возможностей, превышающих его первоначальное назначение. Благодаря новым особенностям ECMAScript, таким как Object Rest/Spread, создавать и смешивать объекты стало стало намного проще.

В небольшой кодовой базе создание объектов “на лету” является функцией повышения производительности. Однако в большой кодовой базе объектные литералы становятся помехой. На мой взгляд, в подобных проектах не следует допускать использование объектов с произвольными свойствами.

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

В JavaScript нет определений типов, но есть несколько способов создания объектов более упорядоченным способом. Например, функция может использоваться для создания похожих объектов. Функция гарантирует наличие необходимых свойств, а также допустимость или наличие значения по умолчанию. Также можно использовать класс, создающий простейший шаблон объект-значение.

Аналогичным образом функция может выполнить проверку использования аргумента во время выполнения. Она может непосредственно выполнить проверку типа с использованием typeof, instanceof, Number.isNaN и т. д. или неявно, с использованием утиной типизации.

Дополнение JavaScript определениями типов, такими как TypeScript или Flow, представляет собой более всесторонний подход. К примеру, работа с TypeScript начинается с определения интерфейсов для важных моделей данных. Функции объявляют тип своих параметров и возвращаемых значений. Компилятор TypeScript дает гарантию передачи только разрешенного типа — при условии доступа компилятора ко всем вызовам.

Надежный код

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

Перевод статьи 9elementsMaintaining large JavaScript applications