Llamar:
cargo run
Desde la raíz del repositorio y estará listo para comenzar.
También intente explorar los documentos de la API para tener una idea de dónde vive todo:
cargo doc --open
Este repositorio contiene una aplicación de óxido de muestra para una tienda en línea. El objetivo es explorar algunos patrones de diseño que aprovechan el lenguaje de óxido para construir aplicaciones escalables y mantenibles.
Es un patio de recreo para diferentes ideas, algunas de ellas podrían no funcionar en la práctica. Si tiene algún comentario sobre algo aquí, ¡no dude en abrir un problema!
Es difícil diseñar software en el vacío. Cuando no tiene un dominio real para impulsar lo importante, las decisiones de diseño pueden sentirse arbitrarias. He hecho un esfuerzo para documentar las decisiones y las razones detrás de ellas, pero ¿ debemos dividir los elementos de pedidos de los pedidos de pedidos? ¿O deberían las consultas en los pedidos poder acceder a tablas de bases de datos para productos? Realmente no se puede responder desde un punto de vista puramente técnico. También requieren una perspectiva sobre los objetivos del proyecto. Para cualquiera que lea este código, le animo a que lo examine en función de esas decisiones de diseño arbitrarias, piense en las limitaciones que enfrenta en su propio entorno y cómo podrían informar sus propias decisiones al construir aplicaciones en óxido.
No se trata de marcos o bibliotecas de óxido específicos, o de resolver problemas inherentes a una solicitud de compras en línea.
Las siguientes secciones describen partes de la aplicación y explican por qué están juntas como son.
El diseño del proyecto se centra en la privacidad. Al limitar el alcance de ciertos elementos, también limita el alcance de la posible rotura. Al limitar el alcance de ciertos elementos, también limita el alcance de la carga de mantener el estado de aplicación. En Rust, los artículos que son privados en un módulo son visibles para todos los hijos de ese módulo . Eso puede sonar como algo malo, pero lo aprovechamos para evitar que las API de dominio filtren los detalles de implementación en aras de las preocupaciones externas, como la serialización y el almacenamiento.
Cada concepto comercial central en la aplicación se divide en su propia carpeta autónoma (en su mayoría), como products o customers . Cada módulo encapsula todo lo que hay que saber sobre un conjunto particular de entidades:
/store )/queries )/commands ) Las entidades pueden depender de las entidades de otro módulo, como un Order dependiendo de un Product al agregarlo. Hay una jerarquía de privacidad en cada módulo de dominio:
from_datafrom_data entidades de hidratoEstos módulos son un poco pesados, pero en una aplicación adecuada agregar nuevos módulos de dominio podrían simplificarse usando macros. No he usado macros en esta aplicación, por lo que el código sigue siendo fácil de seguir.
Un problema con una jerarquía de módulos perfectamente elaborada es que todo puede desmoronarse cuando termina con un concepto que simplemente no encaja en el diseño actual. Cuanto más frecuentemente sucede, más difícil será ajustarse al diseño que existía antes porque se hace imposible saber qué debería ser.
Queremos que estos módulos administren su propio destino, pero no queremos que sean autónomos hasta el punto de que puedan dividirse en servicios separados. Esto es para mantener las cosas simples. Si quisiera hacer esto, sugeriría usar cajas separadas en lugar de solo módulos separados.
La aplicación sigue un diseño de segregación de responsabilidad de consulta de comando simple. Este es un enfoque que funciona bien para la aplicación basada en datos sin mucha lógica compleja. Los comandos capturan alguna interacción de dominio y trabajan directamente en entidades, mientras que las consultas son totalmente arbitrarias. Esta aplicación no utiliza ninguna infraestructura especial para realizar CQRS, son solo rasgos simples implementados utilizando un patrón de inyección de dependencia. Esencialmente:
Result<()>Result<T>&mut self&selfLa diferencia en la mutabilidad significa que los comandos pueden llamar consultas, pero las consultas no pueden llamar comandos.
Las entidades son el corazón de la aplicación. A pesar de la falta de un negocio real, he hecho un esfuerzo para mantener rico al modelo de dominio. Las entidades no son solo bolsas de Estado Cruddy. Ellos son:
.to_data() . Mientras ve una entidad, no puede llamar a la modificación del comportamiento. Esto está garantizado por el sistema de préstamos de Rust. Una entidad puede mover la propiedad a sus datos de solo lectura con .into_data() . Esta es una operación unidireccional, por lo que cualquier cambio realizado en el estado no puede persistirse de regreso a la tienda.El objetivo de una entidad es encapsular a los invariantes de algún concepto de dominio clave. Las entidades aquí son fáciles de usar con una tienda simulada en memoria o una base de datos externa. Debemos tener cuidado de no confiar en los cambios de estado con una entidad reflejada en otra porque apuntan a la misma fuente.
Las entidades también deben tener cuidado de no depender de los tipos de datos de otra entidad porque no hay garantía de que los datos sean realmente válidos. En cambio, dependen de una entidad y las convierten en datos según sea necesario, por lo que siempre saben que el estado es válido.
Utilizamos las siguientes características de óxido para proteger nuestro estado de entidad:
Serialize ni Deserialize . Esto puede cambiarse en la pista, pero me resulta más fácil mantener el estado serializable rápido y bueno para la compatibilidad hacia atrás.Las entidades encapsulan algún estado o datos y se aseguran que cualquier cambio realizado a esos datos no rompa ningún invariante que los datos esperen mantener. En lugar de implementar Getters, exponemos una visión de solo lectura de los datos como una estructura. El beneficio es que no tiene que renunciar a las buenas características de Rust para trabajar con datos, como lo haría con los métodos Getter. Esta vista es de solo lectura , por lo que los cambios no se pueden escribir directamente de nuevo a la estructura. La entidad todavía proporciona métodos setter para eso.
Podría argumentar que exponer el estado de esta manera filtra los detalles de implementación, como la version que no tiene valor ser público. Esto es probablemente cierto. Para trabajar en torno a él, puede mover la vida de la visión de solo lectura a los campos, y componer una visión prestada potencialmente diferente del estado y mantener la estructura de datos administrada por la entidad privada.
También podría argumentar que mantener invariantes en una estructura que no los almacena es frágil. Esto tiene sentido cuando el límite de privacidad para algún campo está a nivel de objeto, como si estuviera en C#. Sin embargo, el óxido es un poco diferente. El límite de privacidad más ajustado está en el módulo y sus hijos . Entonces, la carga de mantener los invariantes de un campo dado recae en todos los elementos en el módulo en los que se define, además de todos los niños de ese módulo.
Esto puede sonar como una fuga horrible, pero esta aplicación explota que para construir un almacenamiento bien abstraído. En lugar de tener que exponer agujeros en nuestra API para apoyar a un ORM, mantener el estado de los invariantes simplemente se extiende a la tienda de modelos, sin retroceder al público.
Los tipos de Id y Version tienen un parámetro genérico fantasma. Este parámetro existe exclusivamente para permitirle expresar identificaciones con tipos incompatibles, como Id<ProductData> e Id<OrderData> , pero aún así compartir otros detalles de implementación.
Es un patrón que es más fácil de seguir que usar una macro para reducir la caldera porque siempre hay una diferencia en la fuente a la que puede volver.
Cada entidad persistible tiene un campo version . Este campo es un identificador no secuencial que corresponde al estado de la entidad en un momento dado. Cuando se obtiene una entidad de la tienda, hidratamos su versión, esto se verifica justo antes de actualizarse y si no coinciden, nos dimos cuenta.
La verificación de la versión funciona bien para el almacén en memoria porque tenemos un bloqueo exclusivo en los datos (solo 1 persona que llama puede modificar el estado a la vez), pero necesitará un enfoque diferente para un DB adecuado. Probablemente podamos actualizar dónde coinciden la ID y la versión, seleccione el número de registros actualizados y Balk si es 0 (significa que la versión no coincide o no existe).
La capa de almacenamiento utiliza un esquema transaccional simple que permite que las tiendas de datos independientes participen en transacciones. Un repositorio central realiza un seguimiento de las transacciones activas y se consulta cuando los datos se obtienen de las tiendas de datos para asegurarse de que estén listos para ser utilizados. La concurrencia optimista en los datos garantiza que varias transacciones activas no puedan intentar establecer el mismo valor al mismo tiempo. Esto viola el verdadero aislamiento, pero mantiene las cosas simples y nos permite minimizar el estado necesario para cada valor almacenado.
La inyección de dependencia es beneficiosa como una práctica para apoyarse al diseñar aplicaciones. Le permite separar las preocupaciones de la resolución de dependencia de la lógica de la aplicación. También le brinda una forma obvia de escalar una aplicación. Esta aplicación adopta un patrón simple que nos brinda estos beneficios sin mucha infraestructura.
Esta aplicación no utiliza una inversión de contenedor de control como se puede usar si escribe aplicaciones .NET. Esto se debe principalmente a que realmente no hay nada para el óxido. Es un problema difícil. Sin embargo, utiliza un patrón de inyección de dependencia simple para componer comandos y consultas, incluso sin un contenedor sofisticado.
El objetivo principal de la inyección de dependencia aquí no es soportar la burla. Es para reducir la complejidad empujando las preocupaciones periféricas más lejos de la lógica de un componente individual.
Los componentes inyectables viven en su propio módulo. Ese módulo contiene:
Resolver compartido que contiene un método que devuelve la implementación predeterminada sin requerir sus dependencias.impl Trait . Nunca se sabe qué tipo concreto utiliza esta implementación predeterminada.Arc , Box . El Resolver compartido suena un poco de servicio de servicio, y lo es, pero debido a que la resolución de dependencia está totalmente contenida en los bloques de implicación en el Resolver en sí, evitamos el problema de depender de Magic Global State en nuestra lógica de la aplicación.
Para reducir la caldera, para los componentes con un solo método, también los implementamos para los rasgos Fn . Esto le permite evitar declarar una estructura para ellos que es genérico sobre todas sus dependencias. El compilador de óxido se encargará de eso por usted.
Este patrón es difícil de describir en prosa, necesitas verlo. Eche un vistazo al módulo domain/products/commands/create_product , o los módulos domain/products/model/store para ejemplos de este patrón de inyección de dependencia en el trabajo.
Resolver un "objeto de Dios"? Un objeto de Dios " es un objeto en su aplicación que recopila toda la lógica importante a un punto en el que no puede trabajar con los componentes sin trabajar también a través del objeto de Dios. Son un problema porque se vuelven difíciles de construir o cambiar. El patrón Resolver aquí es un objeto de Dios, pero no es necesario construir componentes individuales. El Resolver solo con la entrega de un componente de las dependencias que solicita, no es necesario construir o trabajar con los componentes.