Rust - это язык, который появился в мире недавно и сразу же стал самым любимым языком. Но почему? Почему она стала такой популярной и любимой?
Я начал играть с этим, и что-то сработало во мне, потому что моя ментальная модель подверглась испытанию, поэтому я начал глубоко изучать ее, и мой ответ на предыдущий вопрос - система типов. Я был разработчиком NodeJS, поэтому система типов меня никогда не привлекала, но Rust меня очень привлекает.
В частности, мое внимание привлекла система перечисления: она очень интересна и работает не так, как на других языках.
Перечисления
Перечисление обычно определяется как набор предопределенных элементов, называемых вариантами. Во многих языках перечисления представлены в виде целого числа: первый элемент обозначается 0, второй - 1 и так далее…
// C example
enum Direction {
UP, // = 0
LEFT, // = 1
RIGTH, // = 2
DOWN// = 3
};
В Rust вы также можете указать простое перечисление следующим образом:
enum Direction {
Up,
Left,
Rigth,
Down,
};
Комментарии удалены, потому что Rust абстрагирует перечисления, позволяя нам ссылаться на вариант только через его название, не используя индекс.
Еще одно отличие, на самом деле главное, заключается в том, что Rust позволяет вам присоединять типы к вариантам. Но не только, вы можете присоединить другой тип к другому варианту перечисления.
Этот функционал открывает моему сознанию новый мир.
Моделирование вашего типа данных становится намного проще. Но давайте попробуем привести несколько примеров.
struct GameState { /* ... */ }
struct GameOverReason { /* ... */ }
enum GameStatus {
NotStarted,
Playing(GameState),
Paused(GameState),
GameOver(GameOverReason)
};
let not_started = GameStatus::NotStarted;
let playing = GameStatus::Playing(GameState { /* ... */ } );
let paused = GameStatus::Paused(GameState { /* ... */ } );
let game_over = GameStatus::GameOver(GameOverReason { /* ... */ });
// The following line throws a compile error !!
let not_possible = GameStatus::GameOver(GameState{ /* ... */ });
В приведенном выше примере GameStatus - это перечисление, которое содержит различные типы в зависимости от того, какой вариант предполагала переменная.
Используйте перечисления в DDD
Как было сказано ранее, перечисление описывает исключительное условие для собственных вариантов: экземпляр enum
не может идентифицировать 2 различных варианта одновременно.
Это свойство в сочетании с возможностью хранения данных позволяет вашему коду быть более понятным, избегая невозможных состояний.
Давайте приведем несколько примеров
Система тикетов
Мы хотим создать систему продажи билетов для поддержки обслуживания клиентов: клиент хочет знать, почему что-то пошло не так с продуктом. Для этого клиент создает билет, вставляя некоторые данные (тексты, изображения и так далее). Наш бизнес требует, чтобы билет был работоспособен только в том случае, если служба поддержки клиентов примет его. В противном случае заявка отклоняется. В конце клиент закрывает билет.
В этой ситуации в заявке следует рассмотреть:
- Невозможно создать тикет без исходных данных клиента.
- После открытия только служба поддержки клиентов может принять решение о принятии или отклонении билета.
- Если заявка принята, служба поддержки клиентов должна предоставить в ответ некоторые данные (тексты, изображения, ссылку на документацию).
Учитывая эти ограничения, возможным решением могло бы быть следующее:
enum TicketState {
Open,
Taken,
Declined,
Closed,
}
struct CustomerInitialInformation { /* ... */ }
struct CustomerCareTakenInformation { /* ... */ }
struct DeclineReason { /* ... */ }
struct CloseInformation { /* ... */ }
struct Ticket {
// Отслеживание состояния билета
state: TicketState,
// Отслеживать при открытии билета
open_date: Date,
// Хранит первоначальный запрос клиента
customer_intial_information: CustomerInitialInformation,
// Отслеживать, когда билет забирается группой по работе с клиентами
taken_date: Option<Date>,
// Хранит ответ от группы по работе с клиентами
customer_care_taken_information: Option<CustomerCareTakenInformation>,
// Отслеживать при отклонении билета
declined_date: Option<Date>,
// Хранит причину разграничения
decline_reason: Option<DeclineReason>,
// Отслеживать дату закрытия
close_date: Option<Date>,
// Хранит закрытую информацию
close_information: Option<CloseInformation>,
}
// Мы можем написать что-то вроде:
let ticket1 = {
taken_date: Some(Date::now()),
customer_care_taken_information: None,
// ...
};
let ticket2 = {
close_date: Some(Date::now()),
declined_date: Some(Date::now()),
// ...
};
ПРИМЕЧАНИЕ: В Rust нет null. Опция позволяет нам сохранить значение None как null, а Some - для хранения ненулевых данных.
Используя приведенную выше структуру, мы могли бы столкнуться с некоторыми незаконными состояниями: что произойдет, если у нас будет taken_date
без customer_care_taken_information
? И является ли допустимым состояние, если установлено close_date
, а также declined_date
? Наша структура допускает незаконные состояния.
Как мы можем это улучшить? Использование перечислений!
struct CustomerInitialInformation { /* ... */ }
struct CustomerCareTakenInformation { /* ... */ }
struct DeclineReason { /* ... */ }
struct CloseInformation { /* ... */ }
enum Ticket {
Open(CustomerInitialInformation),
Taken(CustomerInitialInformation, CustomerCareTakenInformation),
Declined(CustomerInitialInformation, DeclineReason),
Closed(CustomerInitialInformation, CustomerCareTakenInformation, CloseInformation),
}
Таким образом, код больше не содержит опции. И незаконные состояния невозможны: ваш домен безопаснее.
Корзина для покупок
Распространенной проблемой является корзина для покупок. Предположим, что электронной коммерции необходимо реализовать эту функцию в соответствии со следующими требованиями:
- Клиент создает корзину покупок, только добавляя товар.
- Клиент добавляет товар из корзины или повторно выбирает его.
- Клиент оформляет непустую корзину, оплачивая ее.
- Никакие изменения не могут быть внесены после оплаты.
В этой ситуации ваше приложение должно быть разработано таким образом, чтобы избежать несогласованных состояний.
Возможным дизайном может быть следующий код:
struct Item { /* ... */ }
enum ShoppingCartState {
Empty,
Full,
Paid,
}
struct PaymentInformation { /* ... */ }
struct ShoppingCart {
// Отслеживание состояния корзины
state: ShoppingCartState,
// Хранит позиции
items: Vec<Item>,
// Информация о платеже
payment_information: Option<PaymentInformation>,
}
// Но мы можем написать что-то вроде:
let cart1 = ShoppingCart {
items: Vec::new(),
payment_information: Some(PaymentInformation { /* ... */ }),
// ...
};
let cart2 = ShoppingCart {
state: ShoppingCartState::Full,
payment_information: Some(PaymentInformation { /* ... */ }),
// ...
};
В приведенном выше коде тип данных не обеспечивает несогласованное состояние: на самом деле, кто гарантирует, что массив items
не будет пустым при выполнении payment_information
? payment_information
может быть установлена также при заполнении состояния? Опять же, мы можем сделать это лучше.
Давайте вместо этого посмотрим на следующий код:
struct Item { /* ... */ }
struct PaymentInformation { /* ... */ }
struct FullCart { items: Vec<Item> }
struct PaidCart {
paid_items: Vec<Item>,
payment_information: PaymentInformation,
}
enum ShoppingCart {
Empty,
Full(FullCart),
Paid(PaidCart),
}
Теперь мы не можем держать корзину в нелегальном состоянии: наш домен безопасен, намного лучше!
Вывод
Rust имеет сильную систему типов. Возможности перечислений поразительны, позволяя вашему приложению учитывать только допустимое состояние. Вы пробовали это? Каков ваш опыт использования перечислений?