Présentation
Nous avons donc déjà décidé de la portée , de la méthodologie et de l' architecture . Passons de la théorie à la pratique, pour écrire du code. Je voudrais commencer par des modèles de conception qui décrivent la logique métier - Service et Interactor . Mais avant de les entreprendre, nous allons examiner les modèles structurels - ValueObject et Entity . Nous développerons en langage rubis . Dans d'autres articles, nous analyserons tous les modèles nécessaires au développement à l'aide de l' architecture variable . Tous les développements qui sont des applications à cette série d'articles seront rassemblés dans un cadre séparé.

Et nous avons déjà choisi un nom approprié - LunaPark .
Développements actuels publiés sur Github .
Après avoir examiné tous les modèles, nous assemblerons un microservice à part entière.
Donc, historiquement
Il était nécessaire de refactoriser une application d'entreprise complexe écrite en Ruby on Rails. Il y avait une équipe prête à l'emploi de développeurs rubis. La méthodologie de développement piloté par domaine était parfaite pour ces tâches, mais il n'existait pas de solution clé en main dans la langue utilisée. Malgré le fait que le choix de la langue était principalement déterminé par notre spécialisation, il s'est avéré être un succès. Parmi toutes les langues couramment utilisées pour les applications Web, ruby est, à mon avis, la plus expressive. Et donc, plus que d'autres adaptés à la modélisation d'objets réels. Ce n'est pas seulement mon avis.
Voilà le monde Java. Ensuite, vous avez les nouveaux venus comme Ruby. Ruby a une syntaxe très expressive, et à ce niveau de base, il devrait être un très bon langage pour DDD (même si je n'ai pas encore entendu parler de son utilisation réelle dans ce genre d'applications). Rails a généré beaucoup d'enthousiasme car il semble finalement rendre la création d'interfaces utilisateur Web aussi facile que les interfaces utilisateur l'étaient au début des années 1990, avant le Web. À l'heure actuelle, cette capacité a été principalement appliquée à la création d'une partie du grand nombre d'applications Web qui n'ont pas beaucoup de richesse de domaine derrière elles, car même celles-ci ont été douloureusement difficiles dans le passé. Mais j'espère que, à mesure que la partie mise en œuvre de l'interface utilisateur du problème sera réduite, les gens verront cela comme une opportunité de concentrer davantage leur attention sur le domaine. Si l'utilisation de Ruby commence à aller dans cette direction, je pense que cela pourrait fournir une excellente plate-forme pour DDD. (Quelques éléments d'infrastructure devraient probablement être remplis.)
Eric Evans 2006
Malheureusement, au cours des 13 dernières années, rien n'a beaucoup changé. Sur Internet, vous pouvez trouver des tentatives d'adaptation de Rails pour cela, mais elles ont toutes l'air horribles. Le framework Rails est lourd, lent et non SOLIDE. Il est très difficile de voir sans larmes comment quelqu'un essaie de décrire l'implémentation du modèle de référentiel sur la base d' ActiveRecord . Nous avons décidé d'adopter un microframework et de l'adapter à nos besoins. Nous avons essayé Grape , l'idée de l'auto-documentation semblait réussie, mais sinon, elle a été abandonnée et nous avons rapidement abandonné l'idée de l'utiliser. Et presque immédiatement, ils ont commencé à utiliser une autre solution - Sinatra . Nous continuons à l'utiliser pour les contrôleurs et les points de terminaison REST.
REPOS?Si vous avez développé des applications Web, vous avez déjà une idée de la technologie. Il a ses avantages et ses inconvénients, dont la liste complète dépasse le cadre de cet article. Mais pour nous, en tant que développeurs d'applications d'entreprise, l'inconvénient le plus important sera que REST (cela ressort clairement du nom) ne reflète pas le processus, mais son état. Et l'avantage est sa compréhensibilité - la technologie est claire pour les développeurs back-end et les développeurs front-end.
Mais alors ne vous concentrez peut-être pas sur REST, mais implémentez votre solution http + json? Même si vous parvenez à développer votre API de service, en fournissant sa description à des tiers, vous recevrez de nombreuses questions. Bien plus que si vous fournissez le REST familier.
Nous envisagerons d'utiliser REST une solution de compromis. Nous utilisons JSON pour la concision et la norme jsonapi afin de ne pas perdre de temps aux développeurs sur les guerres sacrées concernant le format de la requête.
À l'avenir, lorsque nous analyserons Endpoint , nous verrons que pour se débarrasser du repos, il suffit de réécrire une seule classe. Donc, REST ne devrait pas déranger du tout en cas de doute.
Au cours de l'écriture de plusieurs microservices, nous avons acquis des bases - un ensemble de classes abstraites. Chacune de ces classes peut être écrite en une demi-heure, son code est facile à comprendre si vous savez à quoi sert ce code.
Ici, les principales difficultés sont apparues. Les nouveaux employés qui ne s'occupaient pas des pratiques DDD et de l'architecture propre ne pouvaient pas comprendre le code et son objectif. Si je voyais moi-même ce code pour la première fois avant de lire Evans, je le considérerais comme un héritage, une ingénierie excessive.
Afin de surmonter cet obstacle, il a été décidé de rédiger une documentation (ligne directrice) décrivant la philosophie des approches utilisées. Les grandes lignes de cette documentation semblaient réussies et il a été décidé de les mettre sur Habré. Des classes abstraites qui se répétaient de projet en projet, il a été décidé de mettre dans un bijou séparé.
La philosophie

Si vous vous souvenez d'un film classique sur les arts martiaux, alors il y aura un gars cool qui manipule très habilement un poteau. Un sixième est essentiellement un bâton, un outil très primitif, l'un des premiers à tomber entre les mains de l'homme. Mais entre les mains du maître, il devient une arme redoutable.
Vous pouvez passer du temps à créer un pistolet qui ne vous tire pas dans la jambe, ou vous pouvez passer du temps à apprendre la technique de tir. Nous avons identifié 4 principes de base:
- Vous devez simplifier des choses complexes.
- La connaissance est plus importante que la technologie. La documentation est plus compréhensible pour une personne que le code; il ne faut pas remplacer les uns par les autres.
- Le pragmatisme est plus important que le dogmatisme. Les normes devraient guider la voie, et non définir un cadre de délimitation.
- Structuralité en architecture, flexibilité dans le choix des solutions.
Une philosophie similaire peut être tracée, par exemple, dans le système d'exploitation ArchLinux - The Arch Way . Sur mon ordinateur portable, Linux n'a pas pris racine depuis longtemps, tôt ou tard il s'est cassé et j'ai dû constamment le réinstaller. Cela a causé un certain nombre de problèmes, parfois graves tels que la rupture des délais de travail. Mais après avoir passé 2-3 jours à installer Arch, j'ai compris comment fonctionne mon système d'exploitation. Après cela, elle a commencé à travailler plus stable, sans échecs. Mes notes m'ont aidé à l'installer sur de nouveaux PC en quelques heures. Une documentation abondante m'a aidé à résoudre de nouveaux problèmes.
Le cadre a un caractère absolument de haut niveau. Les classes qui le décrivent sont responsables de la structure de l'application. Les solutions tierces sont utilisées pour interagir avec les bases de données, implémenter le protocole http et d'autres choses de bas niveau. Nous aimerions que le programmeur examine le code et comprenne comment fonctionne une classe particulière, et la documentation nous permettrait de comprendre comment les gérer. Comprendre la conception du moteur ne vous permettra pas de conduire une voiture.
Cadre
Il est difficile d'appeler LunaPark un framework au sens habituel. Cadre - cadre, Travail - travail. Nous exhortons à ne pas nous limiter à la portée. Le seul cadre que nous déclarons est celui qui indique à la classe dans laquelle telle ou telle logique doit être décrite. Il s'agit plutôt d'un ensemble d'outils avec des instructions détaillées pour eux.
Chaque classe est abstraite et comporte trois niveaux:
module LunaPark
Si vous souhaitez implémenter un formulaire qui crée un seul élément, vous héritez de cette classe:
module Forms class Create < LunaPark::Forms::Single
S'il y a plusieurs éléments, nous utiliserons une autre implémentation .
module Forms class Create < LunaPark::Forms::Multiple
Pour le moment, tous les développements n'ont pas été mis en parfait état et la gemme est en état alpha. Nous le citerons par étapes, en fonction de la publication des articles. C'est-à-dire si vous voyez un article sur ValueObject
et Entity
, ces deux modèles sont déjà implémentés. À la fin du cycle, tous seront adaptés à une utilisation sur le projet. Étant donné que le cadre lui-même est de peu d'utilité sans lien avec sinatra \ roda, un référentiel séparé sera créé qui montrera comment «tout visser» pour démarrer rapidement votre projet.
Le framework est avant tout une application à la documentation. Ne percevez pas ces articles comme une documentation pour le framework.
Alors, passons aux choses sérieuses.
Objet de valeur
- Quelle est la taille de ta copine?
- 151
- Vous avez commencé à rencontrer la statue de la liberté?
Quelque chose comme ça aurait pu arriver en Indiana. La croissance humaine n'est pas seulement un nombre, mais aussi une unité de mesure. Les attributs d'un objet ne peuvent pas toujours être décrits uniquement par des primitives (entier, chaîne, booléen, etc.), parfois des combinaisons de ceux-ci sont requises:
- L'argent n'est pas seulement un nombre, c'est un nombre (montant) + une devise.
- Une date se compose d'un jour, d'un mois et d'une année.
- Pour mesurer le poids, un seul chiffre ne nous suffit pas, il nécessite également une unité de mesure.
- Le numéro de passeport se compose d'une série et, en fait, du numéro.
D'un autre côté, ce n'est pas toujours une combinaison, c'est peut-être une sorte d'extension du primitif.
Un numéro de téléphone est souvent considéré comme un numéro. En revanche, il est peu probable qu'il ait une méthode d'addition ou de division. Il existe peut-être une méthode qui émettra un code de pays et une méthode qui définit un code de ville. Il y aura peut-être une certaine méthode décorative, qui la présentera non seulement comme une chaîne de chiffres 79001231212
, mais comme une chaîne lisible: 7-900-123-12-12
.
peut-être un décorateur?Basé sur le dogme, il est incontestable - oui. Si nous abordons ce dilemme de la part du bon sens, alors lorsque nous déciderons d'appeler ce numéro, nous transférerons l'objet lui-même sur le téléphone:
phone.call Values::PhoneNumber.new(79001231212)
Et si nous avons décidé de le présenter sous forme de chaîne, cela est clairement fait pour une personne. Alors pourquoi ne rendons-nous pas cette ligne immédiatement lisible pour une personne?
Values::PhoneNumber.new(79001231212).to_s
Imaginez que nous créons le site de casino en ligne Three Axes et vendons des jeux de cartes. Nous aurons besoin du cours de «carte à jouer».
module Values class PlayingCard < Lunapark::Values::Compound attr_reader :suit, :rank end end
Ainsi, notre classe a deux attributs en lecture seule:
- costume - costume de carte
- rang - dignité de la carte
Ces attributs sont définis uniquement lors de la création d'une carte et ne peuvent pas être modifiés lors de son utilisation. Bien sûr, vous pouvez prendre une carte à jouer et barrer 8 , écrire Q, mais cela est inacceptable. Dans une société décente, vous serez probablement abattu. L'incapacité de modifier les attributs après la création de l'objet détermine la première propriété de l'objet valeur - l'immuabilité.
La deuxième propriété importante de l'objet valeur sera la façon dont nous les comparerons.
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
Un tel test échouera, car ils seront comparés à l'adresse. Pour que le test réussisse, nous devons comparer Value-Obects par valeur, pour cela nous ajouterons une méthode de comparaison:
def ==(other) suit == other.suit && rank == other.rank end
Maintenant, notre test passera. Nous pouvons également ajouter des méthodes qui sont responsables de la comparaison, mais comment comparer 10 et K? Comme vous l'avez probablement déjà deviné, nous les présenterons également sous forme d'objets de valeur . Ok, alors maintenant nous devrons créer les dix meilleurs clubs comme celui-ci:
ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) ten_clubs = Values::PlayingCards.new(rank: ten, clubs: clubs)
Trois lignes suffisent pour le rubis. Afin de contourner cette limitation, nous introduisons la troisième propriété de la valeur de l' objet - chiffre d'affaires. Ayons une méthode spéciale de la classe .wrap
, qui peut prendre des valeurs de différents types et les convertir en la bonne.
class PlayingCard < Lunapark::Values::Compound def self.wrap(obj) case obj.is_a? self.class
Cette approche donne un gros avantage:
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'
Toutes ces cartes seront égales les unes aux autres. Si la méthode wrap
développe en bonnes pratiques, la mettre dans une classe séparée le sera. Du point de vue de l'approche dogmatique, une classe séparée sera également obligatoire.
Hmm, qu'en est-il de l'espace dans le pont? Comment savoir si cette carte est un atout? Ce n'est pas une carte à jouer. Il s'agit de la valeur de la carte à jouer. C'est exactement l'inscription 10 que vous menez sur le coin du carton.
Il est nécessaire de se rapporter à la valeur de l' objet ainsi qu'à la primitive, qui pour une raison quelconque n'a pas été implémentée dans ruby. De là surgit la dernière propriété - Object-Value n'est lié à aucun domaine.
Recommandations
Parmi toute la variété de méthodes et d'outils utilisés à chaque instant de chaque processus, il y a toujours une méthode et un outil qui fonctionnent plus vite et mieux que les autres.
Frederick Taylor 1914
Les opérations arithmétiques doivent renvoyer un nouvel objet
Les attributs d'un objet de valeur ne peuvent être que des primitives ou d'autres objets de valeur
Conserver des opérations simples dans les méthodes de classe
Si l'opération de "conversion" est importante, il est peut-être judicieux de la déplacer vers une classe distincte
Une telle suppression de logique dans un service séparé n'est possible qu'à la condition que le service soit isolé: il n'utilise pas de données provenant de sources externes. Ce service doit être limité par le contexte de l'objet valeur lui-même.
La valeur de l'objet ne peut rien savoir de la logique du domaine
Supposons que nous écrivons une boutique en ligne et que nous ayons une évaluation des marchandises. Pour l'obtenir, vous devez faire une demande à la base de données via le référentiel .
Entité
La classe Entity est responsable d'un objet réel. Cela peut être un contrat, une chaise, un agent immobilier, une tarte, un fer à repasser, un chat, un réfrigérateur - n'importe quoi. Tout objet dont vous pourriez avoir besoin pour modéliser vos processus métier est une entité .
Le concept d' entité est différent pour Evans et pour Martin. Du point de vue d'Evans, une entité est un objet caractérisé par quelque chose qui met l'accent sur son individualité.
Essence par ZvansSi un objet est déterminé par une existence individuelle unique, et non par un ensemble d'attributs, cette propriété doit être lue comme la principale lors de la définition d'un objet dans un modèle. La définition de la classe doit être simple et construite autour de la continuité et de l'unicité du cycle de l'existence de l'objet. Trouvez un moyen de distinguer chaque objet indépendamment de sa forme ou de son histoire. Portez une attention particulière aux exigences techniques associées à la comparaison d'objets en fonction de leurs attributs. Définissez une opération qui donnerait nécessairement un résultat unique pour chacun de ces objets - il peut être nécessaire d'associer un certain symbole avec unicité garantie pour cela. Un tel moyen d'identification peut avoir une origine externe, mais il peut également s'agir d'un identifiant arbitraire généré par le système pour sa propre commodité. Cependant, un tel outil doit respecter les règles de distinction entre les objets du modèle. Le modèle doit donner une définition exacte de ce que sont des objets identiques.
Du point de vue de Martin, l' Entité n'est pas un objet, mais un calque. Cette couche combinera à la fois l'objet et la logique métier pour le modifier.
Désolation de MartinMon point de vue sur les entités est qu'elles contiennent des règles commerciales indépendantes des applications. Ce ne sont pas simplement des objets de données. Ils peuvent contenir des références à des objets de données; mais leur objectif est d'implémenter des méthodes de règles métier qui peuvent être utilisées par de nombreuses applications différentes.
Les passerelles renvoient des entités. L'implémentation (sous la ligne) récupère les données de la base de données et l'utilise pour construire des structures de données qui sont ensuite transmises aux entités. Cela peut être fait avec confinement ou héritage.
Par exemple:
classe publique MyEntity {données MyDataStructure privées;}
ou
classe publique MyEntity étend MyDataStructure {...}
Et rappelez-vous, nous sommes tous des pirates par nature; et les règles dont je parle ici sont plus comme des directives ...
Par essence, nous entendons uniquement la structure. Dans sa forme la plus simple, la classe Entity ressemblera à ceci:
module Entities class MeatBag < LunaPark::Entities::Simple attr_accessor :id, :name, :hegiht, :weight, :birthday end end
Un objet modifiable qui décrit la structure d'un modèle d'entreprise peut contenir des types et des valeurs primitifs.
La LunaPark::Entites::Simple
est incroyablement simple, vous pouvez voir son code, elle ne nous donne qu'une chose - une initialisation facile.
LunaPark :: Entités :: Simple module LunaPark module Entities class Simple def initialize(params) set_attributes params end private def set_attributes(hash) hash.each { |k, v| send(:"
Vous pouvez écrire:
john_doe = Entity::MeatBag.new( id: 42, name: 'John Doe', height: '180cm', weight: '80kg', birthday: '01-01-1970' )
Comme vous l'avez probablement déjà deviné, nous voulons envelopper le poids, la taille et la date de naissance dans 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
Afin de ne pas perdre de temps sur ces constructeurs, nous avons préparé une implémentation plus complexe 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
Comme son nom l'indique, cette implémentation vous permet de créer des arborescences.
Répondons à ma passion pour les appareils électroménagers encombrants. Dans un article précédent, nous avons établi une analogie entre la «torsion» d'une machine à laver et l'architecture . Et maintenant, nous allons décrire un objet commercial aussi important qu'un réfrigérateur:

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
Cette approche nous évite de créer des entités inutiles, telles que la porte du réfrigérateur. Sans réfrigérateur, il devrait faire partie du réfrigérateur. Cette approche est pratique pour compiler des documents relativement volumineux, par exemple, une demande d'achat d'assurance.
La LunaPark::Entites::Nested
possède 2 propriétés plus importantes:
Comparabilité:
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
Les deux utilisateurs spécifiés ne sont pas équivalents, car ils ont été créés à des moments différents et par conséquent, la valeur de l'attribut registred_at
sera différente. Mais si nous barrons l'attribut de la liste des comparés:
module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at, comparable: false end end
alors nous obtenons deux objets comparables.
Cette implémentation a également la propriété du chiffre d'affaires - nous pouvons utiliser la méthode de classe
Entites::User.wrap(email: 'john.doe@mail.com', registred_at: Time.now)
Vous pouvez utiliser Hash, OpenStruct ou n'importe quel joyau que vous aimez en tant qu'entité, ce qui vous aidera à réaliser la structure de votre entité.
Une entité est un modèle d'un objet métier, laissez-le simple. Si certains biens ne sont pas utilisés par votre entreprise, ne le décrivez pas.
Changements d'entité
Comme vous l'avez remarqué, la classe Entity n'a aucune méthode de son propre changement. Toutes les modifications sont effectuées de l'extérieur. L'objet valeur est également immuable. Toutes ces fonctions qui y sont présentes, dans l'ensemble, décorent l'essence ou créent de nouveaux objets. L'essence elle-même reste inchangée. Pour un développeur Ruby on Rails, cette approche sera inhabituelle. De l'extérieur, il peut sembler que nous utilisons généralement le langage OOP pour autre chose. Mais si vous regardez un peu plus profondément - ce n'est pas le cas. Une fenêtre peut-elle s'ouvrir d'elle-même? Obtenir une voiture pour travailler, réserver un hôtel, un chat mignon, obtenir un nouvel abonné? Ce sont toutes des influences externes. Quelque chose se passe dans le monde réel, et nous reflétons cela en nous-mêmes. Pour chaque demande, nous modifions notre modèle. Et ainsi nous le maintenons à jour, suffisant pour nos tâches commerciales. Il est nécessaire de séparer l'état du modèle et les processus qui provoquent des changements dans cet état. Comment faire cela, nous verrons dans le prochain article.