Профессиональные практики: Bundler, гемы, оптимизация, архитектура
Вы уже умеете писать Ruby-код, работать с коллекциями и блоками, проектировать классы и модули, обрабатывать исключения и писать тесты с RSpec, а также поддерживать стиль с RuboCop. Следующий шаг к профессиональному уровню — научиться собирать проекты так, чтобы они были:
воспроизводимыми на любой машине
управляемыми по зависимостям
измеримо быстрыми
архитектурно понятными и тестируемымиЭта статья связывает практики в единое целое: Bundler и гемы для зависимостей, базовые подходы к оптимизации производительности и рабочие приёмы архитектуры в Ruby-проектах.
Полезные источники:
Bundler
RubyGems
RubyGems.org
Semantic Versioning
Benchmark (Ruby stdlib)Bundler как стандарт управления зависимостями
Bundler решает практическую проблему: в разных окружениях должны устанавливаться одни и те же версии библиотек. Это особенно важно, когда проект запускается:
на компьютерах нескольких разработчиков
на CI
на сервереБазовые элементы: Gemfile и Gemfile.lock
В проектах с Bundler обычно есть два ключевых файла.
Gemfile описывает какие зависимости нужны (и какие версии допустимы).
Gemfile.lock фиксирует какие именно версии были выбраны и установлены.Практическое правило: Gemfile.lock нужно коммитить в репозиторий для приложений и большинства CLI-инструментов, чтобы окружение было воспроизводимым.
Типовой рабочий цикл
Инициализируйте проект:Добавьте зависимости в Gemfile, например:Установите зависимости:Запускайте команды через Bundler:Почему это важно: bundle exec гарантирует, что будет использован набор гемов из вашего Gemfile.lock, а не случайные версии, установленные в системе.
!Как Gemfile и Gemfile.lock превращаются в воспроизводимое окружение
Версионирование зависимостей и оператор ~>
Ruby-проекты обычно используют идеи семантического версионирования: версия выглядит как MAJOR.MINOR.PATCH.
MAJOR — потенциально несовместимые изменения
MINOR — новая функциональность без поломок совместимости
PATCH — исправления без изменения публичного поведенияОператор ~> в Gemfile называется пессимистичным ограничением.
Примеры:
Это означает: разрешены версии 3.13.x, 3.14.x, и так далее, но не 4.0.
Это означает: разрешены только 2.2.8, 2.2.9, 2.2.10 и так далее, но не 2.3.0.
Выбор ограничения — это баланс:
слишком жёстко зафиксировали версии — сложно обновляться
слишком свободно — можно неожиданно получить несовместимостьГруппы зависимостей
Bundler позволяет разделять зависимости по группам.
Типичные группы:
development — инструменты разработчика
test — тестовые зависимости
production — зависимости для запуска на сервереПример:
На сервере часто ставят только нужные группы, чтобы не тянуть лишние инструменты.
Обновления: аккуратно и предсказуемо
Bundler даёт две важные команды.
bundle update обновит зависимости (в рамках ограничений в Gemfile) и перепишет Gemfile.lock.
bundle update some_gem обновит только конкретный гем и его дерево зависимостей.Профессиональная привычка: обновлять зависимости небольшими шагами и запускать тесты после каждого обновления.
Binstubs: удобные исполняемые файлы
Чтобы не писать каждый раз bundle exec ..., можно создать binstubs.
Например:
После этого в проекте появятся файлы в bin/, и можно запускать:
Эти команды автоматически используют окружение проекта.
Гемы как единицы переиспользования
Gem — это упакованная библиотека или инструмент.
Что важно на практике:
выбирайте гемы с живой поддержкой и понятной документацией
фиксируйте версии через Bundler
минимизируйте число зависимостей, особенно для маленьких утилитStandard library и гемы
В Ruby есть стандартная библиотека, но многие её части подключаются явно.
Пример:
Это не гемы вашего приложения, но они всё равно являются зависимостями на уровне Ruby-окружения.
Как правильно подключать гем
Если гем нужен как библиотека, обычно достаточно require.
Пример:
Если гем нужен как CLI-инструмент, его обычно запускают командой (через bundle exec или bin/...), а require в коде может быть не нужен. Поэтому часто встречается:
Оптимизация: измеряйте, затем улучшайте
Производительность — это не только скорость, но и память, и стабильность времени выполнения.
Профессиональный подход строится так:
сначала измеряем текущую ситуацию
затем улучшаем узкие места
затем повторяем измерениеМини-замеры через Benchmark
В Ruby есть модуль Benchmark.
Пример сравнения двух подходов:
Что вы получаете:
время выполнения каждого сценария
возможность принять решение на фактах, а не по ощущениямВажно: один прогон может быть шумным. Если результат сомнительный, повторяйте замер несколько раз.
Типовые источники замедлений в Ruby-коде
лишние аллокации объектов (создание большого числа временных массивов и строк)
лишние проходы по коллекциям
неоптимальные структуры данных для задачи
смешивание I/O и вычислений, из-за чего сложно измерять и ускорятьПрактичные приёмы без фанатизма
#### Выбирайте правильный итератор
map создаёт новый массив
each подходит для побочных эффектов
select создаёт новый массив с отфильтрованными элементами
reduce сворачивает в одно значениеЕсли вам не нужен новый массив, map почти всегда будет лишним.
#### Делайте вычисления поточными для больших данных
Если данные большие, цепочки select.map.select.map могут создавать много временных массивов. Иногда лучше:
использовать lazy
или объединять шаги в один проходПример с lazy:
#### Уменьшайте число временных строк
Пример: при накоплении строки часто выгоднее использовать << вместо постоянной конкатенации через +.
#### Мемоизация для дорогих вычислений
Если метод дорогой и результат для объекта не меняется, используйте мемоизацию:
Смысл: вычисление выполнится один раз, затем будет использоваться кэш.
Архитектура: как сделать код понятным и тестируемым
Под архитектурой в рамках Ruby-проекта мы будем понимать практики, которые помогают:
отделять бизнес-логику от ввода-вывода
контролировать зависимости
поддерживать проект по мере ростаРазделение ответственности: логика отдельно, I/O отдельно
Из статьи про исключения и I/O вы уже знаете, что внешний мир нестабилен. Поэтому полезно разделять:
слой вычислений и правил
слой чтения файлов, аргументов CLI, печати, сетиЭто сразу помогает тестированию:
бизнес-логику легко тестировать без файлов
I/O можно тестировать точечно или подменятьПример структуры небольшого проекта
Один из рабочих вариантов структуры для учебного CLI-проекта:
bin/ — точка входа (CLI)
lib/ — классы и модули приложения
spec/ — RSpec-тестыПример:
bin/my_app может быть тонким:
А основная логика живёт в lib/ и тестируется отдельно.
Внедрение зависимостей через initialize
Чтобы код был гибким и тестируемым, зависимости лучше передавать в объект, а не создавать внутри.
Пример: класс, которому нужен объект для чтения данных.
В тесте вы сможете передать поддельный reader, не трогая файловую систему.
Явные границы ошибок
Из статьи про исключения вы знаете приём с доменными ошибками. Он хорошо сочетается с архитектурой:
слой I/O ловит системные ошибки (Errno::...)
слой доменной логики поднимает понятные ошибки предметной областиЭто делает поведение предсказуемым и тестируемым.
Минимальная многослойность для небольших проектов
Для большинства задач курса достаточно трёх уровней:
domain — правила и вычисления
application — оркестрация сценария, вызывает domain
infrastructure — файлы, сеть, внешние зависимости!Простая модель слоёв и направления зависимостей
Как всё связать с тестированием и стилем
Из предыдущей статьи про RSpec и RuboCop вы уже знаете базовый рабочий процесс. На практике он хорошо дополняется тем, что разобрано здесь:
Bundler фиксирует зависимости и делает запуск команд воспроизводимым
архитектура отделяет логику от I/O, и тесты становятся стабильнее
оптимизация начинается с измерений, а не с догадокМинимальный набор команд, который полезно запускать регулярно:
Если проект структурирован и зависимости управляются корректно, вы получаете профессиональную базу: проект проще поддерживать, проще проверять, проще развивать.