Developer Cookbook: DDD Recipes (Parte 5, Procesos)

Introduccion


En los artículos anteriores, describimos: alcance , fundamentos metodológicos , un ejemplo de arquitectura y estructura . En este artículo, me gustaría contar cómo describir los procesos, sobre los principios de recopilación de requisitos, cómo los requisitos empresariales difieren de los funcionales, cómo pasar de los requisitos al código. Hable sobre los principios del uso del Caso de uso y cómo pueden ayudarnos. Explore ejemplos de opciones de implementación para patrones de diseño de Interactor y Capa de servicio.


como tu abuela


Los ejemplos dados en el artículo se dan usando nuestra solución LunaPark , lo ayudará con los primeros pasos en los enfoques descritos.


Separe los requisitos funcionales de los requisitos comerciales.


Una y otra vez, sucede que muchas ideas de negocios realmente no se convierten en el producto final previsto. Esto a menudo se debe a la incapacidad de comprender la diferencia entre los requisitos comerciales y los requisitos funcionales, lo que finalmente conduce a una recopilación inadecuada de requisitos, documentación innecesaria, retrasos en los proyectos y fallas importantes del proyecto.


O a veces nos enfrentamos a situaciones en las que, aunque la solución final satisface las necesidades de los clientes, pero de alguna manera no se alcanzan los objetivos comerciales.


Por lo tanto, es imprescindible separar los requisitos comerciales de los requisitos funcionales hasta que comience a definirlos. Tomemos un ejemplo.


Supongamos que estamos escribiendo una solicitud para una empresa de reparto de pizzas y decidimos crear un sistema de seguimiento de mensajería. Los requisitos comerciales son los siguientes:


"Introducir un sistema basado en la web y un sistema móvil de seguimiento de empleados basado en los empleados que capture a los correos en sus rutas y mejore la eficiencia al monitorear la actividad de los correos, su ausencia del trabajo y la productividad laboral".


Aquí podemos distinguir una serie de características que indicarán que estos son requisitos del negocio:


  • los requisitos comerciales siempre se escriben desde el punto de vista del cliente;
  • Estos son requisitos amplios, de alto nivel, pero aún orientados en parte;
  • no son objetivos de la empresa, pero ayudan a la empresa a alcanzar sus objetivos;
  • responda las preguntas " por qué " y " qué ". ¿Qué quiere recibir la empresa? ¿Y por qué lo necesita?

Los requisitos funcionales son acciones que el sistema debe realizar para implementar los requisitos del negocio. Por lo tanto, los requisitos funcionales están relacionados con la solución o el software desarrollado. Formulamos los requisitos funcionales para el ejemplo anterior:


  • el sistema debe mostrar la longitud y latitud del empleado a través de GPS / GLONASS;
  • el sistema debe mostrar las posiciones de los empleados en el mapa;
  • el sistema debería permitir a los gerentes enviar notificaciones a sus subordinados de campo.

Destacamos las siguientes características:


  • los requisitos funcionales siempre se escriben desde el punto de vista del sistema;
  • son más específicos y detallados;
  • Es gracias al cumplimiento de los requisitos funcionales que se desarrolla una solución efectiva que satisfaga las necesidades del negocio y los objetivos del cliente;
  • responde la pregunta " cómo ". Cómo resuelve el sistema los requisitos comerciales.

Deben decirse algunas palabras sobre los requisitos no funcionales (también conocidos como "requisitos de calidad"), que imponen restricciones en el diseño o la implementación (por ejemplo, requisitos de rendimiento, seguridad, disponibilidad, fiabilidad). Dichos requisitos responden a la pregunta " qué " debería ser el sistema.


El desarrollo es la traducción de los requisitos comerciales a los funcionales. La programación aplicada es la implementación de requisitos funcionales, y sistema - no funcional.


Casos de uso


La implementación de requisitos funcionales es a menudo la más compleja en los sistemas comerciales. En una arquitectura pura, los requisitos funcionales se implementan a través de la capa de casos de uso .


Pero para empezar, quiero recurrir a la fuente. Ivar Jacobson - el autor de la definición de caso de uso , uno de los autores de UML, y la metodología RUP, en su artículo Use-Case 2.0 The Hub of Software Development identifica 6 principios para usar casos de uso:


  1. hacerlos simples a través de la narración de historias
  2. tener un plan estratégico, estar al tanto de todo el panorama;
  3. centrarse en el significado;
  4. alinear el sistema en capas;
  5. entregar el sistema paso a paso;
  6. Satisfacer las necesidades del equipo.

Consideramos brevemente cada uno de estos principios, nos son útiles para una mayor comprensión. A continuación se muestra mi traducción gratuita, con abreviaturas e inserciones, le recomiendo que se familiarice con el original.


Simplicidad a través de la narración


La narración es parte de nuestra cultura; Esta es la forma más fácil y efectiva de transferir conocimiento, información de una persona a otra. Esta es la mejor manera de comunicar lo que debe hacer el sistema y ayudar al equipo a centrarse en objetivos comunes.


Los casos de uso reflejan los objetivos del sistema. Para entender el caso de uso, contamos, contar una historia determinada. La historia cuenta cómo lograr un objetivo y cómo resolver los problemas que surgen en el camino. Los casos de uso, como un libro de cuentos, proporcionan una manera de identificar y cubrir todas las historias diferentes pero relacionadas de una manera simple e integral. Esto facilita la recopilación, distribución y comprensión de los requisitos del sistema.


Este principio se correlaciona con el lenguaje Ubiques del enfoque DDD.


Comprender la imagen completa


Independientemente del sistema que esté desarrollando, grande, pequeño, software, hardware o negocio, es muy importante comprender el panorama general. Sin comprender el sistema en su conjunto, no puede tomar las decisiones correctas sobre qué incluir en el sistema, qué descartar, cuánto costará y qué beneficios traerá.


Ivar Jacobson sugiere utilizar el diagrama de casos de uso , que es muy conveniente para recopilar requisitos. Si los requisitos están compilados y son claros, el mapa de contexto de Eric Evans es la mejor opción. A menudo, el enfoque Scrum se interpreta para que las personas no pasen tiempo en un plan estratégico, considerando la planificación, más de dos semanas después, de una reliquia del pasado. La propaganda de Jeff Sutherland cayó sobre el flujo de agua, y las personas que completaron cursos de capacitación de dos semanas para Scrum Masters a quienes se les permitió administrar proyectos hicieron su trabajo. Pero el sentido común reconoce la importancia de la planificación estratégica. No es necesario hacer un plan estratégico detallado, pero debería serlo.


Centrarse en el valor


Al intentar comprender cómo se utilizará el sistema, siempre es importante centrarse en el valor que proporcionará a sus usuarios y otras partes interesadas. El valor se forma solo cuando se usa el sistema. Por lo tanto, es mucho mejor centrarse en cómo se aplicará el sistema que en largas listas de características o capacidades que puede ofrecer.


Los casos de uso brindan este enfoque, ayudándole a concentrarse en cómo el sistema será utilizado por un usuario específico para lograr su objetivo. Los casos de uso cubren muchas formas de usar el sistema: aquellos que logran exitosamente sus objetivos y aquellos que resuelven cualquier dificultad que surja.


Además, el autor ofrece un esquema maravilloso, al que se le debe prestar la mayor atención:



El diagrama muestra un caso de uso, "Retiro de efectivo en un cajero automático". La forma más fácil de lograr el objetivo se describe en la Dirección básica (flujo básico). Otros casos se describen como flujo alternativo. Estas instrucciones ayudan con la narración de historias, estructuran el sistema y ayudan con la escritura de pruebas.


Capas


La mayoría de los sistemas requieren mucho trabajo antes de estar listos para usar. Tienen muchos requisitos, la mayoría de los cuales dependen de otros requisitos, deben implementarse antes de que se cumplan y evalúen los requisitos.


Es un gran error crear un sistema de este tipo a la vez. El sistema debe construirse a partir de piezas, cada una de las cuales tiene un valor claro para los usuarios.


Estas ideas resuenan con enfoques ágiles y con ideas de dominio .


Lanzamiento de producto paso a paso.


La mayoría de los sistemas de software han evolucionado a lo largo de muchas generaciones. No se producen a la vez; se crean como una serie de lanzamientos, cada uno de los cuales se basa en un lanzamiento anterior. Incluso los lanzamientos en sí a menudo no salen a la vez, sino que se desarrollan a través de una serie de versiones intermedias. Cada paso proporciona una versión clara y utilizable del sistema. Esta es la forma en que se deben crear todos los sistemas.


Satisfacer las necesidades del equipo.


Desafortunadamente, no existe una solución universal para los problemas de desarrollo de software; diferentes equipos y diferentes situaciones requieren diferentes estilos y diferentes niveles de detalle. No importa qué métodos y técnicas elija, debe asegurarse de que sean lo suficientemente adaptables para satisfacer las necesidades actuales del equipo.


Eric Evans en su libro le insta a que no pase mucho tiempo describiendo todos los procesos a través de UML. Es suficiente usar cualquier esquema visual. Diferentes equipos, diferentes proyectos requieren un nivel de detalle diferente, ya que el propio autor de UML habla sobre esto.


Implementación


En arquitectura pura, Robert Martin define los siguientes casos de uso :


Estos casos de uso organizan el flujo de datos hacia y desde las entidades, y ordenan a esas entidades que usen sus Reglas comerciales críticas para lograr los objetivos del caso de uso.

Intentemos traducir estas ideas en código. Recordemos el esquema del tercer principio del uso de los casos de uso y tomémoslo como base. Considere un proceso comercial realmente complejo: "Cocinar un pastel de col".


Intentemos descomponerlo:


  • verificar disponibilidad del producto;
  • sacarlos de existencias;
  • amasa la masa;
  • deja que la masa suba;
  • preparar el relleno
  • hacer un pastel
  • hornear un pastel

Implementamos toda esta secuencia a través del Interactor , y cada paso se implementará a través de una función u Objeto funcional en la capa de servicio.


Secuencia de acciones (Interactor)


paso a paso


Recomiendo encarecidamente comenzar el desarrollo de un proceso empresarial complejo con la secuencia de acciones . Más precisamente, no es así, debe determinar el dominio de dominio al que pertenece el proceso de negocio. Aclarar todos los requisitos comerciales. Identifique todas las entidades que participan en el proceso. Documente los requisitos y definiciones de cada entidad en la base de conocimiento.


Pintar todo en papel en pasos. A veces necesitas un diagrama de secuencia. Su autor es el mismo que inventó el caso de uso: Ivar Jacobson. El diagrama fue inventado por él cuando estaba desarrollando un sistema de mantenimiento de red telefónica para Erickson, basado en el circuito de relé. Realmente me gusta este diagrama, y ​​el término Secuencia , en mi opinión, es más expresivo que el término Interactor . Pero en vista de la mayor prevalencia de este último, utilizaremos el término familiar - Interactor .


Una pequeña pista cuando describe un proceso de negocio es una buena ayuda para usted, la regla principal de la gestión de documentos puede ser: "Como resultado de cualquier actividad comercial, se debe redactar un documento". Por ejemplo, estamos desarrollando un sistema de descuento. Al ofrecer un descuento, de hecho, desde el punto de vista comercial, celebramos un acuerdo entre la empresa y el cliente. Todas las condiciones deben especificarse en este contrato. Es decir, en el dominio DiscountSystem, tendrá Entites :: Contract. No vincule el descuento al cliente, sino que cree un Contrato de entidad , que describa las reglas para su provisión.


Volvamos a la descripción de nuestro proceso comercial, después de que se haya vuelto transparente para todas las personas involucradas en su desarrollo, y todo su conocimiento sea fijo. Le recomiendo que comience a escribir código con la Secuencia de acciones .


La plantilla de diseño de secuencia es responsable de:


  • secuencia de acciones ;
  • coordinación de datos transmitidos entre acciones ;
  • errores de procesamiento cometidos por Actions durante su ejecución;
  • devolución del resultado del conjunto de Acciones comprometidas;
  • IMPORTANTE : la responsabilidad más importante de este patrón de diseño es la implementación de la lógica empresarial.

Me gustaría detenerme en la última responsabilidad con más detalle si tenemos algún tipo de proceso complejo: debemos describirlo de tal manera que quede claro lo que sucede sin entrar en detalles técnicos. Debe describirlo de manera tan expresiva como lo permitan sus habilidades de programación . Confíe esta clase al miembro más experimentado de su equipo.


Volvamos al pastel: tratemos de describir el proceso de su preparación a través de Interactor .


Implementación


Doy un ejemplo de implementación con nuestra solución LunaPark , que presentamos en un artículo anterior.


module Kitchen module Sequences class CookingPieWithabbage < LunaPark::Interactors::Sequence TEMPERATURE = Values::Temperature.new(180, unit: :cel) def call! Services::CheckProductsAvailability.call list: ingredients dough = Services::BeatDough.call from: Repository::Products.get(beat_ingredients) filler = Services::MakeabbageFiller.call from: Repository::Products.get(filler_ingredients) pie = Services::MakePie.call dough, with: filler bake = Services::BakePie.new pie, temp: TEMPERATURE sleep 5.min until bake.call pie end private attr_accessor :beat_ingredients, :filler_ingredients attr_accessor :pie def ingredients_list beat_ingredients_list + filler_ingredients_list end end end end 

Como podemos ver, la call! describe toda la lógica de negocios del proceso de horneado de pasteles. Y es conveniente usarlo para comprender la lógica de la aplicación.


Además, podemos describir fácilmente el proceso de hornear pastel de pescado al reemplazar MakeabbageFiller con MakeFishFiller . Por lo tanto, cambiamos muy rápidamente el proceso de negocio, sin modificaciones significativas en el código. Y también, podemos dejar ambas secuencias al mismo tiempo, ampliando los casos de negocios.


Arreglos


  • Método de call! es un método requerido; describe el orden de las acciones .
  • Cada parámetro de inicialización se puede describir a través de un setter o attr_acessor :

 class Foo < LunaPark::Interactors::Sequence # ... private attr_accessor :bar end Foo.call(bar: 42) 

  • El resto de los métodos deben ser privados.

Ejemplo de uso


 beat_ingredients = [ Entity::Product.new :flour, 500, :gr, Entity::Product.new :oil, 50, :gr, Entity::Product.new :salt, 1, :spoon, Entity::Product.new :milk, 150, :ml, Entity::Product.new :egg, 1, :unit, Entity::Product.new :yeast, 1, :spoon ] filler_ingredients = [ Entity::Product.new :cabbage, 500, :gr, Entity::Product.new :salt, 1, :spoon, Entity::Product.new :pepper, 1, :spoon ] cooking = CookingPieWithabbage.call( beat_ingredients: beat_ingredients, filler_ingredients: filler_ingredients ) #   : cooking.success? # => true cooking.fail # => false cooking.fail_message # => '' cooking.data # => Entity::Pie #   : cooking.success? # => false cooking.fail # => true cooking.fail_message # => 'The pie burned out' cooking.data # => nil 

El proceso se representa a través del objeto y tenemos todos los métodos necesarios para llamarlo: ¿tuvo éxito la llamada, se produjo algún error durante la llamada y, de ser así, cuál?


Manejo de errores


Si ahora recordamos el tercer principio de la aplicación Caso de uso, prestamos atención al hecho de que, además de la línea Principal , también teníamos direcciones alternativas . Estos son errores que debemos manejar. Considere un ejemplo: ciertamente no queremos que los eventos vayan de esa manera, pero no podemos hacer nada al respecto, la dura realidad es que los pasteles se queman periódicamente.


Interactor intercepta todos los errores heredados de la LunaPark::Errors::Processing .


¿Cómo hacemos un seguimiento del pastel? Para hacer esto, defina el error Burned en la acción BakePie .


 module Kitchen module Errors class Burned < LunaPark::Errors::Processing; end end end 

Y durante la cocción, verifique que nuestro pastel no se haya quemado:


 module Kitchen module Services class BakePie < LunaPark::Callable def call # ... rescue Errors::Burned, 'The pie burned out' if pie.burned? # ... end end end end 

En este caso, la trampa de errores funcionará y podremos tratarlos en los .
Los errores no heredados del Processing se perciben como errores del sistema y serán interceptados a nivel del servidor. A menos que se especifique lo contrario, el usuario recibirá 500 ServerError.


Uso de la práctica


1. ¡Intenta describir todas las llamadas en el método de llamada!


No debe implementar cada Acción en un método separado, esto hace que el código sea más hinchado. Tienes que mirar a toda la clase varias veces para entender cómo funciona. Eche a perder la receta para hornear un pastel:


 module Service class CookingPieWithabbage < LunaPark::Interactors::Sequence def call! check_products_availability make_cabbage_filler make_pie bake end private def check_products_availability Services::CheckProductsAvailability.call list: ingredients end # ... end end 

Use la llamada de acción directamente en el aula. Desde el punto de vista del rubí, este enfoque puede parecer inusual, por lo que parece más legible:


 class DrivingStart < LunaPark::Interactors::Sequence def call! Service::CheckEngine.call Service::StartUpTheIgnition.call car, with: key Service::ChangeGear.call car.gear_box, to: :drive Service::StepOnTheGas.call car.pedals[:right] end end 

2. Si es posible, use el método de clase de llamada


 # good - ,   ,  . #    . Sequence::RingingToPerson.call(params) # good -   ,      e, #    ,     , #    . ring = Sequence::RingingToPerson.new(person) unless ring.success? ring.call sleep 5.min end 

3. No cree objetos funcionales por el simple hecho de escribir el código, observe la situación.


 # bad -        ,  #     . module Services class BuildUser < LunaPark::Callable def initialize(first_name:, last_name:, phone:) @first_name = first_name @last_name = last_name @phone = phone end def call Entity::User.new( first_name: first_name, last_name: last_name, phone: phone ) end private attr_reader :first_name, :last_name, :phone end end module Sequences class RegisteringUser < LunaPark::Interactors::Sequence attr_accessor :first_name, :last_name, :phone def call! user = Service::BuildUser.call(first_name: first_name, last_name: last_name, phone: phone) end end end # good -     ,  . #        , #       . module Sequences class RegisteringUser < LunaPark::Interactors::Sequence attr_accessor :first_name, :last_name, :phone def call! user #... end private def user @user = Entity::User.new( first_name: first_name, last_name: last_name, phone: phone ) end end end 

Capa de servicio


lsd


El Interactor, como dijimos, describe la lógica de negocios al más alto nivel. La capa de servicio ( capa de servicio) ya revela los detalles de la implementación de los requisitos funcionales. Si estamos hablando de hacer un pastel, entonces al nivel del Interactor simplemente decimos "amasar la masa", sin entrar en detalles sobre cómo amasarla. El proceso de amasado se describe en el nivel de Servicio . Volvamos a la fuente original, el gran libro azul :


En el dominio aplicado, hay operaciones que no pueden encontrar un lugar natural en un objeto de tipo Entidad u Objeto de valor. No son inherentemente objetos, sino actividades. Pero dado que la base de nuestro paradigma de modelado es el enfoque de objetos, intentaremos convertirlos en objetos.


En este punto, es fácil cometer un error común: abandonar el intento de colocar la operación en un objeto adecuado para ello, y así llegar a la programación de procedimientos. Pero si coloca con fuerza una operación en un objeto con una definición ajena a ella, esto hará que el objeto mismo pierda su pureza, lo que hará que sea más difícil de entender y refactorizar. Si implementa muchas operaciones complejas en un objeto simple, puede convertirse en incomprensible qué, lo que está haciendo no está claro qué. Dichas operaciones a menudo involucran otros objetos del área temática y la coordinación entre ellos se realiza para realizar una tarea conjunta. La responsabilidad adicional crea cadenas de dependencia entre objetos, mezclando conceptos que podrían considerarse independientemente.


Al elegir una ubicación para la implementación de un funcional, siempre use el sentido común. Su tarea es hacer que el modelo sea más expresivo. Veamos un ejemplo, "Necesitamos cortar madera":


 module Entities class Wood def chop # ... end end end 

Este método será un error. La leña no se cortará sola, necesitamos un hacha:


 module Entities class Axe def chop(sacrifice) # ... end end end 

Si utilizamos un modelo de negocio simplificado, eso será suficiente. Pero si el proceso necesita ser modelado con más detalle, necesitaremos una persona que corte esta leña, y tal vez algún registro que se utilizará como soporte para el proceso.


 module Entities class Human def chop_firewood(wood, axe, chock) # ... end end end 

Como probablemente ya haya adivinado, esta no es una buena idea. No todos estamos involucrados en cortar madera, esto no es un deber directo de una persona. A menudo vemos cuán sobrecargados están los modelos en Ruby on Rails, que contienen una lógica similar: obtener descuentos, agregar productos a la cesta, retirar dinero al saldo. Esta lógica no se aplica a la entidad, sino al proceso en el que esta entidad está involucrada.


 module Services class ChopFirewood # ... end end 

Después de descubrir qué lógica almacenamos en los Servicios, intentaremos implementar uno de ellos. Muy a menudo, los servicios se implementan a través de métodos u objetos funcionales.


Objetos funcionales


Un objeto funcional cumple un requisito funcional. En su forma más primitiva, un objeto funcional tiene un único método público: call .


 module Serivices class Sum def initialize(x, y) @x = x @y = y end def call x + y end def self.call(x,y) new(x,y).call end private attr_reader :x, :y end end 

Tales objetos tienen varias ventajas: son concisos, son muy simples de probar. Hay un inconveniente, tales objetos pueden llegar a ser un gran número. Hay varias formas de agrupar objetos similares; en parte de nuestros proyectos, los dividimos por tipo:


  • Objeto de servicio (Servicio): un objeto, crea un nuevo objeto;
  • Comando (Comando): cambia el objeto actual;
  • Guardian (Guard): devuelve un error si algo salió mal.

Objeto de servicio


En nuestra implementación, Servicio : implementa un requisito funcional y siempre devuelve un valor.


 module KorovaMilkBar module Services class FindMilk < LunaPark::Callable GLASS_SIZE = Values::Unit.wrap '200g' def initialize(fridge:) @fridge = fridge end def call fridge.shelfs.find { |shelf| shelf.has?(GLASS_SIZE, of: :milk) } end private attr_reader :fridge end end end FindMilk.call(fridge: the_red_one) # => #<Glass: ... > 

Comando


En nuestra implementación, Comando : realiza una Acción , modifica el objeto, si verdadero devuelve verdadero. De hecho, el Equipo no crea un objeto, sino que modifica uno existente.


 module KorovaMilkBar module Commands class FillGlass < LunaPark::Callable def initialize(glass, with:) @glass = glass @content = with end def call glass << content true end private attr_reader :fridge end end end glass = Glass.empty milk = Milk.new(200, :gr) glass.empty? # => true FillGlass.call glass, with: milk # => true glass.empty? # => false 

Guardian (Guardia)


El vigilante realiza una verificación lógica y, en caso de falla, genera un error de procesamiento. Este tipo de objeto no afecta a la Dirección principal de ninguna manera, pero nos cambia a la Dirección alternativa si algo sale mal.


Cuando sirva leche, es importante asegurarse de que esté fresca:


 module KorovaMilkBar module Guards class IsFresh < LunaPark::Callable def initialize(product) @products = products end def call products.each do |product| raise Errors::Rotten, "#{product.title} is not fresh" if product.expiration_date > Date.today end nil end private attr_reader :products end end end 

Puede resultarle conveniente separar los objetos funcionales por tipo. Puede agregar el suyo propio, por ejemplo, Generador : crea un objeto basado en parámetros.


Arreglos


  • El método de call es el único método público obligatorio.
  • El método initialize es el único método público opcional.
  • El resto de los métodos deben ser privados.
  • Los errores lógicos se deben heredar de la LunaPark::Errors::Processing .

Manejo de errores


Hay 2 tipos de errores que pueden ocurrir durante la operación de una Acción .


Errores de tiempo de ejecución

Dichos errores pueden ocurrir como resultado de la violación de la lógica de procesamiento.


Por ejemplo:


  • cuando se crea un correo electrónico de usuario está reservado;
  • cuando tratas de tomar leche, se acaba;
  • otro microservicio rechazó la acción (por una razón lógica, y no porque el servicio no esté disponible).

Con toda probabilidad, el usuario querrá saber acerca de estos errores. Además, estos son probablemente los errores
que podemos prever.


Dichos errores deben heredarse de LunaPark::Errors::Processing


Errores del sistema

Errores que ocurrieron como resultado de un bloqueo del sistema.


Por ejemplo:


  • la base de datos no funciona;
  • algo dividido por cero.

Con toda probabilidad, no podemos prever estos errores y no podemos decir nada al usuario, excepto que todo está muy mal, y enviar a los desarrolladores un informe pidiendo acción. Dichos errores deben heredarse de SystemError


También hay errores de validación , que discutiremos con más detalle en el próximo artículo.


Uso de la práctica


1. Use variables para aumentar la legibilidad


 module Fishing # bad -   Serivices::Catch.call(fish, rod) # bad -  Serivices::Catch.call(fish: fish, rod: rod) # good -   Serivices::Catch.call(fish, with: rod) module Serivices class Catch def initialize(fish, with:) @fish = fish @rod = with #      #   . end # ... private attr_reader :fish, :rod end end end 

2. Pase objetos, no parámetros


Intente simplificar el inicializador si el procesamiento de parámetros no es su propósito.
Pase objetos, no parámetros.


 module Service # bad -        -.  #      ,   . class Foo def initialize(foo_params:, bar_params:) @foo = Values::Foo.new(*foo_params) @bar = Values::Bar.new(*bar_params) end # ... end Services::Foo.call(foo: {a: 1, b: 2}, bar: 34) # good -   -. class Bar def initialize(foo:, bar:) @foo = foo @bar = bar # ... end end foo = Values::Foo.new(a: 1, b: 2) bar = Values::Bar.new(34) Services::Bar.call(foo: foo, bar: bar) # good -       - Builder. class BuildFoo def initialize(param_1:, param_2:) @param_1 = param_1 @param_1 = param_1 end def call Foo.new( param_1: param_1.foo, param_2: param_2.bar, param_3: some_magick ) end # ... end end 

3. Use el nombre Acciones: el verbo de la acción y el objeto de influencia.


 # bad module Services class Milk; end class Work; end class FooBuild; end class PasswordGenerator; end end # good module Services class GetMilk; end class WorkOnTable; end class BuildFoo; end class GeneratePassword; end end 

4. Si es posible, use el método de clase de llamada


Por lo general, es una instancia de la clase Actions , que rara vez se usa además de escribir para hacer una llamada.


 # good -    . Services::BuildFoo.call(params) # good -     Services::BuildFoo.(params) # good -   ,      , #    ,     ,   #  . ring = Services::RingToPhone.new(phone: neighbour) 10.times do ring.call end 

5. El manejo de errores no es una tarea de servicio


 # bad -    ,   . def call #... rescue SystemError => e return false end 

Módulos


Hasta este momento, consideramos la implementación de la capa de Servicio como un conjunto de objetos funcionales. Pero podemos colocar fácilmente métodos en esta capa:


 module Services def sum(a, b) a + b end end 

Otro problema que nos enfrenta es una gran cantidad de instalaciones de servicio. En lugar del "modelo gordo de rieles", que obtuvimos de borde, obtenemos la "carpeta fat de servicios". Hay varias formas de organizar la estructura para reducir la escala de la tragedia. Eric Evans resuelve esto combinando varias funciones en una clase. Imagine que necesitamos modelar los procesos comerciales de la niñera, Arina Rodionovna, que puede alimentar a Pushkin y acostarlo:


 class NoonService def initialize(arina_radionovna, pushkin) # ... end def to_feed # ... end def to_sleep # ... end end 

Este enfoque es más correcto desde el punto de vista de OOP. Pero sugerimos abandonarlo, al menos en las etapas iniciales. Los programadores poco experimentados comienzan a escribir mucho código en esta clase, lo que finalmente conduce a una mayor conectividad. En su lugar, puede usar el módulo, que representa la actividad como una abstracción:


 module Services module Noon class ToFeed def call! # ... end end class << self #    ,   #    def to_feed(arina_radionovna, pushkin) ToFeed.new(arina_radionovna, pushkin).call end #    ,    def to_sleep(arina_radionovna, pushkin) arina_radionovna.tell_story pushkin pushkin.state = :sleep end end end end 

Cuando se divide en módulos, se debe observar un acoplamiento externo bajo (acoplamiento bajo) con alta cohesión interna (alta cohesión), pero utilizamos módulos tales como Servicios o Interactores, esto también va en contra de las ideas de la arquitectura pura. Esta es una elección consciente que facilita la percepción. Por el nombre del archivo, entendemos qué patrón de diseño implementa esta o aquella clase, si para un programador experimentado esto es obvio, para un principiante este no es siempre el caso. Una vez que tu equipo esté listo, descarta este exceso.


Para citar otro pequeño extracto del gran libro azul:


Elija módulos que cuenten la historia del sistema y contengan conjuntos coherentes de conceptos. De esto a menudo surge una baja dependencia de los módulos entre sí. Pero si este no es el caso, encuentre una manera de cambiar el modelo de tal manera que separe los conceptos entre sí, o busque el concepto que faltaba en el modelo, que podría convertirse en la base del módulo y, por lo tanto, reunir los elementos del modelo de una manera natural y significativa. Lograr una baja dependencia de los módulos entre sí en el sentido de que los conceptos en diferentes módulos se pueden analizar y percibir independientemente uno del otro. Refine el modelo hasta que aparezcan los límites naturales de acuerdo con los conceptos de alto nivel del área temática, y el código correspondiente no se divide en consecuencia.


Dé los nombres de los módulos que se incluirán en el IDIOMA UNIFICADO. Tanto los MÓDULOS mismos como sus nombres deben reflejar el conocimiento y la comprensión del área temática.


El tema de los módulos es amplio e interesante, pero en su totalidad claramente va más allá del tema de este artículo. La próxima vez hablaremos con usted sobre repositorios y adaptadores . Abrimos un acogedor canal de telegramas donde nos gustaría compartir materiales sobre el tema de DDD. Agradecemos sus preguntas y comentarios.




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


All Articles