Ferramentas de Design Orientadas a Domínios

A baleia azul é um ótimo exemplo de como o design de um projeto complexo deu errado. A baleia parece um peixe, mas é um mamífero: alimenta os filhotes com leite, tem lã e os ossos do antebraço e as mãos com os dedos, como os de terra, ainda são preservados nas barbatanas. Ele mora nos oceanos, mas não consegue respirar debaixo d'água, por isso sobe regularmente à superfície para engolir ar, mesmo quando está dormindo. A baleia é o maior animal do mundo, com uma casa de nove andares e pesando 75 carros Volkswagen Touareg, mas não é um predador, mas se alimenta de plâncton.

Quando os desenvolvedores trabalharam na baleia, eles não começaram a escrever tudo do zero, mas usaram a experiência de projetos antigos. Parece ter sido construído com partes incompatíveis do código que não foram testadas, e todo o design se resumiu à escolha de uma estrutura e de um “ciclo” urgente já em produção. Como resultado, o projeto mostrou-se bonito na aparência, mas com pedaços de legado denso e muletas sob o capô.



Para criar projetos que ajudam as empresas a ganhar dinheiro, em vez de parecer um animal marinho que não pode respirar debaixo d'água, existe o DDD. Essa é uma abordagem que não se concentra em ferramentas ou código, mas no estudo da área de assunto, processos de negócios individuais e como o código ou as ferramentas funcionam para a lógica de negócios.

O que é DDD e quais ferramentas existem, contaremos em um artigo baseado no relatórioArtyom Malyshev . A abordagem DDD em Python, ferramentas, armadilhas, programação de contratos e design de produtos em torno do problema que está sendo resolvido, em vez da estrutura usada, estão todos em falta.

Apresentação completa do relatório .

Artem Malyshev (proofit404) - um desenvolvedor independente, escreveu em Python por 5 anos, ajudou ativamente com o Django Channels 1.0. Posteriormente, ele se concentrou nas abordagens arquitetônicas: estudou quais ferramentas os arquitetos Python não possuem e iniciou um projeto de dry-python . Co-fundador da Drylabs.

Complexidade


O que é programação?
A programação é uma luta constante com a complexidade que os próprios desenvolvedores criam quando tentam resolver problemas.
A complexidade é dividida em dois tipos: introduzidos e naturais. O apresentado se estende junto com linguagens de programação, frameworks, SO, modelo de assincronia. Este é um desafio técnico que não se aplica aos negócios. A complexidade natural está oculta no produto e simplifica a vida dos usuários - pois essas pessoas pagam dinheiro.
Bons engenheiros devem reduzir a complexidade adicional e aumentar o natural para aumentar a utilidade do produto.
Mas nós programadores somos pessoas complexas e gostamos de adicionar complexidade técnica aos projetos. Por exemplo, não nos preocupamos com os padrões de codificação, não usamos o linter, práticas de design modular e recebemos muito código de estilo nos projetos if c==1.

Como trabalhar com esse código? Leia muitos arquivos, entenda variáveis, condições e quando e como tudo funcionará. É difícil ter em mente esse código - complexidade adicional absolutamente técnica.

Outro exemplo de complexidade adicional é o meu "inferno de retorno de chamada" favorito.



Quando escrevemos na estrutura da arquitetura orientada a eventos (EDA) e escolhemos uma estrutura moderna não tão boa, obtemos um código no qual não está claro o que acontece e quando. É difícil ler esse código. Essa é novamente a complexidade adicionada.

Os programadores não apenas adoram dificuldades técnicas, mas também argumentam qual é o melhor:

  • AsyncIO ou Gevent;
  • PostgreSQL ou MongoDB;
  • Python ou Go;
  • Emacs ou Vim;
  • abas ou espaços;

A resposta correta de um bom programador para todas essas perguntas: "Não faz diferença!" Bons desenvolvedores não discutem sobre cavalos esféricos no vácuo, mas resolvem problemas de negócios e trabalham na utilidade do produto. Alguns deles há muito estabelecem um conjunto de práticas que reduzem a complexidade introduzida e o ajudam a pensar mais sobre os negócios.

Um deles é Eric Evans . Em 2004, ele escreveu o livro Domain Driven Design. Ela atirou e deu um impulso para pensar mais sobre os negócios e colocar os detalhes técnicos em segundo plano.



O que é DDD?


Primeiro, uma solução para o problema e, em seguida, ferramentas . Antes de tudo, Evans investiu no conceito de DDD, que não é uma tecnologia, mas uma filosofia. Na filosofia, você primeiro precisa pensar em como resolver o problema e, somente então, com a ajuda de quais ferramentas.

Trabalhe em modelos com especialistas no assunto e desenvolvedores de software. Precisamos nos comunicar com pessoas de negócios: procure uma linguagem comum, construa um modelo do mundo no qual nosso produto funcione e resolva problemas.

Escreva software que expresse explicitamente modelos. A diferença mais importante entre DDD e colaboração simples em uma equipe é que devemos escrever software no mesmo estilo que conversamos com especialistas em domínio. Toda terminologia, abordagens para discussão e tomada de decisão devem ser armazenadas no código fonte, para que mesmo uma pessoa não técnica possa entender o que está acontecendo lá.

Fale o mesmo idioma com os negócios . DDD é uma filosofia sobre como falar o mesmo idioma com especialistas em negócios em um campo específico e aplicar terminologia a esse campo. Temos uma linguagem ou dialeto comum dentro do contexto vinculado, que consideramos verdadeiro. Criamos limites em torno de soluções arquitetônicas.

DDD não é sobre tecnologia.

Primeiro, a parte técnica, então - DDD. O escultor que esculpe a estátua em pedra não lê o manual sobre como segurar um martelo e um cinzel - ele já sabe como trabalhar com eles. Para trazer o DDD para o seu projeto, domine a parte técnica: aprenda o Django até o fim, leia o tutorial e pare de discutir sobre o uso do PostgreSQL ou MongoDB.

A maioria dos padrões e padrões de design são ruídos técnicos. A maioria dos padrões que conhecemos e usamos é técnica. Eles dizem como reutilizar o código, como estruturá-lo, mas não dizem como usá-lo para usuários, empresas e modelar o mundo exterior. Portanto, fábricas ou classes abstratas são fracamente ligadas ao DDD.

O primeiro livro "azul" saiu quase 20 anos atrás. As pessoas tentaram escrever nesse estilo, andaram bastante e perceberam que a filosofia é boa, mas na prática incompreensível. Portanto, um segundo livro apareceu - "vermelho", sobre como os programadores pensam e escrevem no DDD.


Os livros "vermelho" e "azul" são os pilares sobre os quais todos os DDD se apoiam.

Nota. Os livros vermelho e azul são uma fonte única de informações sobre DDD, mas são pesados. Os livros não são fáceis de ler: no original por causa da linguagem e termos complexos, e em russo devido à má tradução. Portanto, comece a aprender DDD com um livro verde . Esta é uma versão simplificada dos dois primeiros, com exemplos mais simples e descrições gerais. Mas é melhor do que se os livros em vermelho e azul superassem seu desejo de estudar e aplicar DDD. É melhor ler no original.

O livro vermelho ignora a idéia de como melhor inserir DDD no projeto, como estruturar o trabalho em torno dessa abordagem. Uma nova terminologia aparece - “Model-Driven Design”, em que nosso modelo do mundo exterior é colocado em primeiro lugar.



O único local escolhido pela tecnologia é a Smart UI. Essa é uma camada entre o mundo exterior, o usuário e nós (uma referência a Robert Martin e sua arquitetura limpa com camadas). Como você pode ver, tudo vai para o modelo.

O que é um modelo? Essa é a dor fantasma de qualquer arquiteto. Todo mundo pensa que isso é UML, mas não é.
Um modelo é um conjunto de classes, métodos e links entre eles que refletem os cenários de negócios no programa.
O modelo reflete um objeto real com todas as propriedades e funções necessárias. Este é um kit de ferramentas de alto nível para tomar decisões do ponto de vista dos casos de negócios. Métodos e classes, no entanto, são um kit de ferramentas de baixo nível para soluções de arquitetura.

Pitão-seco


Para preencher o nicho do modelo, iniciei um projeto dry-python que se transformou em uma coleção de bibliotecas de arquitetura de alto nível para a construção de Model Driven Design. Cada uma das bibliotecas está tentando fechar um círculo na arquitetura e não interfere na outra. As bibliotecas podem ser usadas separadamente ou juntas, se você provar.



A sequência da narrativa corresponde à cronologia da adição ótima de DDD ao projeto - por camadas. A primeira camada é serviços , uma descrição dos cenários de negócios (processos) em nosso sistema. A biblioteca de histórias é responsável por essa camada.

Histórias


Os cenários de negócios são divididos em três partes:

  • especificação - uma descrição do processo de negócios;
  • O estado em que o cenário de negócios pode existir
  • implementação de cada etapa do script.

Essas peças não devem ser misturadas. A biblioteca Stories separa essas partes e desenha uma linha clara entre elas.

Considere a introdução de DDD e Stories com um exemplo. Por exemplo, temos um projeto no Django com uma mistura de sinais do Django e modelos "grossos" obscuros. Adicione um pacote de serviços vazio a ele. Usando a biblioteca Stories em partes, reescrevemos esse hash em um conjunto de scripts claro e compreensível em nosso projeto.

Especificação DSL. A biblioteca permite que você escreva uma especificação e fornece DSL para isso. Essa é uma maneira de descrever as ações do usuário passo a passo. Por exemplo, para comprar subscription, sigo várias etapas: vou encontrar um pedido, verificar a relevância do preço, verificar se o usuário pode pagar. Esta é uma descrição de alto nível.

Contrato.Abaixo desta classe, escreveremos um contrato para o estado do cenário de negócios. Para isso, denotamos a área de variáveis ​​que surgem no processo de negócios e, para cada variável, atribuímos um conjunto de validadores.

Assim que alguém tentar atribuir uma variável a essa área como parte do processo de negócios, um conjunto de validadores será elaborado. Garantiremos que o estado do processo no tempo de execução esteja sempre funcionando. Mas, se não, cai dolorosamente e grita bem alto.

Fase de implementação de cada etapa . Na mesma classe, subscriptionescrevemos um conjunto de métodos cujos nomes correspondem às etapas de negócios. Cada método de entrada recebe um estado com o qual pode trabalhar, mas não tem o direito de modificá-lo. O método pode retornar algum marcador e relatório:

  • , () ;
  • - , .


Existem marcadores mais complexos: eles podem confirmar que o estado está funcionando, sugerir excluir ou alterar algumas partes do processo de negócios. Você também pode escrever nas aulas.

História de lançamento. Como executar o Story na execução? Este é um objeto de negócios que funciona como um método: transferimos dados para a entrada, os valida, interpreta as etapas. A História em execução lembra o histórico de execução, registra o estado que ocorreu no processo de negócios e nos diz quem influenciou esse estado .

Barra de ferramentas de depuração. Se escrevermos no Django e usarmos o painel de depuração, podemos ver quais cenários de negócios foram processados ​​em cada solicitação e seu status.

Py.test. Se escrevermos em py.test, para o teste finalizado, podemos ver quais scripts de negócios foram executados em cada linha e o que deu errado. Isso é conveniente - em vez de escolher o código, lemos a especificação e entendemos o que aconteceu.



Sentinela. Melhor ainda, quando recebemos o erro 500. Em um sistema regular, toleramos e começamos a investigar. No Sentry, um relatório detalhado aparecerá sobre o que o usuário fez para cometer o erro. É conveniente e agradável quando, às 3 da manhã, essas informações foram coletadas para você.


ELK . Agora, estamos trabalhando ativamente em um plug-in que grava tudo isso no Elasticsearch na pilha Kibana e cria índices competentes.



Por exemplo, temos um contrato para o status de um processo de negócios. Sabemos o que há, por exemplo,relation IDrelatório. Em vez de pesquisas arcaicas do que aconteceu lá, escrevemos um pedido em Kibana. Ele mostrará todas as histórias relacionadas a um usuário específico. Em seguida, examinamos o estado em nossos processos e cenários de negócios. Não escrevemos uma única linha de código de registro, mas o projeto é registrado no mesmo nível de abstração no qual estamos interessados ​​em assistir.

Mas eu quero algo de nível superior, por exemplo, objetos leves. Esses objetos contêm estruturas e métodos de dados alfabetizados relacionados à adoção de decisões de negócios e não ao trabalho com o banco de dados, por exemplo. Portanto, passamos à próxima parte da arquitetura orientada a modelo - entidades, agregados e objetos de valor.



Entidades, agregados e objetos de valor


Como tudo isso está interconectado? Por exemplo, um usuário faz um pedido de produto e faturamos. Qual é a raiz da agregação e o que é um objeto simples?



Tudo o que é sublinhado é a raiz da agregação. É com isso que quero trabalhar diretamente: importante, valioso, holístico.

Por onde começar? Criaremos um pacote vazio no projeto, onde colocaremos nossas unidades. Os agregados são melhor escritos com algo declarativo, como dataclassesou attrs.

Dataclasses . Se indicarmos algum tipo de dataclassagregação, escreveremos uma anotação usando NewType . Na anotação, indicamos uma referência explícita, que é expressa no sistema de tipos. Se for dataclassapenas uma estrutura de dados (entidade), salve-a dentro do agregado.

No contexto de Histórias, apenas agregados podem mentir. O acesso a algo incorporado neles só pode ser obtido através de métodos públicos e regras de alto nível. Isso permite que você construa de maneira lógica e competente um modelo sobre o qual trabalhamos em conjunto com especialistas da área. Este é o mesmo idioma único .



O problema surge imediatamente - repositórios. Eu tenho um banco de dados com o qual trabalho através do Django, um microsserviço vizinho para o qual envio solicitações, existe JSON e uma instância do modelo Django. Para receber dados e transferi-los manualmente, apenas para chamar ou testar o método lindamente? Claro que não. O dry-python possui uma biblioteca Mappers que permite mapear abstrações de alto nível e agregações de domínio para os locais onde as armazenamos.

Mapeadores


Adicionamos mais um pacote ao nosso projeto - um repositório no qual armazenaremos o nosso mappers. É assim que transferiremos a lógica de negócios de alto nível para o mundo real.

Por exemplo, podemos descrever como mapeamos um dataclasspara um modelo do Django.

Django ORM. Comparamos o modelo de pedidos com a descrição do Django ORM - examinamos os campos.

Por exemplo, podemos reescrever alguns campos através da configuração opcional. O seguinte acontecerá: mapperdurante a declaração, ele comparará como o dataclassmodelo é escrito . Por exemplo, as anotações int( Order dataclassexiste um campo costcom anotação int) no modelo do Django correspondem integer fieldà opção nullable="true". Aqui ele vai dataclassoferecer para adicionar optionala dataclass, ou removernullablede field.

Através dos mapeadores, você pode adicionar funções que leem ou escrevem algo. Leitores são funções que recebem um agregado na entrada e retornam uma referência a ele. Os escritores fazem o oposto - as unidades de retorno. Sob o capô, por exemplo, pode haver uma solicitação ao banco de dados através do Django.

Definições do Swagger As mesmas operações podem ser realizadas com microsserviços. Você pode escrever uma parte do esquema de arrogância nelas e verificar quanto o esquema de arrogância de um serviço específico corresponde aos seus modelos de domínio. Além disso, a solicitação retornada da biblioteca Request será traduzida de forma transparente para dataclass.

Consultas GraphQL. GraphQL e microsserviços: o esquema de tipo de interface GraphQL funciona bem contradataclass. Você pode converter consultas específicas do GraphQL em estruturas de dados internas.

Por que se preocupar com o modelo de dados interno de alto nível dentro do aplicativo? Para ilustrar o "porquê", contarei uma história "divertida".

Em um de nossos projetos, os soquetes da Web funcionavam através do serviço Pusher. Nós não nos incomodamos, envolvemos em uma interface para não ligar diretamente. Essa interface foi vinculada a todas as histórias e foi satisfeita.

Mas os requisitos de negócios mudaram. Verificou-se que as garantias que o Pusher fornece para os soquetes da Web não são suficientes. Por exemplo, você precisa de entrega e histórico de mensagens garantidos nos últimos 2 minutos. Portanto, decidimos mudar para o serviço Ably Realtime. Ele também tem uma interface - vamos escrever um adaptador e amarrá-lo em todos os lugares, tudo será ótimo. Na verdade não.

As abstrações que o Pusher usa (argumentos da função) são capturadas em todos os objetos de negócios. Eu tive que consertar cerca de 100 histórias e consertar a formação do canal do usuário para o qual estamos enviando alguma coisa.

Voltar para os testes.

Testes e zombarias


Como você costuma testar esse comportamento com serviços externos? Estamos molhando algo, estamos assistindo como uma biblioteca de terceiros é chamada, e é tudo - temos certeza de que está tudo bem. Mas quando a biblioteca muda, os formatos dos argumentos também mudam.

Você pode economizar uma semana reescrevendo milhares de testes e centenas de casos de negócios se testar o comportamento do modelo interno de maneira diferente. Por exemplo, algo semelhante ao teste de integração: escrevemos no fluxo do usuário e, já dentro do adaptador, Pusher ou Ably, convertemos esse fluxo no nome do canal normal para não gravar tudo na lógica de negócios.

Dependências


Nessa arquitetura de modelo, muitas entidades supérfluas aparecem. Anteriormente, pegamos algum tipo de função do Django e a escrevemos: solicitação, resposta, movimentos mínimos do corpo. Aqui você precisa inicializar os Mapeadores, colocar Histórias e inicializar, processar a linha de solicitação da solicitação HTTP, ver qual resposta dar. Tudo isso resulta em 30-50 linhas de clichê chamando o código Stories dentro do Django-view.

Por outro lado, já escrevemos interfaces e mapeadores. Podemos verificar sua compatibilidade com um caso de negócios específico, por exemplo, usando a biblioteca Dependencies. Quão? Através do padrão de injeção de dependência, tudo é declaradamente colado a um padrão mínimo.

Aqui indicamos a tarefa de fazer uma aula no pacote de serviços, coloque trêsmappers, inicialize o objeto Stories e forneça-o. Com essa abordagem, o número de clichês no código é reduzido enormemente.

Mapa de refatoração


Usando tudo o que falei, desenvolvemos um esquema pelo qual reescrevemos um grande projeto a partir de sinais do Django (implícito "inferno de retorno de chamada") para o Django usando DDD.

O primeiro passo sem DDD . No começo, não tínhamos DDD - escrevemos MVP. Quando eles ganharam seu primeiro dinheiro, convidaram investidores e os convenceram a mudar para o DDD.

Histórias sem contratos. Dividimos o projeto em casos de negócios lógicos sem contratos de dados.

Contratos e agregados . Depois, um por um, arrastamos o contrato de dados para cada modelo que pode ser rastreado em nossa arquitetura.

Mapeadores . Os mapeadores escreveram para se livrar dos modelos de data warehouse.

Injeção de dependência . Livre-se dos padrões de colagem.

Se o seu projeto superou o MVP e precisa urgentemente ser alterado na arquitetura para que não caia no legado - observe o DDD.

legacy Python-, , , Moscow Python Conf++ 27 . Python . unconference, , , , Drylabs.

DDD Python, TechLead ConfIT-, DDD . 8 , Call for Papers 6 .

All Articles