Скрипты, модули и библиотеки

0
377

Скрипты, модули и библиотеки

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

Примеры в этой статье приводятся на JavaScript, но язык не принципиален. Все эти же особенности присущи и остальным динамическим языкам, например, таким как php, python или ruby.

Модуль

Начнем с терминологии. Модуль это файл содержащий определения функций, классов и других сущностей (в зависимости от языка). В разных языках модули называют разными словами, но суть от этого не меняется. Ниже пример модуля содержащего класс User:

// По соглашениям, принятым в JavaScript,
// этот файл должен называться User.js
export default class User {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

Сами по себе модули не являются законченными программами. Их нельзя (бессмысленно) выполнять напрямую, например, запустив в командной строке. Модули предназначены для использования другими модулями (или скриптами). Обычно в языках для этого есть либо механизм импортов, либо механизм автозагрузки, либо то и другое вместе. В JavaScript, чтобы получить доступ к определениям внутри какого-то модуля, его нужно импортировать:

// Какой-то модуль, который использует класс User
import User from ‘./User’;

// Определение функции
const create = (data) => {
  const user = new User(data);
  return user.save()
};

Частая ошибка, которую совершают новички при создании модулей, – выполнение кода вне определений. Например, так:

// Определение функции
const sayHi = () => {
  console.log(‘Boom!’);
};

// Выполнение функции!
sayHi();

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

// Вызов произойдет только при первом импорте. Остальные импорты кешируют результаты вызова.
import User from ‘./User’; // Boom!

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

A file SHOULD declare new symbols (classes, functions, constants, etc.) and cause no other side effects, or it SHOULD execute logic with side effects, but SHOULD NOT do both.

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

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

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

Чтобы быть до конца справедливым, иногда это оправдано.

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

// Вызов вне функций, на уровне файла
const element = document.querySelector(‘div’);

то такой файл будет невозможно загрузить в среду, где отсутствует document. И подмену тоже никак не сделать, так как модуль – это не функция, он не позволяет реализовать инверсию зависимостей.

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

Скрипт

Что такое скрипт? Скрипт – это любой файл, который предназначен для запуска из командной строки. Он может быть исполняемым, но в общем случае это не обязательно:

// date.js
console.log(new Date());

Этот скрипт содержит вызов функции, которая печатает на экран текущую дату:

$ node ./date.js
2019-09-17T17:04:10.267Z

Сам скрипт запускается не напрямую, а через интерпретатор. Такой подход часто используется во время разработки.

Когда скрипт “выпускается” для использования другими людьми, то наличие интерпретатора скрывают, а сам скрипт делают исполняемым. При этом из названия файла удаляют расширение. Это связано с тем, что для пользователей этого скрипта становится неважно, на каком языке он написан. Запуск такого скрипта выглядит так:

$ ./date
2019-09-17T17:04:10.267Z

Чтобы сделать наш файл date исполняемым, нужно выполнить две задачи:

  1. Добавить права на исполнение: chmod +x date.
  2. Добавить шебанг в начало файла:
#!/usr/bin/env node
  1. Такая запись внутри исполняемых файлов (скриптов) помогает командному интерпретатору, например, bash, подобрать правильный интерпретатор для запуска файла на исполнение.

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

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

Что насчет тестирования? Из-за своей природы скрипты крайне плохо тестируются. С ними нельзя работать, как с обычным кодом. В тестах придётся запускать их как обычную программу и смотреть, что она делает через, например, анализ STDOUT.

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

Библиотеки и утилиты командной строки

Такие пакеты, как eslint или babel, могут использоваться в двух режимах:

  1. Как программы, которые можно запускать из командной строки.
  2. Как библиотеки, которые можно поставить в качестве зависимостей, импортировать в код и вызывать.

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

Это разделение резко упрощает тестирование. Так как логика сосредоточена внутри библиотеки, то с ней можно работать, как с обычным кодом.

В свою очередь скрипт может иметь отдельную логику, которая не имеет отношения к библиотеке. К такой логике, например, относится парсинг аргументов командной строки: eslint —formatter json src. Эта часть приложения существует только тогда, когда с пакетом работают как с утилитой. Парсинг можно размещать напрямую в скриптах, либо выносить в отдельный модуль, который не связан напрямую с используемой библиотекой.

источник