Implementar un Patrón de Diseño Orientado a Objetos

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Implementando un Patrón de Diseño Orientado a Objetos. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, implementaremos el patrón de estado en un diseño orientado a objetos para crear una estructura de publicación de blog que transicione a través de diferentes estados (borrador, revisión y publicada) según su comportamiento, lo que garantiza que solo las publicaciones de blog publicadas puedan devolver contenido.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/mutable_variables -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/string_type -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/function_syntax -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/expressions_statements -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/method_syntax -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/traits -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} rust/operator_overloading -.-> lab-100443{{"Implementar un Patrón de Diseño Orientado a Objetos"}} end

Implementando un Patrón de Diseño Orientado a Objetos

El patrón de estado es un patrón de diseño orientado a objetos. El eje del patrón es que definimos un conjunto de estados que un valor puede tener internamente. Los estados se representan por un conjunto de objetos de estado, y el comportamiento del valor cambia según su estado. Vamos a trabajar en un ejemplo de una estructura de publicación de blog que tiene un campo para almacenar su estado, que será un objeto de estado del conjunto "borrador", "revisión" o "publicado".

Los objetos de estado comparten funcionalidad: en Rust, por supuesto, usamos structs y traits en lugar de objetos e herencia. Cada objeto de estado es responsable de su propio comportamiento y de gobernar cuándo debe cambiar a otro estado. El valor que contiene un objeto de estado no sabe nada sobre el comportamiento diferente de los estados ni cuándo hacer la transición entre estados.

La ventaja de usar el patrón de estado es que, cuando los requisitos comerciales del programa cambien, no tendremos que cambiar el código del valor que contiene el estado ni el código que usa el valor. Solo tendremos que actualizar el código dentro de uno de los objetos de estado para cambiar sus reglas o quizás agregar más objetos de estado.

Primero implementaremos el patrón de estado de una manera más tradicional orientada a objetos, luego usaremos un enfoque que es un poco más natural en Rust. Vamos a profundizar para implementar incrementalmente un flujo de trabajo de publicación de blog usando el patrón de estado.

La funcionalidad final se verá así:

  1. Una publicación de blog comienza como un borrador vacío.
  2. Cuando el borrador está listo, se solicita una revisión de la publicación.
  3. Cuando la publicación es aprobada, se publica.
  4. Solo las publicaciones de blog publicadas devuelven contenido para imprimir, por lo que las publicaciones no aprobadas no se pueden publicar accidentalmente.

Cualquier otro cambio intentado en una publicación no debería tener ningún efecto. Por ejemplo, si intentamos aprobar una publicación de blog en borrador antes de haber solicitado una revisión, la publicación debería permanecer como un borrador no publicado.

La Lista 17-11 muestra este flujo de trabajo en forma de código: este es un ejemplo de uso de la API que implementaremos en un crat de biblioteca llamado blog. Esto todavía no se compilará porque no hemos implementado el crat blog.

Nombre de archivo: src/main.rs

use blog::Post;

fn main() {
  1 let mut post = Post::new();

  2 post.add_text("I ate a salad for lunch today");
  3 assert_eq!("", post.content());

  4 post.request_review();
  5 assert_eq!("", post.content());

  6 post.approve();
  7 assert_eq!("I ate a salad for lunch today", post.content());
}

Lista 17-11: Código que demuestra el comportamiento deseado que queremos que tenga nuestro crat blog

Queremos permitir que el usuario cree una nueva publicación de blog en borrador con Post::new [1]. Queremos permitir que se agregue texto a la publicación de blog [2]. Si intentamos obtener el contenido de la publicación inmediatamente, antes de la aprobación, no deberíamos obtener ningún texto porque la publicación todavía es un borrador. Hemos agregado assert_eq! en el código con fines de demostración [3]. Una excelente prueba unitaria para esto sería afirmar que una publicación de blog en borrador devuelve una cadena vacía del método content, pero no vamos a escribir pruebas para este ejemplo.

Luego, queremos habilitar una solicitud de revisión de la publicación [4], y queremos que content devuelva una cadena vacía mientras se espera la revisión [5]. Cuando la publicación recibe la aprobación [6], debería publicarse, lo que significa que el texto de la publicación se devolverá cuando se llame a content [7].

Tenga en cuenta que el único tipo con el que estamos interactuando del crat es el tipo Post. Este tipo usará el patrón de estado y contendrá un valor que será uno de tres objetos de estado que representan los diferentes estados en los que puede estar una publicación: borrador, revisión o publicada. El cambio de un estado a otro se gestionará internamente dentro del tipo Post. Los estados cambian en respuesta a los métodos llamados por los usuarios de nuestra biblioteca en la instancia Post, pero no tienen que gestionar directamente los cambios de estado. Además, los usuarios no pueden cometer un error con los estados, como publicar una publicación antes de que se revise.

Definiendo Post y Creando una Nueva Instancia en el Estado Borrador

Comencemos con la implementación de la biblioteca. Sabemos que necesitamos una estructura pública Post que almacene algún contenido, así que comenzaremos con la definición de la estructura y una función pública asociada new para crear una instancia de Post, como se muestra en la Lista 17-12. También crearemos un trato privado State que definirá el comportamiento que todos los objetos de estado de un Post deben tener.

Luego, Post contendrá un objeto de trato de Box<dyn State> dentro de un Option<T> en un campo privado llamado state para almacenar el objeto de estado. Verás por qué es necesario el Option<T> en un momento.

Nombre de archivo: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
          1 state: Some(Box::new(Draft {})),
          2 content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Lista 17-12: Definición de una estructura Post y una función new que crea una nueva instancia de Post, un trato State y una estructura Draft

El trato State define el comportamiento compartido por diferentes estados de publicación. Los objetos de estado son Draft, PendingReview y Published, y todos ellos implementarán el trato State. Por ahora, el trato no tiene ningún método, y comenzaremos definiendo solo el estado Draft porque ese es el estado en el que queremos que comience una publicación.

Cuando creamos una nueva instancia de Post, establecemos su campo state en un valor Some que contiene un Box [1]. Este Box apunta a una nueva instancia de la estructura Draft. Esto garantiza que cada vez que creemos una nueva instancia de Post, comenzará como un borrador. Debido a que el campo state de Post es privado, no hay forma de crear un Post en cualquier otro estado. En la función Post::new, establecemos el campo content en una String nueva y vacía [2].

Almacenando el Texto del Contenido de la Publicación

Vimos en la Lista 17-11 que queremos poder llamar a un método llamado add_text y pasarle un &str que luego se agregue como el contenido de texto de la publicación de blog. Lo implementamos como un método, en lugar de exponer el campo content como pub, para que más adelante podamos implementar un método que controlará cómo se lee los datos del campo content. El método add_text es bastante sencillo, así que agreguemos la implementación en la Lista 17-13 al bloque impl Post.

Nombre de archivo: src/lib.rs

impl Post {
    --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Lista 17-13: Implementando el método add_text para agregar texto al content de una publicación

El método add_text toma una referencia mutable a self porque estamos cambiando la instancia de Post en la que estamos llamando a add_text. Luego llamamos a push_str en la String en content y pasamos el argumento text para agregarlo al content guardado. Este comportamiento no depende del estado en el que se encuentra la publicación, por lo que no es parte del patrón de estado. El método add_text no interactúa con el campo state en absoluto, pero es parte del comportamiento que queremos admitir.

Asegurándose de que el Contenido de una Publicación en Borrador Sea Vacio

Incluso después de haber llamado a add_text y agregado algún contenido a nuestra publicación, todavía queremos que el método content devuelva una porción de cadena vacía porque la publicación todavía está en el estado de borrador, como se muestra en [3] de la Lista 17-11. Por ahora, implementemos el método content con lo más simple que cumpla con este requisito: siempre devolver una porción de cadena vacía. Lo cambiaremos más adelante una vez que implementemos la capacidad de cambiar el estado de una publicación para que pueda publicarse. Hasta ahora, las publicaciones solo pueden estar en el estado de borrador, por lo que el contenido de la publicación siempre debe estar vacío. La Lista 17-14 muestra esta implementación temporal.

Nombre de archivo: src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

Lista 17-14: Agregando una implementación temporal para el método content en Post que siempre devuelve una porción de cadena vacía

Con este método content agregado, todo en la Lista 17-11 hasta la línea en [3] funciona como se esperaba.

Solicitar una Revisión Cambia el Estado de la Publicación

A continuación, necesitamos agregar funcionalidad para solicitar una revisión de una publicación, lo que debería cambiar su estado de Draft a PendingReview. La Lista 17-15 muestra este código.

Nombre de archivo: src/lib.rs

impl Post {
    --snip--
  1 pub fn request_review(&mut self) {
      2 if let Some(s) = self.state.take() {
          3 self.state = Some(s.request_review())
        }
    }
}

trait State {
  4 fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      5 Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      6 self
    }
}

Lista 17-15: Implementando métodos request_review en Post y el trato State

Le damos a Post un método público llamado request_review que tomará una referencia mutable a self [1]. Luego llamamos a un método interno request_review en el estado actual de Post [3], y este segundo método request_review consume el estado actual y devuelve un nuevo estado.

Agregamos el método request_review al trato State [4]; todos los tipos que implementen el trato ahora necesitarán implementar el método request_review. Tenga en cuenta que en lugar de tener self, &self o &mut self como el primer parámetro del método, tenemos self: Box<Self>. Esta sintaxis significa que el método solo es válido cuando se llama en un Box que contiene el tipo. Esta sintaxis toma posesión de Box<Self>, invalidando el antiguo estado para que el valor de estado de Post pueda transformarse en un nuevo estado.

Para consumir el antiguo estado, el método request_review necesita tomar posesión del valor de estado. Aquí es donde entra el Option en el campo state de Post: llamamos al método take para sacar el valor Some del campo state y dejar un None en su lugar porque Rust no nos permite tener campos no rellenados en structs [2]. Esto nos permite mover el valor de state fuera de Post en lugar de prestarlo. Luego estableceremos el valor de state de la publicación en el resultado de esta operación.

Necesitamos establecer state en None temporalmente en lugar de establecerlo directamente con código como self.state = self.state.request_review(); para obtener la posesión del valor de state. Esto garantiza que Post no pueda usar el antiguo valor de state después de haberlo transformado en un nuevo estado.

El método request_review en Draft devuelve una nueva instancia empaquetada de una nueva estructura PendingReview, que representa el estado cuando una publicación está esperando una revisión [5]. La estructura PendingReview también implementa el método request_review pero no realiza ninguna transformación. En lugar de eso, devuelve a sí misma [6] porque cuando solicitamos una revisión de una publicación ya en el estado PendingReview, debería permanecer en el estado PendingReview.

Ahora podemos comenzar a ver las ventajas del patrón de estado: el método request_review en Post es el mismo independientemente del valor de su state. Cada estado es responsable de sus propias reglas.

Dejaremos el método content en Post tal como está, devolviendo una porción de cadena vacía. Ahora podemos tener un Post en el estado PendingReview así como en el estado Draft, pero queremos el mismo comportamiento en el estado PendingReview. La Lista 17-11 ahora funciona hasta la línea en [5]!

Agregar aprobar para Cambiar el Comportamiento de content

El método aprobar será similar al método request_review: establecerá state en el valor que el estado actual dice que debe tener cuando ese estado es aprobado, como se muestra en la Lista 17-16.

Nombre de archivo: src/lib.rs

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> {
      1 self
    }
}

struct PendingReview {}

impl State for PendingReview {
    --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
      2 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
    }
}

Lista 17-16: Implementando el método approve en Post y el trato State

Agregamos el método approve al trato State y agregamos una nueva estructura que implementa State, el estado Published.

Similar a la forma en que funciona request_review en PendingReview, si llamamos al método approve en un Draft, no tendrá ningún efecto porque approve devolverá self [1]. Cuando llamamos a approve en PendingReview, devuelve una nueva instancia empaquetada de la estructura Published [2]. La estructura Published implementa el trato State, y para ambos métodos request_review y approve, devuelve a sí misma porque la publicación debería permanecer en el estado Published en esos casos.

Ahora necesitamos actualizar el método content en Post. Queremos que el valor devuelto por content dependa del estado actual de Post, por lo que vamos a hacer que Post delegue a un método content definido en su state, como se muestra en la Lista 17-17.

Nombre de archivo: src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    --snip--
}

Lista 17-17: Actualizando el método content en Post para delegar a un método content en State

Debido a que el objetivo es mantener todas estas reglas dentro de las estructuras que implementan State, llamamos a un método content en el valor en state y pasamos la instancia de la publicación (es decir, self) como argumento. Luego devolvemos el valor que se devuelve al usar el método content en el valor de state.

Llamamos al método as_ref en el Option porque queremos una referencia al valor dentro del Option en lugar de la posesión del valor. Debido a que state es un Option<Box<dyn State>>, cuando llamamos a as_ref, se devuelve un Option<&Box<dyn State>>. Si no llamáramos a as_ref, obtendríamos un error porque no podemos mover state fuera del &self prestado del parámetro de la función.

Luego llamamos al método unwrap, que sabemos que nunca causará un panic porque sabemos que los métodos en Post aseguran que state siempre contendrá un valor Some cuando esos métodos se completen. Este es uno de los casos que mencionamos en "Casos en los que Tienes Más Información que el Compilador" cuando sabemos que un valor None nunca es posible, aunque el compilador no sea capaz de entenderlo.

En este momento, cuando llamamos a content en el &Box<dyn State>, la coerción de dereferencia entrará en efecto en el & y el Box para que el método content finalmente se llame en el tipo que implementa el trato State. Eso significa que necesitamos agregar content a la definición del trato State, y ahí es donde pondremos la lógica de qué contenido devolver según el estado que tengamos, como se muestra en la Lista 17-18.

Nombre de archivo: src/lib.rs

trait State {
    --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
      1 ""
    }
}

--snip--
struct Published {}

impl State for Published {
    --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
      2 &post.content
    }
}

Lista 17-18: Agregando el método content al trato State

Agregamos una implementación predeterminada para el método content que devuelve una porción de cadena vacía [1]. Eso significa que no necesitamos implementar content en las estructuras Draft y PendingReview. La estructura Published sobrescribirá el método content y devolverá el valor en post.content [2].

Tenga en cuenta que necesitamos anotaciones de tiempo de vida en este método, como discutimos en el Capítulo 10. Estamos tomando una referencia a un post como argumento y devolviendo una referencia a una parte de ese post, por lo que el tiempo de vida de la referencia devuelta está relacionado con el tiempo de vida del argumento post.

Y ya terminamos: todo lo de la Lista 17-11 ahora funciona. Hemos implementado el patrón de estado con las reglas del flujo de trabajo de la publicación de blog. La lógica relacionada con las reglas reside en los objetos de estado en lugar de estar dispersa en todo Post.

¿Por qué No Un Enum?

Es posible que hayas estado preguntándote por qué no usamos un enum con los diferentes posibles estados de publicación como variantes. Esa ciertamente es una solución posible; pruébalo y compara los resultados finales para ver cuál prefieres. Una desventaja de usar un enum es que cada lugar que verifica el valor del enum necesitará una expresión match o similar para manejar cada posible variante. Esto podría ser más repetitivo que esta solución de objeto de trato.

Ventajas y Desventajas del Patrón de Estado

Hemos demostrado que Rust es capaz de implementar el patrón de estado orientado a objetos para encapsular los diferentes tipos de comportamiento que una publicación debería tener en cada estado. Los métodos en Post no saben nada sobre los diversos comportamientos. Con la forma en que organizamos el código, solo tenemos que buscar en un solo lugar para conocer las diferentes maneras en que una publicación publicada puede comportarse: la implementación del trato State en la estructura Published.

Si tuviéramos que crear una implementación alternativa que no utilizara el patrón de estado, en lugar de eso podríamos usar expresiones match en los métodos de Post o incluso en el código de main que comprueba el estado de la publicación y cambia el comportamiento en esos lugares. Eso significaría que tendríamos que buscar en varios lugares para entender todas las implicaciones de una publicación estar en el estado publicado. Esto solo aumentaría a medida que agregáramos más estados: cada una de esas expresiones match necesitaría otro brazo.

Con el patrón de estado, los métodos de Post y los lugares donde usamos Post no necesitan expresiones match, y para agregar un nuevo estado, solo necesitaríamos agregar una nueva estructura y implementar los métodos del trato en esa sola estructura.

La implementación que utiliza el patrón de estado es fácil de extender para agregar más funcionalidad. Para ver la simplicidad de mantener el código que utiliza el patrón de estado, prueba algunas de estas sugerencias:

  • Agrega un método rechazar que cambie el estado de la publicación de PendingReview de vuelta a Draft.
  • Requiere dos llamadas a aprobar antes de que el estado pueda cambiar a Publicado.
  • Permite a los usuarios agregar contenido de texto solo cuando una publicación está en el estado Draft. Consejo: haz que el objeto de estado sea responsable de lo que podría cambiar en el contenido pero no responsable de modificar Post.

Una desventaja del patrón de estado es que, debido a que los estados implementan las transiciones entre estados, algunos de los estados están acoplados entre sí. Si agregamos otro estado entre PendingReview y Publicado, como Programado, tendríamos que cambiar el código en PendingReview para que transite a Programado en lugar. Sería menos trabajo si PendingReview no tuviera que cambiar con la adición de un nuevo estado, pero eso significaría cambiar a otro patrón de diseño.

Otra desventaja es que hemos duplicado algo de lógica. Para eliminar algo de la duplicación, podríamos intentar hacer implementaciones predeterminadas para los métodos request_review y approve en el trato State que devuelvan self. Sin embargo, esto no funcionaría: cuando se utiliza State como un objeto de trato, el trato no sabe exactamente cuál será el self concrete, por lo que el tipo de retorno no es conocido en tiempo de compilación.

Otra duplicación incluye las implementaciones similares de los métodos request_review y approve en Post. Ambos métodos delegan en la implementación del mismo método en el valor del campo state de Option y establecen el nuevo valor del campo state en el resultado. Si tuviéramos muchos métodos en Post que siguieran este patrón, podríamos considerar definir una macro para eliminar la repetición (ver "Macros").

Al implementar exactamente el patrón de estado como está definido para los lenguajes orientados a objetos, no estamos aprovechando al máximo las fortalezas de Rust como podríamos. Echemos un vistazo a algunos cambios que podemos hacer en el crat blog que pueden convertir los estados e transiciones no válidas en errores de tiempo de compilación.

Codificar Estados y Comportamiento como Tipos

Te mostraremos cómo repensar el patrón de estado para obtener un conjunto diferente de ventajas y desventajas. En lugar de encapsular completamente los estados y las transiciones para que el código externo no tenga conocimiento de ellos, codificaremos los estados en diferentes tipos. En consecuencia, el sistema de comprobación de tipos de Rust evitará intentos de usar publicaciones en borrador donde solo se permiten publicaciones publicadas emitiendo un error del compilador.

Consideremos la primera parte de main en la Lista 17-11:

Nombre de archivo: src/main.rs

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
}

Todavía habilitamos la creación de nuevas publicaciones en el estado de borrador usando Post::new y la capacidad de agregar texto al contenido de la publicación. Pero en lugar de tener un método content en una publicación en borrador que devuelva una cadena vacía, haremos que las publicaciones en borrador no tengan el método content en absoluto. De esa manera, si intentamos obtener el contenido de una publicación en borrador, obtendremos un error del compilador que nos dice que el método no existe. Como resultado, será imposible para nosotros mostrar accidentalmente el contenido de una publicación en borrador en producción porque ese código ni siquiera se compilará. La Lista 17-19 muestra la definición de una estructura Post y una estructura DraftPost, así como los métodos en cada una.

Nombre de archivo: src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
  1 pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

  2 pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
  3 pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Lista 17-19: Una Post con un método content y una DraftPost sin un método content

Tanto la estructura Post como la estructura DraftPost tienen un campo privado content que almacena el texto de la publicación del blog. Las estructuras ya no tienen el campo state porque estamos moviendo la codificación del estado a los tipos de las estructuras. La estructura Post representará una publicación publicada y tiene un método content que devuelve el content [2].

Todavía tenemos una función Post::new, pero en lugar de devolver una instancia de Post, devuelve una instancia de DraftPost [1]. Debido a que content es privado y no hay ninguna función que devuelva Post, no es posible crear una instancia de Post en este momento.

La estructura DraftPost tiene un método add_text, por lo que podemos agregar texto a content como antes [3], pero tenga en cuenta que DraftPost no tiene un método content definido. Entonces, ahora el programa asegura que todas las publicaciones empiecen como publicaciones en borrador y que las publicaciones en borrador no tienen su contenido disponible para mostrar. Cualquier intento de circunvenir estas restricciones resultará en un error del compilador.

Implementar Transiciones como Transformaciones en Diferentes Tipos

Entonces, ¿cómo obtenemos una publicación publicada? Queremos imponer la regla de que una publicación en borrador debe ser revisada y aprobada antes de poder ser publicada. Una publicación en el estado de revisión pendiente todavía no debe mostrar ningún contenido. Vamos a implementar estas restricciones agregando otra estructura, PendingReviewPost, definiendo el método request_review en DraftPost para devolver un PendingReviewPost y definiendo un método approve en PendingReviewPost para devolver una Post, como se muestra en la Lista 17-20.

Nombre de archivo: src/lib.rs

impl DraftPost {
    --snip--
    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

Lista 17-20: Un PendingReviewPost que se crea llamando a request_review en DraftPost y un método approve que convierte un PendingReviewPost en una Post publicada

Los métodos request_review y approve toman la posesión de self, consumiendo así las instancias DraftPost y PendingReviewPost y transformándolas en una PendingReviewPost y una Post publicada, respectivamente. De esta manera, no tendremos ninguna instancia de DraftPost pendiente después de haber llamado a request_review en ellas, y así sucesivamente. La estructura PendingReviewPost no tiene un método content definido en ella, por lo que intentar leer su contenido resulta en un error del compilador, como con DraftPost. Debido a que la única forma de obtener una instancia de Post publicada que tenga un método content definido es llamar al método approve en un PendingReviewPost, y la única forma de obtener un PendingReviewPost es llamar al método request_review en un DraftPost, ahora hemos codificado el flujo de trabajo de la publicación del blog en el sistema de tipos.

Pero también tenemos que hacer algunos pequeños cambios a main. Los métodos request_review y approve devuelven nuevas instancias en lugar de modificar la estructura en la que se llaman, por lo que necesitamos agregar más asignaciones de sombreado let post = para guardar las instancias devueltas. Tampoco podemos tener las afirmaciones de que los contenidos de las publicaciones en borrador y en revisión pendiente son cadenas vacías, ni las necesitamos: ya no podemos compilar el código que intenta usar el contenido de las publicaciones en esos estados. El código actualizado en main se muestra en la Lista 17-21.

Nombre de archivo: src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

Lista 17-21: Modificaciones a main para usar la nueva implementación del flujo de trabajo de la publicación del blog

Los cambios que tuvimos que hacer a main para reasignar post significa que esta implementación ya no sigue exactamente el patrón de estado orientado a objetos: las transformaciones entre los estados ya no están encapsuladas por completo dentro de la implementación de Post. Sin embargo, nuestra ganancia es que los estados inválidos ahora son imposibles debido al sistema de tipos y la comprobación de tipos que se realiza en tiempo de compilación. Esto garantiza que ciertos errores, como la visualización del contenido de una publicación no publicada, se descubrirán antes de llegar a producción.

Prueba las tareas sugeridas al principio de esta sección en el crat blog tal como está después de la Lista 17-21 para ver qué opinas sobre el diseño de esta versión del código. Tenga en cuenta que algunas de las tareas pueden estar ya completadas en este diseño.

Hemos visto que aunque Rust es capaz de implementar patrones de diseño orientados a objetos, otros patrones, como codificar el estado en el sistema de tipos, también están disponibles en Rust. Estos patrones tienen diferentes ventajas y desventajas. Aunque es posible que estés muy familiarizado con los patrones orientados a objetos, repensar el problema para aprovechar las características de Rust puede proporcionar beneficios, como prevenir algunos errores en tiempo de compilación. Los patrones orientados a objetos no siempre serán la mejor solución en Rust debido a ciertas características, como la propiedad, que los lenguajes orientados a objetos no tienen.

Resumen

¡Felicidades! Has completado el laboratorio de Implementación de un Patrón de Diseño Orientado a Objetos. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.