Главная страница
Навигация по странице:

  • Листинг 17-12. Определение структуры Post и функции new, которая создаёт новый экземпляр Post, типажа State и структуры

  • Хранение текста содержимого записи

  • Листинг 17-13. Реализация add_text для добавления текста к content (содержимому записи)

  • Убедимся, что содержание черновика будет пустым

  • Листинг 17-14. Добавление реализации-заглушки для метода content в Post, которая всегда возвращает пустой фрагмент строки.

  • Запрос на проверку записи меняет её состояние

  • Листинг 17-15. Реализация методов request_review в структуре Post и типаже

  • Добавление

  • Листинг 17-16. Реализация метода approve для типа Post и типажа

  • Листинг 17-17: Обновление метода content в структуре Post для делегирования части функциональности методу content структуры

  • Листинг 17-18. Добавление метода content в трейт

  • Компромиссы шаблона "Состояние"

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница48 из 62
    1   ...   44   45   46   47   48   49   50   51   ...   62
    Определение 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 является типом
    OptionState>>
    , то при вызове метода 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
    , чтобы недопустимые состояния и переходы превратить в ошибки времени компиляции.
    1   ...   44   45   46   47   48   49   50   51   ...   62


    написать администратору сайта