Базовый CS с нуля
Модули
Один объект связывает одну часть состояния с операциями над ней. Но реальная программа имеет десятки объектов, сотни функций и много вспомогательных значений. Если бы все они лежали в одном плоском пространстве, каждое имя было бы видно каждому другому имени. Любая функция могла бы прочитать или перезаписать любое значение. Опечатка в одном углу могла бы тихо сломать код в дальнем углу.
Программе такого размера нужна более крупная единица, чем объект, — способ провести линию вокруг группы связанного кода и сказать: внутри этой линии — одна вещь; снаружи можно пользоваться только тем, что я решу показать.
Эта линия и код, который она огораживает, — это модуль. Этот урок определяет, что такое модуль, что делает его граница и как он решает, какие из его имён остальной программе разрешено видеть.
После этого урока ты сможешь определить модуль как группу связанного кода за границей, объяснить, что такое экспорт и как он образует публичный интерфейс модуля, описать, что граница предотвращает, и объяснить, почему группировка имён под модулем не даёт им сталкиваться.
Модуль: связанный код за границей. Модуль — это единица кода, которая группирует связанные определения — функции, объекты, значения — и проводит вокруг них границу.
Граница — это не физическая вещь. Это правило, которое язык обеспечивает: код по одну сторону границы не может свободно видеть имена по другую сторону. По умолчанию каждое имя, определённое внутри модуля, приватно для этого модуля — видно только другому коду внутри того же модуля.
Это расширяет идею, которую ты уже знаешь. В уроке 04 раздела 08 ты видел область видимости: переменная, объявленная внутри функции, видна только внутри этой функции. Модуль применяет тот же принцип на уровень выше — имя, определённое внутри модуля, по умолчанию видно только внутри этого модуля. Функция давала границу вокруг нескольких переменных; модуль даёт границу вокруг целой группы определений.
Экспорт: публичный интерфейс модуля. Если бы всё в модуле было приватным, никакой другой код никогда не смог бы этим пользоваться. Модуль был бы наглухо запечатан и бесполезен.
Поэтому модуль может пометить некоторые из своих имён как экспорты. Экспорт — это имя, которое модуль явно решает сделать видимым для кода вне своей границы. Всё, что не помечено как экспорт, остаётся приватным.
Набор экспортированных имён — это публичный интерфейс модуля, та же идея, что и интерфейс из урока 01, теперь в масштабе целого модуля. Внешний код может пользоваться экспортированными именами и больше ничем. Приватные имена — вспомогательные функции, внутренние значения, недоделанные части — это скрытая реализация модуля. Поэтому модуль — это абстракция: граница с выбранным интерфейсом снаружи и скрытой реализацией внутри.
Что предотвращает граница. Граница делает настоящую защитную работу. Поскольку внешний код может достичь модуля только через его экспорты, два сбоя становятся невозможными.
Во-первых, внешний код не может тронуть приватное состояние. Внутреннее значение модуля нельзя прочитать или перезаписать снаружи. Какой бы инвариант ни поддерживал модуль — счётчик, который должен оставаться неотрицательным, кеш, который должен оставаться согласованным — он в безопасности, потому что единственный код, который может его изменить, — это собственный код модуля.
Во-вторых, изменение приватного имени не может сломать внешний код. Если имя приватно, ничто снаружи от него не зависит, поэтому автор модуля может переименовать его, удалить или переписать свободно. Только экспортированные имена — это обязательство. Это снова правило из урока 01 — неизменный интерфейс, заменяемая реализация — но именно граница обеспечивает его: язык делает невозможным для внешнего кода случайно зависеть от приватной детали.
Пространства имён: почему имена перестают сталкиваться. Программе могут понадобиться
две функции, обе разумно названные format — одна форматирует даты, другая форматирует
валюту. В едином плоском пространстве два определения столкнулись бы: второе перезаписало
бы первое.
Модуль даёт каждому имени дом. format модуля дат и format модуля валют — это разные
имена, потому что каждое достигается через свой модуль: dateModule.format и
currencyModule.format. Внешний код достигает экспортов модуля через имя модуля —
именованную ручку — тем же синтаксисом точки, которым ты пользовался для полей объекта в
Блоке 09. Имя модуля впереди держит их раздельно. Это разделение — имена,
сгруппированные под модулем так, что одинаковые короткие имена не конфликтуют — называется
пространством имён.
Пространства имён — это причина, по которой большая программа не остаётся без хороших имён. Каждый модуль — это свежее пространство; имя, использованное внутри одного модуля, ничего не говорит об имени внутри другого. Без них каждое имя в программе соперничало бы с каждым другим именем, и большие программы было бы невозможно писать.
Почему это работает
Зачем обеспечивать границу, а не просто доверять программистам? Команда могла бы договориться по соглашению «не трогать внутренности другого модуля» — но соглашения забываются, и под давлением сроков кто-нибудь всегда залезает за границу. Обеспеченная граница снимает вопрос полностью: приватное имя просто не видно, поэтому никто и не может от него зависеть — ни случайно, ни намеренно. Автор модуля тогда свободен менять каждую приватную деталь, точно зная, что ничто снаружи на неё не опирается. Граница превращает надежду в гарантию.
Чтение небольшого модуля и поиск его интерфейса.
Вот модуль с четырьмя определениями:
// модуль: text-format
const TABLE = { ... }; // таблица поиска
function pad(s: string): string { ... } // вспомогательная
function trim(s: string): string { ... } // вспомогательная
export function format(s: string): string {
return pad(trim(s)); // использует обе вспомогательные
}Какие имена публичны? Ищи ключевое слово export. Оно есть только у format. Значит,
публичный интерфейс модуля — ровно одно имя: format.
Какие имена приватны? Всё без export: TABLE, pad и trim — три приватных имени.
Внешний код не может назвать ни одно из них.
Может ли внешний код вызвать pad? Нет. pad приватна. Код в другом модуле, который
написал бы textFormat.pad("x"), упал бы — имя не экспортировано, поэтому оно не видно
за границей.
Может ли автор переименовать TABLE в LOOKUP? Да, безопасно. TABLE приватна;
ничто вне модуля не может на неё сослаться, поэтому переименование не может сломать
никакой другой код. Только format — обязательство перед внешним миром.
Вывод: модуль предоставил одно тщательно выбранное имя и скрыл три. Пользователь
модуля думает о format и больше ни о чём — ровно то сокращение, ради которого
существует абстракция, теперь применённое к целой группе кода.
Частая ошибка
Распространённая ошибка — думать, что приватное имя не выполняется. Оно прекрасно
выполняется. В примере pad и trim исполняются каждый раз, когда вызывается format,
— они делают настоящую работу. «Приватный» не значит бездействующий; это значит не
называемый снаружи модуля. Различие в видимости, а не в том, выполняется ли код.
Модуль определяет 6 имён. Он помечает 2 из них как экспорты. Сколько из его имён приватны?
Модуль text-format экспортирует только format. Сколько из его имён внешний код может вызвать напрямую?
Вспомогательная функция модуля приватна. Автор модуля переименовывает эту вспомогательную функцию. Сколько строк внешнего кода должно измениться из-за переименования?
В модуле A есть функция с именем format. В модуле B тоже есть функция с именем format. Достигаемые как A.format и B.format, сталкиваются ли эти два имени? Введи 1 за «да», 0 за «нет».
Модуль держит приватный счётчик, который никогда не должен опускаться ниже 0. Сколько мест во всей программе могут изменить значение этого счётчика?
Что такое модуль и что делает его граница?
Модуль — это единица кода, которая группирует связанные определения за границей.
По умолчанию каждое имя внутри модуля приватно — видно только другому коду в том же
модуле, так же как переменная ограничена своей функцией. Модуль помечает выбранные имена
как экспорты; набор экспортированных имён — это его публичный интерфейс, а всё
остальное — скрытая реализация. Граница делает настоящую работу: внешний код не может
прочитать или перезаписать приватное состояние модуля, а изменение приватного имени не
может сломать никакой код снаружи, поэтому автор может свободно переписывать внутренности.
Группировка имён под модулем также даёт каждому имени дом — пространство имён — так
что два модуля могут оба использовать короткое имя вроде format, не сталкиваясь. Модуль
— это абстракция, применённая к целой группе кода: выбранный интерфейс снаружи, защищённая
реализация внутри.