Когда вы пишете библиотеку, которая в дальнейшем будет задействована во множестве других проектов, крайне важно продумать, как именно разработчики будут работать с ней своем коде.
Один из лучших способов позаботиться о том, чтобы работа с вашей библиотекой не обернулась для пользователей кошмаром, — это поставить себя на их место. На мгновение забудьте о всех внутренних тонкостях пакета и сконцентрируйтесь только на его внешнем интерфейсе. Затем придумайте какой-нибудь реалистичный юзкейс и просто реализуйте его.
Другими словами, вы должны создать законченный, универсальный и (в определенной степени) пригодный для использования пример приложения.
Примеры — это хлопоты
Вам может показаться, что от вас требуют слишком много, и я не стану с вами спорить.
Для большинства платформ и языков программирования создание рабочих примеров приложений — действительно довольно муторная задача. И причин для этого может быть сразу несколько:
Обычно это предполагает бутстрэппинг всего проекта с нуля. Если вам повезет, то в вашем распоряжении будет что-то вроде create-react-app, который сделает вашу задачу немного легче. Но вам все еще нужно собрать ваш новый проект так, чтобы он зависел от исходного кода вашей библиотеки, а не ее опубликованной версии, а это, как правило, не совсем стандартная опция — если это вообще возможно.
Не совсем понятно, где пример кода должен храниться. Разве от него не нужно будет избавиться, как только он послужит своей непосредственной цели? Я уверен, что этот вопрос во всей своей полноте способен отпугнуть многих людей от создания примеров. Очевидно, что лучшим решением было бы разместить их в системе контроля версий, потому что это позволит их коду служить в качестве дополнительной документации.
Если вы все-таки решите так сделать, то вам нужно будет соблюдать дополнительную осторожность, чтобы ваш пример не просочился вслед за вашей библиотекой, когда вы загружаете ее в реестр пакетов целевого языка. С этим может помочь ведение явных блэклистов и/или вайтлистов наподобие MANIFEST файлов в Python.Примеры могут ломаться в результате изменения библиотеки. Хоть примеры приложений и не являются интеграционными тестами с четким ожидаемым результатом, они как минимум должны компилироваться.
Единственный способ гарантировать это — включить их в конвейер сборки/тестирования вашей библиотеки. Однако для реализации этого вам может потребоваться усложнить CI сетап, например, добавив дополнительные языки, такие как Bash или Python.Поддерживать качество кода примеров немного сложнее. Любые линтеры и статические анализаторы, которые вы обычно используете, скорее всего, потребуется дополнительно настраивать, чтобы они также применялись к примерам. Однако, с другой стороны, вы, вероятно, не хотели бы, чтобы эти валидаторы были слишком строги по отношению к этому коду (в конце концов, это всего лишь код примера), поэтому вам может потребоваться отключить одни предупреждения, подкрутить уровень других и так далее.
Таким образом, написание примеров сопряжено с большими хлопотами. Было бы здорово, если бы дефолтный инструментарий вашего языка мог бы хоть немного помочь вам в этом деле.
Что ж, у меня для вас хорошие новости! Если вы программируете на Rust, то вам повезло с языком — ему есть чем похвастаться, когда речь идет о помощи в написании примеров.
Cargo (стандартный инструмент сборки и менеджер пакетов для Rust) имеет ряд фич, предназначенных специально для поддержки примеров как концепции первого класса. Хотя он не полностью избавляет нас от всей головной боли, описанной выше, он в значительной степени ее минимизирует.
Что из себя представляют Cargo-примеры?
Выражаясь в терминах Cargo, пример (example) это не что иное, как исходный Rust-код отдельного исполняемого файла1, который обычно заключен в одном .rs-файле. Все эти файлы должны находиться в папке examples/
на том же уровне, что и src/
с Cargo.toml-манифестом2.
Вот простейший пример… эмм… примера:
// examples/hello.rs
fn main() {
println!("Hello from an example!");
}
Вы можете запустить его с помощью простейшей команды cargo run
. Просто укажите имя примера после флага --example
:
$ cargo run --example hello
Hello from an example!
Также у вас есть возможность запустить пример с некоторыми дополнительными аргументами:
$ cargo run --example hello2 -- Alice
Hello, Alice!
которые передаются непосредственно в сам двоичный файл:
// examples/hello2.rs
use std::env;
fn main() {
let name = env::args().skip(1).next();
println!("Hello, {}!", name.unwrap_or("world".into()));
}
Как видите, то, как мы запускаем примеры, очень похоже на то, как мы запускаем двоичные файлы из src/bin, которые некоторые программисты используют в качестве основных точек входа в свои Rust-программы.
Важно то, что вам больше не нужно беспокоиться о том, что же вам делать с кодом вашего примера. Все, что вам нужно сделать, это закинуть его в папку examples/
, и пусть Cargo сделает все остальное.
Зависимость включена
Конечно в реальности ваши примеры будут как минимум немного сложнее, чем этот. Во-первых, они наверняка будут обращаться к вашей библиотеке, чтобы использовать ее API, а это значит, что они должны зависеть от нее и импортировать ее символы.
К счастью, это ничуть не усложняет ситуацию.
Сам крейт библиотеки уже является неявной зависимостью любого кода внутри папки examples/. Об этом позаботиться сам Cargo, поэтому вам не нужно изменять Cargo.toml
(или делать что-нибудь еще), чтобы реализовать это.
Таким образом, без каких-либо дополнительных усилий вы можете очень быстро подключить крейт своей библиотек, просто добавив extern crate
в шапку Rust-файла:
// examples/real.rs
extern crate mylib;
fn main() {
let thing = mylib::make_a_thing();
println!("I made a thing: {:?}", thing);
}
Эта фича не ограничивается на этом и распространяется на любую зависимость самой библиотеки. Все необходимые сторонние крейты автоматически доступны для кода примера, что является невероятно удобным для таких распространенных случаев, как асинхронные API на основе Tokio:
// example/async.rs
extern crate mylib;
extern crate tokio_core; // предполагается, что он находится в [dependencies] mylib
fn main() {
let mut core = tokio_core::reactor::Core::new().unwrap();
let thing = core.run(mylib::make_a_thing_asynchronously()).unwrap();
println!("I made a thing: {:?}", thing);
}
Больше зависимостей
Однако иногда в демонстрационных целях было бы неплохо добавить в код примера один или два дополнительных пакета.
Хорошим примером может послужить логирование.
Если для вывода отладочных сообщений в вашей библиотеке используется стандартный крейт log, вы, вероятно, хотели бы, чтобы они выводились и при запуске ваших примеров. Поскольку крейт log это чистый фасад, он не предлагает никакого встроенного способа передачи логовых сообщений в стандартный вывод. Чтобы восполнить эту часть, вам нужно что-то вроде пакета env_logger:
// example/with_logging.rs
extern crate env_logger;
extern crate mylib;
fn main() {
env_logger::init();
println("{:?}", mylib::make_a_thing());
}
Чтобы иметь возможность импортировать env_logger
таким образом, он, естественно, должен быть объявлен как зависимость в нашем Cargo.toml
.
Однако мы не будем помещать его в раздел [dependencies]
нашего манифеста, поскольку он не нужен коду библиотеки. Вместо этого мы должны поместить его в отдельный раздел, который называется [dev-dependencies]
:
[dev-dependencies]
env_logger = "0.5"
Пакеты, перечисленные там, используются в тестах, бенчмарках и примерах. Однако они не связаны с обычными сборками вашей библиотеки, поэтому вам не нужно беспокоиться о том, что она будет раздуваться за счет ненужного кода.
По мере роста примера
До сих пор мы рассматривали примеры, охватывающие только один файл Rust. Приложения, встречающиеся на практике, как правило, больше, поэтому было бы неплохо, если бы мы также взглянули на несколько примеров с несколькими файлами.
Сделать это несложно, хотя почему-то об этом не упоминается в официальной документации.
В любом случае подход идентичен работе с исполняемыми файлами из src/bin/. В принципе, если у нас есть один файл foo.rs с исполняемым кодом, мы можем расширить его до подпапки foo/
с как точкой входа foo/main.rs
. Затем мы можем добавить любые другие подмодули, какие захотим — точно так же, как мы сделали бы это для самого обычного бинарного крейта Rust:
// examples/multifile/main.rs
extern crate env_logger;
extern crate mylib;
mod util;
fn main() {
env_logger::init();
let ingredient = util::create_ingredient();
let thing = mylib::make_a_thing_with(ingredient);
println("{:?}", thing);
}
// examples/multifile/util.rs
pub fn create_ingredient() -> u64 {
42
}
Конечно, нам далеко не всегда нужны большие примеры. Тем не менее демонстрация того, как библиотека может масштабироваться для более крупных приложений, может оказаться очень воодушевляющим элементом для потенциальных пользователей.
Поддержка поддерживаемости
Мы с вами обсудили, как создавать маленькие и большие примеры, как использовать дополнительные сторонние крейты в примерах программ и как легко создавать и запускать их с помощью встроенных команд Cargo.
Но все эти усилия, направленные на написание примеров, были бы бесполезны, если бы мы не смогли гарантировать, что они будут работать.
Как и любой тип кода, примеры подвержены риску потери работоспособности при изменении базового API. Если библиотека активно развивается, ее интерфейс будет постоянно изменяться. И вполне закономерно, что изменения могут иногда приводить к тому, что старые примеры перестанут компилироваться.
К счастью, Cargo очень четко отслеживает такие неполадки. Всякий раз, когда вы запускаете:
$ cargo test
все примеры собираются в рамках выполнения вашего стандартного набора тестов3. Вы получаете гарантию компиляции ваших примеров совершенно бесплатно — вам даже не нужно редактировать ваш .travis.yml
или настраивать непрерывную интеграцию любым другим способом!
Довольно круто, не правда ли?
При этом вы должны иметь в виду, что просто компиляция ваших примеров на регулярной основе не является надежной гарантией того, что их код никогда не устареет. Примеры — не интеграционные тесты, и они не будут обнаруживать важные изменения в вашей реализации, которые не нарушают интерфейс.
Разработка через примеры?
После всего сказанного выше вы можете задаться мыслью, а в чем вообще смысл писать примеры? Если у вас есть тесты, с одной стороны, для проверки правильности, а с другой стороны, документация для информирования ваших пользователей, то набор специальных исполняемых примеров может показаться излишним.
Для меня, однако, исчерпывающий набор тестов и подробная документация, которые актуализируются на протяжении всего жизненного цикла библиотеки, — это стандарт, к которому следует стремиться :) Добавление примеров почти всегда идет на пользу библиотеки, а усилия по их обслуживанию в большинстве случаев оказываются минимальными.
Я также обнаружил, что начать создавать примеры на раннем этапе — это отличный способ проверить дизайн интерфейса.
Как только создание небольших тестовых программ перестает быть чем-то обременительным, они становятся незаменимыми для создания прототипов новых функций. Хотите попробовать новую фичу, которую вы только что добавили? Просто напишите для нее быстрый пример, запустите его и посмотрите, как это будет выглядеть!
Во многих отношениях это похоже на возможность протестировать что-то в REPL — что-то, что почти является эксклюзивом динамических/интерпретируемых языков. Но в отличие от возни в оболочке Python, примеры это не одноразовый код: они становятся частью вашего проекта и остаются полезными как для вас, так и для ваших пользователей.
Также возможно создание примеров, которые сами по себе являются библиотеками. Я не думаю, что это особенно полезно, поскольку все, что вы можете делать с такими примерами, — это создавать их, поэтому они не привносят никакой дополнительной ценности в сравнению с обычными тестами (и особенно доктестами).↩
Так как они вне папки src/, примеры не становятся частью кода вашей библиотеки и не развертываются в crates.io.↩
Вы также можете запустить cargo build --examples, что скомпилировать только примеры, без запуска каких-либо тестов.↩
Приглашаем всех желающих на открытое занятие «Rust и Blockchain». На этой уроке рассмотрим базовые понятия о blockchain, а также популярные библиотеки, разберём процесс написания blockchain, отработаем создание реализации blockchain и леджера на практике. Записаться на занятие можно на странице курса "Rust Developer. Professional".