Livro de receitas do desenvolvedor: Receitas DDD (parte 4, estruturas)

1. Introdução


Então, nós já decidimos sobre o escopo , metodologia e arquitetura . Vamos passar da teoria para a prática, para escrever código. Gostaria de começar com padrões de design que descrevam a lógica de negócios - Service and Interactor . Mas antes de embarcar neles, examinaremos os padrões estruturais - ValueObject e Entity . Vamos desenvolver na linguagem ruby . Em outros artigos, analisaremos todos os padrões necessários para o desenvolvimento usando a Arquitetura Variável . Todos os desenvolvimentos que são aplicativos para esta série de artigos serão coletados em uma estrutura separada.


Blacjack & hockers


E já escolhemos um nome adequado - LunaPark .
Desenvolvimentos atuais publicados no Github .
Depois de examinar todos os modelos, montaremos um microsserviço completo.


Tão historicamente


Havia uma necessidade de refatorar um aplicativo corporativo complexo escrito em Ruby on Rails. Havia uma equipe pronta de desenvolvedores de ruby. A metodologia de desenvolvimento orientado a domínio foi perfeita para essas tarefas, mas não havia solução pronta na linguagem usada. Apesar do fato de a escolha do idioma ter sido determinada principalmente por nossa especialização, ela se mostrou bastante bem-sucedida. Entre todos os idiomas que são comumente usados ​​para aplicativos da web, o ruby ​​é, na minha opinião, o mais expressivo. E, portanto, mais do que outros adequados para modelar objetos reais. Esta não é apenas a minha opinião.


Esse é o mundo Java. Então você tem os novatos como Ruby. O Ruby tem uma sintaxe muito expressiva e, nesse nível básico, deve ser uma linguagem muito boa para o DDD (embora eu ainda não tenha ouvido falar de muito uso real nesse tipo de aplicativo). O Rails gerou muita empolgação, porque finalmente parece facilitar a criação de UIs da Web, como eram no início dos anos 90, antes da Web. No momento, esse recurso foi aplicado principalmente para a construção de um grande número de aplicativos da Web que não possuem muita riqueza de domínio, uma vez que mesmo esses eram dolorosamente difíceis no passado. Mas minha esperança é que, à medida que a parte do problema de implementação da interface do usuário seja reduzida, as pessoas vejam isso como uma oportunidade de concentrar mais sua atenção no domínio. Se o uso do Ruby começar a seguir nessa direção, acho que poderia fornecer uma excelente plataforma para DDD. (Algumas peças de infraestrutura provavelmente teriam que ser preenchidas.)

Eric Evans 2006

Infelizmente, nos últimos 13 anos, nada mudou muito. Na Internet, você pode encontrar tentativas de adaptar o Rails para isso, mas todas parecem terríveis. A estrutura do Rails é pesada, lenta e não sólida. É muito difícil assistir sem lágrimas como alguém está tentando descrever a implementação do padrão de repositório com base no ActiveRecord . Decidimos adotar um microframework e modificá-lo de acordo com nossas necessidades. Tentamos o Grape , a ideia com a documentação automática parecia bem-sucedida, mas, caso contrário, foi abandonada e rapidamente abandonamos a ideia de usá-la. E quase imediatamente eles começaram a usar outra solução - Sinatra . Ainda continuamos a usá-lo para controladores e pontos finais REST.


REST?

Se você desenvolveu aplicativos da web, já tem uma idéia sobre a tecnologia. Ele tem seus prós e contras, cuja lista completa está além do escopo deste artigo. Mas para nós, como desenvolvedores de aplicativos corporativos, a desvantagem mais importante será que o REST (isso fica claro até pelo nome) reflete não o processo, mas seu estado. E a vantagem é sua compreensão - a tecnologia é clara para desenvolvedores de back-end e desenvolvedores de front-end.
Mas talvez não se concentre no REST, mas implemente sua solução http + json? Mesmo que você consiga desenvolver sua API de serviço, fornecendo sua descrição a terceiros, você receberá muitas perguntas. Muito mais do que se você fornecer o REST familiar.
Consideraremos o uso do REST como uma solução de compromisso. Usamos o JSON para concisão e o padrão jsonapi para não desperdiçar o tempo dos desenvolvedores em guerras sagradas em relação ao formato da solicitação.
No futuro, quando analisarmos o Endpoint , veremos que, para se livrar do descanso, basta reescrever apenas uma classe. Portanto, o REST não deve incomodar se houver dúvidas.


Ao escrever vários microsserviços, ganhamos bases - um conjunto de classes abstratas. Cada classe pode ser escrita em meia hora; seu código é fácil de entender se você souber para que serve esse código.


Aqui surgiram as principais dificuldades. Novos funcionários que não lidaram com práticas de DDD e arquitetura limpa não conseguiram entender o código e sua finalidade. Se eu mesmo visse esse código pela primeira vez antes de ler Evans, consideraria o legado como um excesso de engenharia.


Para superar esse obstáculo, decidiu-se escrever uma documentação (diretriz) que descreve a filosofia das abordagens utilizadas. Os contornos desta documentação pareciam bem-sucedidos e foi decidido publicá-los em Habré. Nas aulas abstratas repetidas de um projeto para outro, foi decidido colocar uma gema separada.


Filosofia


maneira legada
Se você se lembra de algum filme clássico sobre artes marciais, haverá um cara legal que está habilmente lidando com um poste. Um sexto é essencialmente um graveto, uma ferramenta muito primitiva, uma das primeiras que caiu em mãos humanas. Mas nas mãos do mestre, ele se torna uma arma formidável.
Você pode gastar tempo criando uma pistola que não atira em sua perna ou pode aprender a técnica de tiro. Identificamos 4 princípios básicos:


  • Você precisa simplificar as coisas complexas.
  • O conhecimento é mais importante que a tecnologia. A documentação é mais compreensível para uma pessoa do que o código; um não deve substituir um pelo outro.
  • O pragmatismo é mais importante que o dogmatismo. Os padrões devem orientar o caminho, não definir uma caixa delimitadora.
  • Estruturalidade na arquitetura, flexibilidade na escolha das soluções.

Uma filosofia semelhante pode ser rastreada, por exemplo, no ArchLinux OS - The Arch Way . No meu laptop, o Linux não foi criado por muito tempo, mais cedo ou mais tarde ele quebrou e eu constantemente tive que reinstalá-lo. Isso causou vários problemas, às vezes graves, como interrupção do prazo para o trabalho. Mas, depois de passar 2-3 dias depois de instalar o Arch, descobri como meu sistema operacional funciona. Depois disso, ela começou a trabalhar mais estável, sem falhas. Minhas anotações me ajudaram a instalá-lo em novos PCs em algumas horas. Uma documentação abundante me ajudou a resolver novos problemas.


A estrutura tem um caráter absolutamente de alto nível. As classes que o descrevem são responsáveis ​​pela estrutura do aplicativo. Soluções de terceiros são usadas para interagir com bancos de dados, implementar o protocolo http e outras coisas de baixo nível. Gostaríamos que o programador espiasse o código e entendesse como uma classe específica funciona, e a documentação nos permitiria entender como gerenciá-los. Compreender o design do motor não permitirá que você dirija um carro.


Enquadramento


É difícil chamar o LunaPark de estrutura no sentido usual. Quadro - quadro, Trabalho - trabalho. Pedimos que não nos limitemos ao escopo. O único quadro que declaramos é aquele que diz à classe na qual essa ou aquela lógica deve ser descrita. É um conjunto de ferramentas com instruções abrangentes para eles.
Cada classe é abstrata e possui três níveis:


module LunaPark #  module Forms #  class Single # / end end end 

Se você deseja implementar um formulário que cria um único elemento, você herda desta classe:


 module Forms class Create < LunaPark::Forms::Single 

Se houver vários elementos, usaremos outra Implementação .


 module Forms class Create < LunaPark::Forms::Multiple 

No momento, nem todos os desenvolvimentos foram colocados em perfeita ordem e a gema está em estado alfa. O citaremos em etapas, de acordo com a publicação dos artigos. I.e. se você ValueObject um artigo sobre ValueObject e Entity , esses dois modelos já estão implementados. Até o final do ciclo, todos eles serão adequados para uso no projeto. Como o próprio framework é de pouca utilidade sem um link para sinatra \ roda, será criado um repositório separado que mostra como "estragar tudo" para iniciar rapidamente seu projeto.


A estrutura é principalmente um aplicativo para a documentação. Não perceba esses artigos como documentação para a estrutura.


Então, vamos ao que interessa.


Objeto Valor


- Qual a altura da sua namorada?
- 151
- Você começou a se encontrar com a estátua da liberdade?

Algo assim poderia ter acontecido em Indiana. O crescimento humano não é apenas um número, mas também uma unidade de medida. Nem sempre os atributos de um objeto podem ser descritos apenas por primitivas (Inteiro, String, Booleano etc.), às vezes são necessárias combinações deles:


  • Dinheiro não é apenas um número, é um número (valor) + moeda.
  • Uma data consiste em um dia, um mês e um ano.
  • Para medir o peso, um único número não é suficiente para nós, também requer uma unidade de medida.
  • O número do passaporte consiste em uma série e, de fato, no número.

Por outro lado, isso nem sempre é uma combinação, talvez seja um tipo de extensão do primitivo.
Um número de telefone geralmente é considerado um número. Por outro lado, é improvável que ele tenha um método de adição ou divisão. Talvez exista um método que emita um código de país e um método que defina um código de cidade. Talvez haja um certo método decorativo que o apresentará não apenas como uma sequência de números 79001231212 , mas como uma sequência legível: 7-900-123-12-12 .


talvez um decorador?

Baseado no dogma, é indiscutível - sim. Se abordarmos esse dilema por parte do senso comum, quando decidirmos ligar para esse número, transferiremos o próprio objeto para o telefone:


 phone.call Values::PhoneNumber.new(79001231212) 

E se decidimos apresentá-lo como uma sequência, isso é claramente feito para uma pessoa. Então, por que não tornamos essa linha legível para uma pessoa imediatamente?


 Values::PhoneNumber.new(79001231212).to_s 

Imagine que estamos criando o site de cassino online dos Três Machados e vendendo jogos de cartas. Vamos precisar da classe 'baralho'.


 module Values class PlayingCard < Lunapark::Values::Compound attr_reader :suit, :rank end end 

Portanto, nossa classe possui dois atributos somente leitura:


  • naipe - naipe de cartão
  • rank - dignidade do cartão

Esses atributos são definidos apenas ao criar um mapa e não podem ser alterados ao usá-lo. Claro que você pode pegar uma carta de baralho e riscar 8 , escrever Q, mas isso é inaceitável. Em uma sociedade decente, você provavelmente será baleado. A incapacidade de alterar atributos após a criação do objeto determina a primeira propriedade do objeto de valor - imutabilidade.
A segunda propriedade importante do objeto de valor será como os comparamos.


 module Values RSpec.describe PlayingCard do let(:card) { described_class.new suit: :clubs, rank: 10 } let(:other) { described_class.new suit: :clubs, rank: 10 } it 'should be eql' do expect(card).to eq other end end end 

Esse teste falhará, pois eles serão comparados no endereço. Para que o teste seja aprovado, precisamos comparar o valor-objeto por valor; para isso, adicionaremos um método de comparação:


 def ==(other) suit == other.suit && rank == other.rank end 

Agora nosso teste passará. Também podemos adicionar métodos responsáveis ​​pela comparação, mas como comparamos 10 e K? Como você provavelmente já adivinhou, também os apresentaremos na forma de Objetos de Valor . Ok, agora teremos que iniciar os dez melhores clubes como este:


 ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) ten_clubs = Values::PlayingCards.new(rank: ten, clubs: clubs) 

Três linhas são suficientes para rubi. Para contornar essa limitação, introduzimos a terceira propriedade do Objeto-Valor - rotatividade. Vamos .wrap um método especial da classe .wrap , que pode pegar valores de vários tipos e convertê-los para o correto.


 class PlayingCard < Lunapark::Values::Compound def self.wrap(obj) case obj.is_a? self.class #      PlayingCard obj #      case obj.is_a? Hash #    ,      new(obj) #    case obj.is_a String #    ,     new rank: obj[0..-2], suit:[-1] # ,  -  . else #       raise ArgumentError #  . end end def initialize(suit:, rank:) #     @suit = Suit.wrap(suit) #      @rank = Rank.wrap(rank) end end 

Essa abordagem oferece uma grande vantagem:


 ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) from_values = Values::PlayingCard.wrap rank: ten, suit: clubs from_hash = Values::PlayingCard.wrap rank: '10', suit: :clubs from_obj = Values::PlayingCard.wrap from_values from_str = Values::PlayingCard.wrap '10C' #        utf ,  ,  . 

Todos esses cartões serão iguais um ao outro. Se o método wrap expandir em boas práticas, colocá-lo em uma classe separada será. Do ponto de vista da abordagem dogmática, uma classe separada também será obrigatória.
Hmm, e o espaço no convés? Como descobrir se este cartão é um trunfo? Este não é um cartão de jogo. Este é o valor da carta de baralho. Esta é exatamente a inscrição 10 que você lidera no canto do papelão.
É necessário relacionar-se com o valor-objeto , bem como com o primitivo, que por algum motivo não foi implementado em ruby. A partir daqui, a última propriedade surge - o Object-Value não está vinculado a nenhum domínio.


Recomendações


Entre toda a variedade de métodos e ferramentas usadas em todos os momentos de cada processo, há sempre um método e ferramenta que funciona mais rápido e melhor que outros.

Frederick Taylor 1914

Operações aritméticas devem retornar um novo objeto

 # GOOD class Money < LunaPark::Values::Compound def +(other) other = self.class.wrap(other) raise ArgumentError unless same_currency? other self.class.new( amount: amount + other.amount, currency: currency ) end end 

Os atributos de um objeto de valor podem ser apenas primitivos ou outros objetos de valor

 # GOOD class Weight < LunaPark::Values::Compound def intialize(value:, unit:) @value = value @unit = Unit.wrap(unit) end end # BAD class PlaingCard < LunaPark::Value def initialize(rank:, suit:, deck:) ... @deck = Entity::Deck.wrap(deck) #    end end 

Mantenha operações simples dentro dos métodos de classe

 # GOOD class Weight < LunaPark::Values::Compound def >(other) value > other.convert_to(unit).value end end 

Se a operação de "conversão" for grande, talvez faça sentido movê-la para uma classe separada

 # UGLY class Weight < LunaPark::Values::Compound def convert_to(unit) unit = Unit.wrap(unit) case { self.unit.to_sym => unit.to_sym } when { :kg => :ft } Weight.new(value: 2.2046 * value, unit.to_sym) when # ... end end end # GOOD #./lib/values/weight/converter.rb class Weight class Converter < LunaPark::Services::Simple def initialize(weight, to:) ... end end end #./lib/values/weight.rb class Weight < LunaPark::Values::Compound def convert_to(unit) Converter.call! self, to: unit end end 

Essa remoção da lógica em um Serviço separado é possível apenas com a condição de que o Serviço seja isolado: ele não usa dados de fontes externas. Este serviço deve ser limitado pelo contexto do próprio objeto Value .


O valor do objeto não pode saber nada sobre lógica de domínio

Suponha que estamos escrevendo uma loja on-line e que tenhamos uma classificação de mercadorias. Para obtê-lo, você precisa fazer uma solicitação ao banco de dados através do Repositório .


 # DEADLY BAD class Rate < LunaPark::Values::Single def top?(10) Repository::Rates.top(first: 10).include? self end end 

Entidade


A classe Entity é responsável por algum objeto real. Pode ser um contrato, uma cadeira, um agente imobiliário, uma torta, um ferro, um gato, uma geladeira - qualquer coisa. Qualquer objeto que você precise modelar seus processos de negócios é uma Entidade .
O conceito de Entidade é diferente para Evans e Martin. Do ponto de vista de Evans, uma entidade é um objeto caracterizado por algo que enfatiza sua individualidade.


Essence by Zvans
Se um objeto é determinado por uma existência individual única, e não por um conjunto de atributos, essa propriedade deve ser lida como a principal ao definir um objeto em um modelo. A definição da classe deve ser simples e construída em torno da continuidade e singularidade do ciclo de existência do objeto. Encontre uma maneira de distinguir cada objeto, independentemente de sua forma ou história de existência. Preste atenção especial aos requisitos técnicos associados à comparação de objetos de acordo com seus atributos. Defina uma operação que necessariamente daria um resultado único para cada objeto - talvez seja necessário associar um determinado símbolo à exclusividade garantida para isso. Esse meio de identificação pode ter uma origem externa, mas também pode ser um identificador arbitrário gerado pelo sistema para sua própria conveniência. No entanto, essa ferramenta deve obedecer às regras para distinguir entre objetos no modelo. O modelo deve fornecer uma definição exata do que são objetos idênticos.

Do ponto de vista de Martin, Entidade não é um objeto, mas uma camada. Essa camada combinará o objeto e a lógica de negócios para alterá-lo.


Desolação de Martin
Minha opinião sobre as entidades é que elas contêm regras de negócios independentes de aplicativos. Eles não são simplesmente objetos de dados. Eles podem conter referências a objetos de dados; mas seu objetivo é implementar métodos de regras de negócios que possam ser usados ​​por muitos aplicativos diferentes.

Gateways retornam Entidades. A implementação (abaixo da linha) busca os dados do banco de dados e os utiliza para construir estruturas de dados que são passadas às Entidades. Isso pode ser feito com contenção ou herança.

Por exemplo:

classe pública MyEntity {private MyDataStructure data;}

ou

classe pública MyEntity estende MyDataStructure {...}

E lembre-se, somos todos piratas por natureza; e as regras que estou falando aqui são realmente mais como diretrizes ...

Por essência, queremos dizer apenas estrutura. Na sua forma mais simples, a classe Entity ficará assim:


 module Entities class MeatBag < LunaPark::Entities::Simple attr_accessor :id, :name, :hegiht, :weight, :birthday end end 

Um objeto mutável que descreve a estrutura de um modelo de negócios pode conter tipos e valores primitivos.
A LunaPark::Entites::Simple é incrivelmente simples, você pode ver o seu código, isso nos dá apenas uma coisa - fácil inicialização.


LunaPark :: Entites :: Simples
 module LunaPark module Entities class Simple def initialize(params) set_attributes params end private def set_attributes(hash) hash.each { |k, v| send(:"#{k}=", v) } end end end end 

Você pode escrever:


 john_doe = Entity::MeatBag.new( id: 42, name: 'John Doe', height: '180cm', weight: '80kg', birthday: '01-01-1970' ) 

Como você provavelmente já adivinhou, queremos incluir o peso, a altura e a data de nascimento nos objetos de valor .


 module Entities class MeatBag < LunaPark::Entites::Simple attr_accessor :id, :name attr_reader :heiht, :wight, :birthday def height=(height) @height = Values::Height.wrap(height) end def weight=(height) @height = Values::Weight.wrap(weight) end def birthday=(day) @birthday = Date.parse(day) end end end 

Para não perder tempo com esses construtores, preparamos uma implementação mais complexa do LunaPark::Entites::Nested :


 module Entities class MeatBag < LunaPark::Entities::Nested attr :id attr :name attr :heiht, Values::Height, :wrap attr :weight, Values::Weight, :wrap attr :birthday, Values::Date, :parse end end 

Como o nome sugere, esta Implementação permite criar estruturas em árvore.


Vamos satisfazer minha paixão por eletrodomésticos volumosos. Em um artigo anterior, traçamos uma analogia entre o "toque" de uma máquina de lavar e a arquitetura . E agora descreveremos um objeto de negócios tão importante como uma geladeira:


Refregerator


 class Refregerator < LunaPark::Entites::Nested attr :id, attr :brand attr :title namespace :fridge do namespace :door do attr :upper, Shelf, :wrap attr :lower, Shelf, :wrap end attr :upper, Shelf, :wrap attr :lower, Shelf, :wrap end namespace :main do namespace :door do attr :first, Shelf, :wrap attr :second, Shelf, :wrap attr :third, Shelf, :wrap end namespace :boxes do attr :left, Box, :wrap attr :right, Box, :wrap end attr :first, Shelf, :wrap attr :second, Shelf, :wrap attr :third, Shelf, :wrap attr :fourth, Shelf, :wrap end attr :last_open_at, comparable: false end 

Essa abordagem evita a criação de entidades desnecessárias, como a porta da geladeira. Sem uma geladeira, ela deve fazer parte da geladeira. Essa abordagem é conveniente para a compilação de documentos relativamente grandes, por exemplo, um aplicativo para a compra de seguros.


A LunaPark::Entites::Nested possui mais 2 propriedades importantes:


Comparabilidade:


 module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at end end u1 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now) u2 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now) u1 == u2 # => false 

Os dois usuários especificados não são equivalentes, porque eles foram criados em momentos diferentes e, portanto, o valor do atributo registred_at será diferente. Mas se cruzarmos o atributo da lista de comparados:


 module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at, comparable: false end end 

então temos dois objetos comparáveis.


Essa implementação também tem a propriedade de rotatividade - podemos usar o método de classe


 Entites::User.wrap(email: 'john.doe@mail.com', registred_at: Time.now) 

Você pode usar o Hash, o OpenStruct ou qualquer outra jóia que desejar como Entidade , o que o ajudará a realizar a estrutura da sua entidade.


Uma entidade é um modelo de um objeto de negócios, deixe simples. Se alguma propriedade não for usada pela sua empresa, não a descreva.


Alterações de entidade


Como você notou, a classe Entity não possui métodos de sua própria alteração. Todas as alterações são feitas de fora. O objeto de valor também é imutável. Todas as funções que nela estão presentes, em geral, decoram a essência ou criam novos objetos. A própria essência permanece inalterada. Para um desenvolvedor Ruby on Rails, essa abordagem será incomum. Do lado de fora, pode parecer que geralmente usamos a linguagem OOP para outra coisa. Mas se você olhar um pouco mais fundo - não é assim. Uma janela pode abrir sozinha? Consiga um carro para trabalhar, reserve um hotel, gato fofo, consiga um novo assinante? Todas essas são influências externas. Algo acontece no mundo real, e nós refletimos isso em nós mesmos. Para cada solicitação, fazemos alterações em nosso modelo. E, assim, mantemo-lo atualizado, suficiente para nossas tarefas comerciais. É necessário separar o estado do modelo e os processos que causam alterações nesse estado. Como fazer isso, consideraremos no próximo artigo.

Source: https://habr.com/ru/post/pt435920/


All Articles