Chamar:
cargo run
Da raiz do repositório e você está pronto para ir.
Tente também explorar os documentos da API para ter uma idéia de onde tudo vive:
cargo doc --open
Este repositório contém um aplicativo de ferrugem de amostra para uma loja on -line. O objetivo é explorar alguns padrões de design que aproveitam a linguagem ferrugem para criar aplicativos escaláveis e sustentáveis.
É um playground para idéias diferentes, algumas delas podem não dar certo na prática. Se você tiver algum feedback sobre qualquer coisa aqui, sinta -se à vontade para abrir um problema!
É difícil projetar software no vácuo. Quando você não tem um domínio real para conduzir o que é importante, as decisões de design podem parecer arbitrárias. Fiz um esforço para documentar as decisões e as razões por trás delas, mas perguntas como devemos dividir os itens de pedidos dos pedidos? Ou as consultas em pedidos devem acessar tabelas de banco de dados para produtos? Não posso realmente ser respondido de um ponto de vista puramente técnico. Eles também exigem perspectiva dos objetivos do projeto. Para quem ler este código, eu o encorajo a examiná -lo com base nessas decisões de design arbitrárias, pense nas restrições que você enfrenta em seu próprio ambiente e como elas podem informar suas próprias decisões ao criar aplicativos em ferrugem.
Não se trata de estruturas ou bibliotecas de ferrugem específicas ou sobre resolver problemas inerentes a um aplicativo de compras on -line.
As seções a seguir descrevem partes do aplicativo e explicam por que elas são montadas da maneira que são.
O layout do projeto está focado na privacidade. Ao limitar o escopo de certos itens, você também limita o escopo da possível quebra. Ao limitar o escopo de certos itens, você também limita o escopo do ônus de manter o estado de aplicação. Na ferrugem, os itens privados em um módulo são visíveis para todos os filhos desse módulo . Isso pode parecer uma coisa ruim, mas a aproveitamos para impedir que as APIs de domínio vazem detalhes da implementação em prol de preocupações externas, como serialização e armazenamento.
Cada conceito de negócios central no aplicativo é dividido em sua própria pasta (principalmente) independente, como products ou customers . Cada módulo encapsula tudo o que há para saber sobre um conjunto específico de entidades:
/store )/queries )/commands ) As entidades podem depender de entidades de outro módulo, como uma Order dependendo de um Product ao adicioná -lo. Há uma hierarquia de privacidade em cada módulo de domínio:
from_datafrom_data para hidratar entidadesEsses módulos são um pouco pesados, mas em um aplicativo adequado adicionar novos módulos de domínio pode ser simplificado usando macros. Não usei macros neste aplicativo para que o código permaneça fácil de seguir.
Um problema com uma hierarquia de módulos perfeitamente criados é que tudo pode desmoronar quando você acaba com um conceito que simplesmente não se encaixa no layout atual. Quanto mais freqüentemente isso acontece, mais difícil se torna em conformidade com o layout que existia antes, porque se torna impossível dizer o que deveria ser.
Queremos que esses módulos gerenciem seu próprio destino, mas não queremos que eles sejam independentes até o ponto em que possam ser divididos em serviços separados. Isso é para simplificar as coisas. Se você quis fazer isso, sugiro usar caixas separadas em vez de apenas módulos separados.
O aplicativo segue um design simples de segregação de responsabilidade de consulta de comando. Esta é uma abordagem que funciona bem para o aplicativo orientado a dados sem muita lógica complexa. Os comandos capturam alguma interação de domínio e trabalham diretamente em entidades, enquanto as consultas são totalmente arbitrárias. Este aplicativo não usa nenhuma infraestrutura especial para realizar o CQRS, são apenas características simples implementadas usando um padrão de injeção de dependência. Essencialmente:
Result<()>Result<T>&mut self&self -receptorA diferença na mutabilidade significa que os comandos podem chamar consultas, mas as consultas não podem chamar comandos.
As entidades são o coração da aplicação. Apesar da falta de um negócio real, fiz um esforço para manter o modelo de domínio rico. As entidades não são apenas sacos de estado cruel. Eles são:
.to_data() . Ao visualizar uma entidade, você não pode chamar o comportamento de modificação nela. Isso é garantido pelo sistema de empréstimos da Rust. Uma entidade pode mover a propriedade para seus dados somente leitura com .into_data() . Esta é uma operação unidirecional; portanto, quaisquer alterações feitas no estado não podem ser persistidas de volta à loja.O objetivo de uma entidade é encapsular os invariantes de algum conceito de domínio -chave. As entidades aqui são fáceis de usar com uma loja de memória simulada ou um banco de dados externo. Devemos ter cuidado para não confiar nas mudanças estatais, com uma entidade sendo refletida em outra, porque elas apontam para a mesma fonte.
As entidades também precisam ter cuidado para não depender dos tipos de dados de outra entidade, porque não há garantia de que os dados sejam realmente válidos. Em vez disso, eles dependem de uma entidade e a convertem em dados, conforme necessário, para que sempre saibam que o estado é válido.
Usamos os seguintes recursos de ferrugem para proteger nosso estado de entidade:
Serialize ou Deserialize . Isso pode ser alterado na pista, mas acho mais fácil manter o estado serializável em rápido e-loose para compatibilidade com versões anteriores.As entidades encapsulam algum estado ou dados e garantem que as alterações feitas com esses dados não quebrem nenhum invariante que os dados esperam manter. Em vez de implementar o Getters, expomos uma visão somente leitura dos dados como uma estrutura. O benefício é que você não precisa desistir dos bons recursos da Rust para trabalhar com dados de dados, como faria com os métodos Getter. Essa visualização é somente leitura , portanto, as alterações não podem ser escritas diretamente de volta à estrutura. A entidade ainda fornece métodos de setter para isso.
Você pode argumentar que a exposição de estado dessa maneira vaza detalhes da implementação, como a version que não tem valor sendo pública. Isso provavelmente é verdade. Para contornar isso, você pode mover a vida útil da visualização somente leitura para os campos e compor uma visão emprestada potencialmente diferente do estado e manter a estrutura de dados gerenciada pela entidade privada.
Você também pode argumentar que manter invariantes em uma estrutura que não os armazena é quebradiça. Isso faz sentido quando o limite de privacidade para algum campo está no nível do objeto, como se estivesse em C#. A ferrugem é um pouco diferente. O limite de privacidade mais apertado está no módulo e em seus filhos . Portanto, o ônus de manter os invariantes de um determinado campo cai em todos os itens do módulo em que é definido, além de todos os filhos desse módulo.
Isso pode parecer um vazamento terrível, mas esse aplicativo explora isso para criar armazenamento bem abstraído. Em vez de ter que expor buracos em nossa API para apoiar um ORM, manter o estado dos invariantes simplesmente se estende à loja de modelos, sem vazar de volta ao público.
Os tipos de Id e Version têm um parâmetro genérico fantasma. Esse parâmetro existe puramente para permitir que você expresse IDs com tipos incompatíveis, como Id<ProductData> e Id<OrderData> , mas ainda compartilham outros detalhes da implementação.
É um padrão que é mais fácil de seguir do que usar uma macro para reduzir o caldelador, porque sempre há uma diferença na fonte para a qual você pode voltar.
Cada entidade persistente tem um campo version . Este campo é um identificador não sequencial que corresponde ao estado da entidade em um determinado momento. Quando uma entidade é buscada na loja, hidratamos sua versão, isso é verificado logo antes da atualização e, se eles não corresponderem, nos recusamos.
A verificação da versão funciona bem para a loja de memória, porque temos um bloqueio exclusivo nos dados (apenas 1 chamador pode modificar o estado por vez), mas precisará de uma abordagem diferente para um banco de dados adequado. Provavelmente, podemos atualizar onde o ID e a versão correspondem, selecione o número de registros atualizados e o Balk se for 0 (significa que a versão não corresponde ou ela não existe).
A camada de armazenamento usa um esquema transacional simples que permite que os armazenamentos de dados independentes participem de transações. Um repositório central acompanha as transações ativas e é consultado quando os dados são obtidos nas lojas de dados para garantir que estejam prontos para serem usados. A concorrência otimista dos dados garante que várias transações ativas não possam tentar definir o mesmo valor ao mesmo tempo. Isso viola o verdadeiro isolamento, mas mantém as coisas simples e nos permite minimizar o estado necessário para cada valor que está sendo armazenado.
A injeção de dependência é benéfica como prática para se apoiar ao projetar aplicativos. Ele permite separar as preocupações da resolução de dependência da lógica do aplicativo. Também oferece uma maneira óbvia de dimensionar um aplicativo. Este aplicativo adota um padrão simples que nos dá esses benefícios sem muita infraestrutura.
Este aplicativo não usa uma inversão de contêiner de controle como você pode estar acostumado se gravar aplicativos .NET. Isso ocorre principalmente porque não há realmente nenhum para ferrugem. É um problema difícil. No entanto, utiliza um padrão simples de injeção de dependência para compor comandos e consultas, mesmo sem um contêiner sofisticado.
O principal objetivo da injeção de dependência aqui não é apoiar a zombaria. É para reduzir a complexidade, afastando as preocupações periféricas mais longe da lógica de um componente individual.
Componentes injetáveis vivem em seu próprio módulo. Esse módulo contém:
Resolver compartilhado que contém um método que retorna a implementação padrão sem exigir suas dependências.impl Trait . Você nunca sabe qual tipo de concreto essa implementação padrão usa.Arc , Box . O Resolver compartilhado soa um pouco de serviço-Y, e é, mas como a resolução de dependência está totalmente contida nos blocos de implicação no próprio Resolver , evitamos a questão de depender do estado global mágico em nossa lógica de aplicativos.
Para reduzir o caldeira, para componentes com apenas um único método, também ostleamos os implementam para traços Fn . Isso permite que você evite declarar uma estrutura para eles que é genérica sobre todas as suas dependências. O compilador de ferrugem cuidará disso para você.
É difícil descrever esse padrão em prosa, você precisa vê -lo. Dê uma olhada no módulo domain/products/commands/create_product , ou os módulos de domain/products/model/store para exemplos desse padrão de injeção de dependência em ação.
Resolver não é um "objeto de Deus"? Um objeto de Deus " é um objeto em seu aplicativo que coleta toda a lógica importante a um ponto em que você não pode trabalhar com componentes sem também trabalhar com o objeto de Deus. Eles são um problema porque se tornam difíceis de construir ou mudar. O padrão Resolver Resolver aqui é um objeto de Deus, mas não é necessário para construir componentes individuais.