Язык программирования Rust
Скачать 7.02 Mb.
|
Определение Post и создание нового экземпляра в состоянии черновика Приступим к реализации библиотеки! Мы знаем, что нам нужна публичная структура Post , хранящая некоторое содержимое, поэтому мы начнём с определения структуры и use blog::Post; fn main () { let mut post = Post::new(); post.add_text( "I ate a salad for lunch today" ); assert_eq! ( "" , post.content()); post.request_review(); assert_eq! ( "" , post.content()); post.approve(); assert_eq! ( "I ate a salad for lunch today" , post.content()); } связанной с ней публичной функцией new для создания экземпляра Post , как показано в листинге 17-12. Мы также сделаем приватный типаж State , который будет определять поведение, которое должны будут иметь все объекты состояний структуры Post Затем Post будет содержать типаж-объект Box внутри Option в закрытом поле state для хранения объекта состояния. Чуть позже вы поймёте, зачем нужно использовать Option Файл: src/lib.rs Листинг 17-12. Определение структуры Post и функции new , которая создаёт новый экземпляр Post , типажа State и структуры Draft Типаж State определяет поведение, совместно используемое различными состояниями поста. Все объекты состояний ( Draft - "черновик", PendingReview - "ожидание проверки" и Published - "опубликовано") будут реализовывать типаж State . Пока у этого типажа нет никаких методов, и мы начнём с определения состояния Draft , просто потому, что это первое состояние, с которого, как мы хотим, публикация будет начинать свой путь. Когда мы создаём новый экземпляр Post , мы устанавливаем его поле state в значение Some , содержащее Box . Этот Box указывает на новый экземпляр структуры Draft . Это гарантирует, что всякий раз, когда мы создаём новый экземпляр Post , он появляется как черновик. Поскольку поле state в структуре Post является приватным, нет никакого способа создать Post в каком-либо другом состоянии! В функции Post::new мы инициализируем поле content новой пустой строкой типа String Хранение текста содержимого записи pub struct Post { state: Option < Box < dyn State>>, content: String , } impl Post { pub fn new () -> Post { Post { state: Some ( Box ::new(Draft {})), content: String ::new(), } } } trait State {} struct Draft {} impl State for Draft {} В листинге 17-11 показано, что мы хотим иметь возможность вызывать метод add_text и передать ему &str , которое добавляется к текстовому содержимому записи блога. Мы реализуем эту возможность как метод, а не делаем поле content публично доступным, используя pub . Это означает, что позже мы сможем написать метод, который будет контролировать, как именно читаются данные из поля content . Метод add_text довольно прост, поэтому давайте добавим его реализацию в блок impl Post листинга 17- 13 : Файл: src/lib.rs Листинг 17-13. Реализация add_text для добавления текста к content (содержимому записи) Метод add_text принимает изменяемую ссылку на self , потому что мы меняем экземпляр Post , для которого вызываем add_text . Затем мы вызываем push_str для String у поля content и передаём text аргументом для добавления к сохранённому content . Это поведение не зависит от состояния, в котором находится запись, таким образом оно не является частью шаблона "Состояние". Метод add_text вообще не взаимодействует с полем state , но это часть поведения, которое мы хотим поддерживать. Убедимся, что содержание черновика будет пустым Даже после того, как мы вызвали add_text и добавили некоторый контент в нашу запись, мы хотим, чтобы метод content возвращал пустой фрагмент строки, так как запись всё ещё находится в черновом состоянии, как это показано в строке 7 листинга 17-11. А пока давайте реализуем метод content наиболее простым способом, который будет удовлетворять этому требованию: будем всегда возвращать пустой фрагмент строки. Мы изменим код позже, как только реализуем возможность изменить состояние записи, чтобы она могла бы быть опубликована. Пока что записи могут находиться только в черновом состоянии, поэтому содержимое записи всегда должно быть пустым. Листинг 17-14 показывает такую реализацию-заглушку: Файл: src/lib.rs impl Post { // --snip-- pub fn add_text (& mut self , text: & str ) { self .content.push_str(text); } } Листинг 17-14. Добавление реализации-заглушки для метода content в Post , которая всегда возвращает пустой фрагмент строки. С добавленным таким образом методом content всё в листинге 17-11 работает, как задумано, вплоть до строки 7. Запрос на проверку записи меняет её состояние Далее нам нужно добавить функциональность для запроса проверки записи, который должен изменить её состояние с Draft на PendingReview . Листинг 17-15 показывает такой код: Файл: src/lib.rs Листинг 17-15. Реализация методов request_review в структуре Post и типаже State impl Post { // --snip-- pub fn content (& self ) -> & str { "" } } impl Post { // --snip-- pub fn request_review (& mut self ) { if let Some (s) = self .state.take() { self .state = Some (s.request_review()) } } } trait State { fn request_review ( self : Box < Self >) -> Box < dyn State>; } struct Draft {} impl State for Draft { fn request_review ( self : Box < Self >) -> Box < dyn State> { Box ::new(PendingReview {}) } } struct PendingReview {} impl State for PendingReview { fn request_review ( self : Box < Self >) -> Box < dyn State> { self } } Мы добавляем в Post публичный метод с именем request_review ("запросить проверку"), который будет принимать изменяемую ссылку на self . Затем мы вызываем внутренний метод request_review для текущего состояния Post , и этот второй метод request_review поглощает текущее состояние и возвращает новое состояние. Мы добавляем метод request_review в типаж State ; все типы, реализующие этот типаж, теперь должны будут реализовать метод request_review . Обратите внимание, что вместо self , &self или &mut self в качестве первого параметра метода у нас указан self: Box . Этот синтаксис означает, что метод действителен только при его вызове с обёрткой Box , содержащей наш тип. Этот синтаксис становится владельцем Box , делая старое состояние недействительным, поэтому значение состояния Post может быть преобразовано в новое состояние. Чтобы поглотить старое состояние, метод request_review должен стать владельцем значения состояния. Это место, где приходит на помощь тип Option поля state записи Post : мы вызываем метод take , чтобы забрать значение Some из поля state и оставить вместо него значение None , потому что Rust не позволяет иметь неинициализированные поля в структурах. Это позволяет перемещать значение state из Post , а не заимствовать его. Затем мы установим новое значение state как результат этой операции. Нам нужно временно установить state в None , вместо того, чтобы установить его напрямую с помощью кода вроде self.state = self.state.request_review(); . Нам нужно завладеть значением поля state . Это даст нам гарантию, что Post не сможет использовать старое значение state после того, как мы преобразовали его в новое состояние. Метод request_review в Draft должен вернуть новый экземпляр новой структуры PendingReview , обёрнутый в Box. Эта структура будет представлять состояние, в котором запись ожидает проверки. Структура PendingReview также реализует метод request_review , но не выполняет никаких преобразований. Она возвращает сама себя, потому что, когда мы запрашиваем проверку записи, уже находящейся в состоянии PendingReview , она всё так же должна продолжать оставаться в состоянии PendingReview Теперь мы начинаем видеть преимущества шаблона "Состояние": метод request_review для Post одинаков, он не зависит от значения state . Каждое состояние само несёт ответственность за свои действия. Оставим метод content у Post таким как есть, возвращающим пустой фрагмент строки. Теперь мы можем иметь Post как в состоянии PendingReview , так и в состоянии Draft , но мы хотим получить такое же поведение в состоянии PendingReview . Листинг 17-11 теперь работает до строки 10! Добавление approve для изменения поведения content Метод approve ("одобрить") будет аналогичен методу request_review : он будет устанавливать у state значение, которое должна иметь запись при её одобрении, как показано в листинге 17-16: Файл: src/lib.rs Листинг 17-16. Реализация метода approve для типа Post и типажа State impl Post { // --snip-- pub fn approve (& mut self ) { if let Some (s) = self .state.take() { self .state = Some (s.approve()) } } } trait State { fn request_review ( self : Box < Self >) -> Box < dyn State>; fn approve ( self : Box < Self >) -> Box < dyn State>; } struct Draft {} impl State for Draft { // --snip-- fn approve ( self : Box < Self >) -> Box < dyn State> { self } } struct PendingReview {} impl State for PendingReview { // --snip-- fn approve ( self : Box < Self >) -> Box < dyn State> { Box ::new(Published {}) } } struct Published {} impl State for Published { fn request_review ( self : Box < Self >) -> Box < dyn State> { self } fn approve ( self : Box < Self >) -> Box < dyn State> { self } } Мы добавляем метод approve в типаж State , добавляем новую структуру, которая реализует этот типаж State и структуру для состояния Published Подобно тому, как работает request_review для PendingReview , если мы вызовем метод approve для Draft , он не будет иметь никакого эффекта, потому что approve вернёт self . Когда мы вызываем для PendingReview метод approve , то он возвращает новый упакованный экземпляр структуры Published . Структура Published реализует трейт State , и как для метода request_review , так и для метода approve она возвращает себя, потому что в этих случаях запись должна оставаться в состоянии Published Теперь нам нужно обновить метод content для Post . Мы хотим, чтобы значение, возвращаемое из content , зависело от текущего состояния Post , поэтому мы собираемся перенести часть функциональности Post в метод content , заданный для state , как показано в листинге 17.17: Файл: src/lib.rs Листинг 17-17: Обновление метода content в структуре Post для делегирования части функциональности методу content структуры State Поскольку наша цель состоит в том, чтобы сохранить все эти действия внутри структур, реализующих типаж State , мы вызываем метод content у значения в поле state и передаём экземпляр публикации (то есть self ) в качестве аргумента. Затем мы возвращаем значение, которое нам выдаёт вызов метода content поля state Мы вызываем метод as_ref у Option , потому что нам нужна ссылка на значение внутри Option , а не владение значением. Поскольку state является типом Option , то при вызове метода as_ref возвращается Option<&Box . Если бы мы не вызывали as_ref , мы бы получили ошибку, потому что мы не можем переместить state из заимствованного параметра &self функции. Затем мы вызываем метод unwrap . Мы знаем, что этот метод здесь никогда не приведёт к аварийному завершению программы, так все методы Post устроены таким образом, что после их выполнения, в поле state всегда содержится значение Some . Это один из случаев, про которых мы говорили в разделе "Случаи, когда у вас больше информации, чем у компилятора" главы 9 - случай, когда мы знаем, что значение None никогда не встретится, даже если компилятор не может этого понять. impl Post { // --snip-- pub fn content (& self ) -> & str { self .state.as_ref().unwrap().content( self ) } // --snip-- } Теперь, когда мы вызываем content у типа &Box , в действие вступает принудительное приведение (deref coercion) для & и Box , поэтому в конечном итоге метод content будет вызван для типа, который реализует типаж State . Это означает, что нам нужно добавить метод content в определение типажа State , и именно там мы поместим логику для определения того, какое содержимое возвращать, в зависимости от текущего состояния, как показано в листинге 17-18: Файл: src/lib.rs Листинг 17-18. Добавление метода content в трейт State Мы добавляем реализацию по умолчанию метода content , который возвращает пустой фрагмент строки. Это означает, что нам не придётся реализовывать content в структурах Draft и PendingReview . Структура Published будет переопределять метод content и вернёт значение из post.content Обратите внимание, что для этого метода нам нужны аннотации времени жизни, как мы обсуждали в главе 10. Мы берём ссылку на post в качестве аргумента и возвращаем ссылку на часть этого post , поэтому время жизни возвращённой ссылки связано с временем жизни аргумента post И вот, мы закончили - теперь всё из листинга 17-11 работает! Мы реализовали шаблон "Состояние", определяющий правила процесса работы с записью в блоге. Логика, связанная с этими правилами, находится в объектах состояний, а не разбросана по всей структуре Post Почему не перечисление? Возможно, вам было интересно, почему мы не использовали enum с различными возможными состояниями записи в качестве вариантов. Это, безусловно, одно из возможных решений. Попробуйте его реализовать и сравните конечные trait State { // --snip-- fn content < 'a >(& self , post: & 'a Post) -> & 'a str { "" } } // --snip-- struct Published {} impl State for Published { // --snip-- fn content < 'a >(& self , post: & 'a Post) -> & 'a str { &post.content } } результаты, чтобы выбрать, какой из вариантов вам больше нравится! Одним из недостатков использования перечисления является то, что в каждом месте, где проверяется значение перечисления, потребуется выражение match или что-то подобное для обработки всех возможных вариантов. Возможно в этом случае нам придётся повторять больше кода, чем это было в решении с типаж-объектом. Компромиссы шаблона "Состояние" Мы показали, что Rust способен реализовать объектно-ориентированный шаблон "Состояние" для инкапсуляции различных типов поведения, которые должна иметь запись в каждом состоянии. Методы в Post ничего не знают о различных видах поведения. При такой организации кода, нам достаточно взглянуть только на один его участок, чтобы узнать отличия в поведении опубликованной публикации: в реализацию типажа State у структуры Published Если бы мы собирались создать альтернативную реализацию, не использующую шаблон "Состояние", мы могли бы использовать выражения match в методах структуры Post или даже в коде main , для проверки состояния записи и изменения её поведения в этих местах. Это означало бы, что нам пришлось бы анализировать несколько участков кода, чтобы понять что как ведёт себя сообщение в опубликованном состоянии! Если бы мы решили добавить ещё состояний, стало бы ещё хуже: каждому этих выражений match потребовались бы дополнительные ответвления. С помощью шаблона "Состояние" методы Post и участки, где мы используем Post , не требуют использования выражений match , а для добавления нового состояния нужно только добавить новую структуру и реализовать методы типажа у одной этой структуры. Реализацию с использованием шаблона "Состояние" легко расширить для добавления новой функциональности. Чтобы увидеть, как легко поддерживать код, использующий данный шаблон, попробуйте выполнить некоторые из предложений ниже: Добавьте метод reject , который изменяет состояние публикации с PendingReview обратно на Draft Потребуйте два вызова метода approve , прежде чем переводить состояние в Published Разрешите пользователям добавлять текстовое содержимое только тогда, когда публикация находится в состоянии Draft . Подсказка: пусть объект состояния решает, можно ли менять содержимое, но не отвечает за изменение Post Одним из недостатков шаблона "Состояние" является то, что поскольку состояния сами реализуют переходы между собой, некоторые из состояний получаются связанными друг с другом. Если мы добавим другое состояние между PendingReview и Published , например Scheduled ("запланировано"), то придётся изменить код в PendingReview , чтобы оно теперь переходило в Scheduled . Если бы не нужно было менять PendingReview при добавлении нового состояния, было бы меньше работы, но это означало бы, что мы переходим на другой шаблон проектирования. Другим недостатком является то, что мы продублировали некоторую логику. Чтобы устранить некоторое дублирование, мы могли бы попытаться сделать реализации по умолчанию для методов request_review и approve типажа State , которые возвращают self ; однако это нарушило бы безопасность объекта, потому что типаж не знает, каким конкретно будет self . Мы хотим иметь возможность использовать State в качестве типаж-объекта, поэтому нам нужно, чтобы его методы были объектно-безопасными. Другое дублирование включает в себя схожие реализации методов request_review и approve у Post . Оба метода делегируют реализации одного и того же метода значению поля state типа Option и устанавливают результатом новое значение поля state . Если бы у Post было много методов, которые следовали этому шаблону, мы могли бы рассмотреть определение макроса для устранения повторения (смотри раздел "Макросы" в главе 19). Реализуя шаблон "Состояние" точно так, как он определён для объектно- ориентированных языков, мы не настолько полно используем преимущества Rust, как могли бы. Давайте посмотрим на некоторые изменения, которые мы можем внести в крейт blog , чтобы недопустимые состояния и переходы превратить в ошибки времени компиляции. |