Организация кода в Rust

Перевод | Автор оригинала: Pascal Hertleif

Примечание: этот пост о том, как я оформляю код, который пишу на Rust. Если вы хотели «заказать» код Rust в смысле «нанять кого-нибудь для написания кода», вы все равно должны продолжать читать, поскольку это отличный материал для собеседования. (Не то мнение, которое я представляю, а то, что у меня есть мнение по теме.)

Расположите код в предложенном порядке чтения

Я стараюсь упорядочить функции / модули / элементы в моих исходных файлах от наиболее высокоуровневых до наиболее конкретных / небольших. Сначала вы можете назвать это fn main.

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

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

Это не обязательно тот порядок, в котором я пишу код

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

Некоторые особенности кода Rust

Порядок элементов в Rust обычно не имеет значения (макросы - это странный крайний случай). Однако есть несколько вещей, которые нужно решить:

Как упорядочить определения типов (структуры, перечисления) и их реализации?

Есть два очевидных варианта:

Лично меня устраивают оба варианта, и я предпочитаю последнее. Однако я мог бы разделить блоки impl и определить некоторые методы (особенно частные) рядом с функциями, для которых они необходимы. (Иногда это похоже на особые, единичные особенности.)

У @matklad был еще один интересный комментарий:

[Я] люблю заранее читать типы (если вы знаете набор полей, вы знаете все возможные методы)

Где использовать инструкции?

Чтобы использовать элемент (тип, трэйта, функция и т.д.), Который не входит в область действия, вы можете либо сослаться на него по его полному пути (например, std::collections::HashMap), либо импортировать его с помощью use std::collections: : HashMap; после чего вы можете называть его просто HashMap. Вопрос в том, где разместить эти инструкции по использованию?

Один из типичных подходов - разместить их все в верхней части файла. Эту «стену импорта» вы также видите во многих других языках программирования. Это хорошая идея, если единственное место, где вы можете разместить операторы импорта, находится на корневом уровне, и особенно если файл содержит один «главный элемент» (например, если foo.java содержит операторы импорта, за которыми следует класс Foo {…}). Однако в Rust у вас не часто есть только один элемент на корневом уровне. У вас есть файл foo.rs, содержащий структуру Foo {…}, различные блоки impl Bar для Foo {…}, возможно, некоторые бесплатные функции и во многих случаях даже модульные тесты. Итак, мы должны переосмыслить, где разместить эти строки использования!

Один из подходов, которые я использовал ранее, - использовать как можно ближе к области, в которой они необходимы. Если у меня есть функция, которая читает пять файлов, я добавляю use std::fs::File; в начале этой функции. К сожалению, это не работает, когда вы хотите импортировать тип для использования его в сигнатуре функции / метода или в качестве типа поля в структуре: в этом случае использование use должно быть на уровне выше точки использования (т. Е. на уровне определения функции / трэйта / структуры). Кроме того, если вы использовали std::sync::Arc над одной структурой, она становится доступной в общей области. Итак, ваша следующая структура, использующая Arc, не нуждается во втором экземпляре этой строки использования.

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

Разделение общедоступного и частного интерфейсов

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

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

Абстракции поверх абстракций

Часто вы пишете структуры, которые используются только для внутреннего использования, но затем преобразуются в другие структуры для потребителей вашего пакета / модуля. У меня нет хорошего рецепта, как с этим справиться, за исключением того, что я бы порекомендовал попытаться сделать часть шаблона / преобразования «очевидной» / невидимой и таким образом выделить отличительные детали.

  1. «Пакет или модуль»? Да, а также «приложение» и «функция»: это фрактальное свойство.

Спасибо за чтение.