Developer Cookbook: DDD Recipes (Part 4, Structures)

Introduccion


Entonces, ya hemos decidido el alcance , la metodología y la arquitectura . Pasemos de la teoría a la práctica, a escribir código. Me gustaría comenzar con patrones de diseño que describan la lógica de negocios: Servicio e Interactor . Pero antes de embarcarnos en ellos, examinaremos los patrones estructurales: ValueObject y Entity . Nos desarrollaremos en lenguaje rubí . En otros artículos, analizaremos todos los patrones necesarios para el desarrollo utilizando la Arquitectura Variable . Todos los desarrollos que son aplicaciones para esta serie de artículos se recopilarán en un marco separado.


Blacjack y hockers


Y ya hemos elegido un nombre adecuado: LunaPark .
Desarrollos actuales publicados en Github .
Habiendo examinado todas las plantillas, ensamblaremos un microservicio completo.


Tan históricamente


Era necesario refactorizar una aplicación empresarial compleja escrita en Ruby on Rails. Había un equipo de desarrolladores de rubíes listo para usar. La metodología de Desarrollo Dirigido por Dominio fue perfecta para estas tareas, pero no había una solución llave en mano en el lenguaje utilizado. A pesar de que la elección del idioma estuvo determinada principalmente por nuestra especialización, resultó ser bastante exitosa. Entre todos los idiomas que se usan comúnmente para aplicaciones web, ruby, en mi opinión, es el más expresivo. Y por lo tanto, más que otros adecuados para modelar objetos reales. Esta no es solo mi opinión.


Ese es el mundo de Java. Luego tienes a los recién llegados como Ruby. Ruby tiene una sintaxis muy expresiva, y en este nivel básico debería ser un muy buen lenguaje para DDD (aunque aún no he oído hablar de mucho uso real en ese tipo de aplicaciones). Rails ha generado mucha emoción porque finalmente parece hacer que la creación de interfaces de usuario web sea tan fácil como lo fueron a principios de la década de 1990, antes de la web. En este momento, esta capacidad se ha aplicado principalmente a la creación de una gran cantidad de aplicaciones web que no tienen mucha riqueza de dominio detrás de ellas, ya que incluso estas han sido dolorosamente difíciles en el pasado. Pero mi esperanza es que, a medida que se reduce la parte de la implementación de la interfaz de usuario del problema, la gente vea esto como una oportunidad para enfocar más su atención en el dominio. Si el uso de Ruby comienza a ir en esa dirección, creo que podría proporcionar una excelente plataforma para DDD. (Algunas piezas de infraestructura probablemente tendrían que rellenarse).

Eric Evans 2006

Desafortunadamente, en los últimos 13 años, nada ha cambiado mucho. En Internet puedes encontrar intentos de adaptar Rails para esto, pero todos parecen terribles. El marco de Rails es pesado, lento y no SÓLIDO. Es muy difícil ver sin lágrimas cómo alguien está tratando de representar la implementación del patrón del Repositorio sobre la base de ActiveRecord . Decidimos adoptar un microframework y modificarlo según nuestras necesidades. Intentamos con Grape , la idea con documentación automática parecía exitosa, pero por lo demás fue abandonada y rápidamente abandonamos la idea de usarla. Y casi de inmediato comenzaron a usar otra solución: Sinatra . Seguimos usándolo para controladores REST y puntos finales .


DESCANSO?

Si desarrolló aplicaciones web, entonces ya tiene una idea sobre la tecnología. Tiene sus pros y sus contras, una lista completa de los cuales está más allá del alcance de este artículo. Pero para nosotros, como desarrolladores de aplicaciones empresariales, el inconveniente más importante será que REST (esto está claro incluso por el nombre) no refleja el proceso, sino su estado. Y la ventaja es su comprensibilidad: la tecnología es clara tanto para los desarrolladores back-end como para los front-end.
¿Pero entonces quizás no se centre en REST, sino que implemente su solución http + json? Incluso si logra desarrollar su API de servicio, luego de proporcionar su descripción a terceros, recibirá muchas preguntas. Mucho más que si proporciona el REST familiar.
Consideraremos el uso de REST como una solución de compromiso. Utilizamos JSON por concisión y el estándar jsonapi para no perder el tiempo de los desarrolladores en guerras santas con respecto al formato de solicitud.
En el futuro, cuando analicemos Endpoint , veremos que para deshacerse del descanso, es suficiente reescribir solo una clase. Por lo tanto, REST no debería molestar en absoluto si hay dudas al respecto.


En el curso de la escritura de varios microservicios, hemos ganado una base: un conjunto de clases abstractas. Cada clase se puede escribir en media hora, su código es fácil de entender si sabe para qué sirve este código.


Aquí surgieron las principales dificultades. Los nuevos empleados que no se ocuparon de las prácticas DDD y la arquitectura limpia no pudieron entender el código y su propósito. Si yo mismo viera este código por primera vez antes de leer Evans, lo tomaría como legado, sobre ingeniería.


Para superar este obstáculo, se decidió escribir una documentación (guía) que describa la filosofía de los enfoques utilizados. Los esquemas de esta documentación parecían exitosos y se decidió ponerlos en Habré. Clases abstractas que se repitieron de proyecto a proyecto, se decidió poner en una gema separada.


Filosofía


camino heredado
Si recuerdas alguna película clásica sobre artes marciales, habrá un tipo genial que maneja muy hábilmente un palo. Un sexto es esencialmente un palo, una herramienta muy primitiva, una de las primeras que cayó en manos humanas. Pero en manos del maestro, se convierte en un arma formidable.
Puedes pasar tiempo creando una pistola que no te dispare en la pierna, o puedes pasar tiempo aprendiendo la técnica de tiro. Hemos identificado 4 principios básicos:


  • Necesitas simplificar las cosas complejas.
  • El conocimiento es más importante que la tecnología. La documentación es más comprensible para una persona que el código; uno no debe reemplazar uno por otro.
  • El pragmatismo es más importante que el dogmatismo. Las normas deben guiar el camino, no establecer un cuadro delimitador.
  • Estructuralidad en arquitectura, flexibilidad en la elección de soluciones.

Se puede rastrear una filosofía similar, por ejemplo, en ArchLinux OS - The Arch Way . En mi computadora portátil, Linux no se arraigó durante mucho tiempo, tarde o temprano se rompió y tuve que reinstalarlo constantemente. Esto causó una serie de problemas, a veces graves, como la interrupción del plazo para el trabajo. Pero después de pasar 2-3 días una vez que instalé Arch, descubrí cómo funciona mi sistema operativo. Después de eso, ella comenzó a trabajar más estable, sin fallas. Mis notas me ayudaron a instalarlo en nuevas PC en un par de horas. Una abundante documentación me ayudó a resolver nuevos problemas.


El marco tiene un carácter absolutamente de alto nivel. Las clases que lo describen son responsables de la estructura de la aplicación. Las soluciones de terceros se utilizan para interactuar con bases de datos, implementar el protocolo http y otras cosas de bajo nivel. Nos gustaría que el programador eche un vistazo al código y entienda cómo funciona esta o aquella clase, y la documentación nos permitirá comprender cómo administrarlos. Comprender el diseño del motor no le permitirá conducir un automóvil.


Marco


Es difícil llamar a LunaPark un marco en el sentido habitual. Marco - marco, Trabajo - trabajo. Instamos a no limitarnos al alcance. El único marco que declaramos es el que le dice a la clase en la que se debe describir esta o aquella lógica. Es más bien un conjunto de herramientas con extensas instrucciones para ellos.
Cada clase es abstracta y tiene tres niveles:


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

Si desea implementar un formulario que crea un único elemento, hereda de esta clase:


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

Si hay varios elementos, utilizaremos otra implementación .


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

Por el momento, no todos los desarrollos se han puesto en perfecto orden y la gema está en estado alfa. Lo citaremos por etapas, de acuerdo con la publicación de artículos. Es decir Si ve un artículo sobre ValueObject y Entity , estas dos plantillas ya están implementadas. Al final del ciclo, todos serán adecuados para su uso en el proyecto. Dado que el marco en sí es de poca utilidad sin un enlace a sinatra \ roda, se creará un repositorio separado que muestra cómo "arruinar todo" para comenzar rápidamente su proyecto.


El marco es principalmente una aplicación a la documentación. No perciba estos artículos como documentación para el marco.


Entonces, vamos al grano.


Objeto de valor


- ¿Qué altura tiene tu novia?
- 151
- ¿Comenzaste a encontrarte con la estatua de la libertad?

Algo como esto podría haber sucedido en Indiana. El crecimiento humano no es solo un número, sino también una unidad de medida. No siempre los atributos de un objeto se pueden describir solo mediante primitivas (Entero, Cadena, Booleano, etc.), a veces se requieren combinaciones de ellos:


  • El dinero no es solo un número, es un número (cantidad) + moneda.
  • Una fecha consta de un día, un mes y un año.
  • Para medir el peso, un solo número no es suficiente para nosotros, también requiere una unidad de medida.
  • El número de pasaporte consta de una serie y, de hecho, del número.

Por otro lado, esto no siempre es una combinación, quizás es una especie de extensión de lo primitivo.
Un número de teléfono a menudo se toma como un número. Por otro lado, es poco probable que tenga un método de adición o división. Quizás haya un método que emita un código de país y un método que defina un código de ciudad. Quizás haya un cierto método decorativo que lo presente no solo como una cadena de números 79001231212 , sino como una cadena legible: 7-900-123-12-12 .


tal vez un decorador?

Basado en el dogma, es indiscutible, sí. Si abordamos este dilema por parte del sentido común, cuando decidamos llamar a este número, transferiremos el objeto al teléfono:


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

Y si decidimos presentarlo como una cadena, entonces esto se hace claramente para una persona. Entonces, ¿por qué no hacemos que esta línea sea legible para una persona de inmediato?


 Values::PhoneNumber.new(79001231212).to_s 

Imagine que estamos creando el sitio de casino en línea Three Axes y vendiendo juegos de cartas. Necesitaremos la clase de 'naipes'.


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

Entonces, nuestra clase tiene dos atributos de solo lectura:


  • traje - traje de tarjeta
  • rango - dignidad de la tarjeta

Estos atributos se establecen solo cuando se crea un mapa y no pueden cambiar cuando se usa. Por supuesto, puede tomar una tarjeta de juego y tachar 8 , escribir Q, pero esto es inaceptable. En una sociedad decente, lo más probable es que te disparen. La imposibilidad de cambiar los atributos después de crear el objeto determina la primera propiedad del Objeto de valor : la inmutabilidad.
La segunda propiedad importante del objeto de valor será cómo los 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 

Tal prueba fallará, ya que se compararán en la dirección. Para que la prueba pase, debemos comparar Value-Obects por valor, para esto agregaremos un método de comparación:


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

Ahora nuestra prueba pasará. También podemos agregar métodos que son responsables de la comparación, pero ¿cómo comparamos 10 y K? Como probablemente ya haya adivinado, también los presentaremos en forma de objetos de valor . Ok, ahora tendremos que iniciar los diez mejores clubes como este:


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

Tres líneas son suficientes para el rubí. Para evitar esta limitación, introducimos la tercera propiedad del Objeto-Valor : la rotación. Tengamos un método especial de la clase .wrap , que puede tomar valores de varios tipos y convertirlos al correcto.


 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 

Este enfoque ofrece una gran ventaja:


 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 ,  ,  . 

Todas estas cartas serán iguales entre sí. Si el método de wrap se expande a una buena práctica, se colocará en una clase separada. Desde el punto de vista del enfoque dogmático, una clase separada también será obligatoria.
Hmm, ¿qué pasa con el espacio en la cubierta? ¿Cómo saber si esta carta es una carta de triunfo? Esta no es una carta de juego. Este es el valor de la tarjeta de juego. Esta es exactamente la inscripción 10 que llevas en la esquina del cartón.
Es necesario relacionarse tanto con el Objeto-Valor como con el primitivo, que por alguna razón no se implementó en ruby. De aquí surge la última propiedad: Object-Value no está vinculado a ningún dominio.


Recomendaciones


Entre toda la variedad de métodos y herramientas utilizados en cada momento de cada proceso, siempre hay un método y herramienta que funciona más rápido y mejor que otros.

Frederick Taylor 1914

Las operaciones aritméticas deben devolver un nuevo 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 

Los atributos de un objeto de valor solo pueden ser primitivos u otros 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 

Mantenga operaciones simples dentro de los métodos de clase.

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

Si la operación de "conversión" es grande, entonces quizás tenga sentido moverla a una clase 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 

Tal eliminación de la lógica en un Servicio separado solo es posible con la condición de que el Servicio esté aislado: no utiliza datos de ninguna fuente externa. Este servicio debe estar limitado por el contexto del objeto de valor en sí.


El valor del objeto no puede saber nada sobre la lógica del dominio

Supongamos que estamos escribiendo una tienda en línea y tenemos una calificación de productos. Para obtenerlo, debe realizar una solicitud a la base de datos a través del Repositorio .


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

Entidad


La clase de entidad es responsable de algún objeto real. Puede ser un contrato, una silla, un agente de bienes raíces, un pastel, una plancha, un gato, un refrigerador, cualquier cosa. Cualquier objeto que pueda necesitar para modelar sus procesos comerciales es una Entidad .
El concepto de entidad es diferente para Evans y para Martin. Desde el punto de vista de Evans, una entidad es un objeto caracterizado por algo que enfatiza su individualidad.


Esencia de Zvans
Si un objeto está determinado por una existencia individual única, y no por un conjunto de atributos, esta propiedad debe leerse como la principal al definir un objeto en un modelo. La definición de la clase debe ser simple y construida alrededor de la continuidad y unicidad del ciclo de la existencia del objeto. Encuentre una manera de distinguir cada objeto independientemente de su forma o historia de existencia. Preste especial atención a los requisitos técnicos asociados con la comparación de objetos de acuerdo con sus atributos. Defina una operación que necesariamente dé un resultado único para cada objeto; puede ser necesario asociar un cierto símbolo con unicidad garantizada para esto. Tal medio de identificación puede tener un origen externo, pero también puede ser un identificador arbitrario generado por el sistema para su propia conveniencia. Sin embargo, dicha herramienta debe cumplir con las reglas para distinguir objetos en el modelo. El modelo debe dar una definición exacta de lo que son los objetos idénticos.

Desde el punto de vista de Martin, Entity no es un objeto, sino una capa. Esta capa combinará tanto el objeto como la lógica empresarial para cambiarlo.


Desolación de Martin
Mi opinión sobre las Entidades es que contienen reglas de Aplicación Independiente de Negocios. No son simplemente objetos de datos. Pueden contener referencias a objetos de datos; pero su propósito es implementar métodos de reglas de negocio que pueden ser utilizados por muchas aplicaciones diferentes.

Las puertas de enlace devuelven las entidades. La implementación (debajo de la línea) obtiene los datos de la base de datos y los usa para construir estructuras de datos que luego se pasan a las Entidades. Esto se puede hacer con contención o herencia.

Por ejemplo:

clase pública MyEntity {datos privados de MyDataStructure;}

o

clase pública MyEntity extiende MyDataStructure {...}

Y recuerda, todos somos piratas por naturaleza; y las reglas de las que estoy hablando aquí son realmente más como pautas ...

Por esencia, nos referiremos solo a la estructura. En su forma más simple, la clase Entidad se verá así:


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

Un objeto mutable que describe la estructura de un modelo de negocio puede contener tipos y valores primitivos.
La LunaPark::Entites::Simple es increíblemente simple, puedes ver su código, solo nos da una cosa: fácil inicialización.


LunaPark :: Entites :: Simple
 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 

Puedes escribir:


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

Como probablemente ya haya adivinado, queremos incluir el peso, la altura y la fecha de nacimiento en Value Objects .


 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 no perder el tiempo con tales constructores, hemos preparado una implementación más compleja de 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 su nombre indica, esta implementación le permite crear estructuras de árbol.


Satisfagamos mi pasión por los electrodomésticos voluminosos. En un artículo anterior, dibujamos una analogía entre el "giro" de una lavadora y la arquitectura . Y ahora describiremos un objeto comercial tan importante como un refrigerador:


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 

Este enfoque nos evita crear Entidades innecesarias, como la puerta del refrigerador. Sin un refrigerador, debería ser parte del refrigerador. Este enfoque es conveniente para compilar documentos relativamente grandes, por ejemplo, una solicitud para la compra de un seguro.


La LunaPark::Entites::Nested tiene 2 propiedades más importantes:


Comparabilidad:


 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 

Los dos usuarios especificados no son equivalentes, porque fueron creados en diferentes momentos y, por lo tanto, el valor del atributo registred_at será diferente. Pero si tachamos el atributo de la lista de comparados:


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

entonces obtenemos dos objetos comparables.


Esta implementación también tiene la propiedad de rotación: podemos usar el método de clase


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

Puedes usar Hash, OpenStruct o cualquier gema que te guste como entidad , lo que te ayudará a comprender la estructura de tu entidad.


Una entidad es un modelo de un objeto comercial, déjelo simple. Si su empresa no utiliza alguna propiedad, no la describa.


Cambios de entidad


Como notó, la clase Entity no tiene métodos de cambio propios. Todos los cambios se realizan desde el exterior. El objeto de valor también es inmutable. Todas esas funciones que están presentes en ella, en general, decoran la esencia o crean nuevos objetos. La esencia misma permanece sin cambios. Para un desarrollador de Ruby on Rails, este enfoque será inusual. Desde el exterior puede parecer que generalmente usamos el lenguaje OOP para otra cosa. Pero si miras un poco más profundo, esto no es así. ¿Se puede abrir una ventana sola? ¿Conseguir un auto para trabajar, reservar un hotel, lindo gato, obtener un nuevo suscriptor? Estas son todas las influencias externas. Algo sucede en el mundo real, y lo reflejamos en nosotros mismos. Para cada solicitud, realizamos cambios en nuestro modelo. Y así lo mantenemos actualizado, suficiente para nuestras tareas comerciales. Es necesario separar el estado del modelo y los procesos que causan cambios en este estado. Cómo hacer esto, lo consideraremos en el próximo artículo.

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


All Articles