Maison >développement back-end >tutoriel php >Gestion des collections de racines agrégées - le modèle de référentiel
Points de base
L'un des aspects les plus typiques de l'architecture traditionnelle de conception axée sur le domaine (DDD) est l'agnosticité de persistance obligatoire démontrée par le modèle de domaine. Dans des conceptions plus conservatrices, y compris certaines implémentations basées sur des enregistrements actifs ou des passerelles de table de données (dans la recherche d'une simplicité assez trompeuse, se retrouvent souvent avec la logique de pollution des infrastructures), il existe toujours un concept clair de mécanismes de stockage sous-jacents, généralement des bases de données relationnelles. D'un autre côté, le modèle de domaine est conceptuellement conçu du départ à une nature strictement «d'agnostique», passant ainsi une de sa logique de persistance au-delà de ses limites. Même si l'on considère que le DDD est quelque peu insaisissable lorsqu'il se réfère directement à une «base de données», dans le monde réel, il est probable qu'au moins une base de données s'exécute dans les coulisses, car le modèle de domaine doit éventuellement être persisté sous une forme ou une autre. Par conséquent, il est courant de déployer une couche de mappage entre le modèle et la couche d'accès aux données. Ceci non seulement favorise activement le maintien d'un degré d'isolement considérable entre les couches, mais protège également tous les détails complexes du code client qui implique de déplacer des objets de domaine dans les deux sens entre les lacunes dans les couches de problème. mea culpa Pour être juste, il est juste de dire que la manipulation de la singularité dans la couche de mapper de données est un fardeau considérable, et que la stratégie "écrire une fois / utilisation permanente" est souvent adoptée. Néanmoins, le modèle ci-dessus fonctionne bien dans des conditions assez simples, où seul un petit nombre de classes de domaine sont traitées par un petit nombre de mappeurs. Cependant, comme le modèle commence à gonfler et devient plus complexe, la situation peut devenir plus gênante, car il y aura certainement des cartographies supplémentaires ajoutées au fil du temps. Cela suggère brièvement que l'ouverture de la porte à la négligence de persistance peut être difficile à mettre en œuvre dans la pratique lors de l'utilisation d'un modèle de domaine riche composé de plusieurs racines d'agrégats complexes, du moins si vous ne créez pas de graphiques d'objets coûteux à plusieurs endroits ou entreprend le chemin du péché de l'implémentation répétée. Pire, dans les grands systèmes qui doivent extraire des ensembles racinaires agrégés coûteux de la base de données qui correspondent à différentes conditions, l'ensemble du processus de requête lui-même peut devenir un catalyseur actif et prolifique d'une telle duplication erronée si elle n'est pas correctement concentrée par un seul point d'entrée.Dans ce cas d'utilisation complexe, la mise en œuvre d'une couche d'abstraction supplémentaire (souvent appelée entreposage dans le jargon DDD) qui arbitrate entre le mappeur de données et le modèle de domaine contribue efficacement à minimiser la duplication de la logique de requête tout en exposant la sémantique de l'ensemble de mémoire réel dans le modèle. Cependant, contrairement aux mappeurs (qui font partie de l'infrastructure), l'entreposage lui-même est caractérisé par le langage du modèle, car il est étroitement lié au modèle. Et en raison de sa dépendance implicite à l'égard des mappeurs, il conserve également l'ignorance de la persistance, fournissant ainsi un niveau d'abstraction plus élevé, plus proche des objets de domaine. Malheureusement, toutes les applications possibles ne peuvent pas facilement mettre en œuvre les avantages de l'entreposage, il ne vaut donc la peine d'être mis en œuvre que si la situation l'exige. Dans tous les cas, il serait très avantageux de construire un petit entrepôt à partir de zéro afin que vous puissiez voir comment il fonctionne en interne et révéler ce qui est exactement sous sa coque assez ésotérique.
Effectuer des préparations préliminaires
Le processus de mise en œuvre de l'entreposage peut être très complexe car il masque en fait tous les détails de l'injection et du traitement des mappeurs de données après une API de collection simplifiée qui à son tour injecte une sorte d'adaptateur persistant, etc. Cette injection continue de dépendances, associée à une grande quantité de logique, explique pourquoi l'entreposage est souvent considéré comme un look simple, même si certaines perspectives diffèrent actuellement du concept. Dans les deux cas, la première étape que nous devrions franchir pour monter et exécuter l'entreposage fonctionnel est de créer un modèle de domaine de base. Le modèle que je prévois d'utiliser ici sera responsable de la modélisation de l'utilisateur général, avec la structure de base comme suit:
<code class="language-php"><?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function setRole($role); public function getRole(); }</code>
<code class="language-php"><?php namespace Model; class User implements UserInterface { const ADMINISTRATOR_ROLE = "Administrator"; const GUEST_ROLE = "Guest"; protected $id; protected $name; protected $email; protected $role; public function __construct($name, $email, $role = self::GUEST_ROLE) { $this->setName($name); $this->setEmail($email); $this->setRole($role); } public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The ID for this user has been set already."); } if (!is_int($id) || $id throw new InvalidArgumentException( "The user ID is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = htmlspecialchars(trim($name), ENT_QUOTES); return $this; } public function getName() { return $this->name; } public function setEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( "The user email is invalid."); } $this->email = $email; return $this; } public function getEmail() { return $this->email; } public function setRole($role) { if ($role !== self::ADMINISTRATOR_ROLE && $role !== self::GUEST_ROLE) { throw new InvalidArgumentException( "The user role is invalid."); } $this->role = $role; return $this; } public function getRole() { return $this->role; } }</code>
Dans ce cas, le modèle de domaine est une couche plutôt squelettique, à peine supérieure à un simple support de données qui peut s'auto-vérifier, qui définit les données et le comportement de certains utilisateurs fictifs uniquement par des interfaces isolées et des implémenteurs simples. Pour garder les choses simples et faciles à comprendre, je garderai le modèle aussi rationalisé. Comme le modèle est déjà en cours d'isolement facile, le rend plus riche en y ajoutant une autre classe, qui gère la collection d'objets utilisateur. Ce composant "complément" est juste un wrapper de tableau classique qui implémente les interfaces SPL dénombrables, arrayaccess et iteratoraggregate:
<code class="language-php"><?php namespace ModelCollection; use MapperUserCollectionInterface, ModelUserInterface; class UserCollection implements UserCollectionInterface { protected $users = array(); public function add(UserInterface $user) { $this->offsetSet($user); } public function remove(UserInterface $user) { $this->offsetUnset($user); } public function get($key) { return $this->offsetGet($key); } public function exists($key) { return $this->offsetExists($key); } public function clear() { $this->users = array(); } public function toArray() { return $this->users; } public function count() { return count($this->users); } public function offsetSet($key, $value) { if (!$value instanceof UserInterface) { throw new InvalidArgumentException( "Could not add the user to the collection."); } if (!isset($key)) { $this->users[] = $value; } else { $this->users[$key] = $value; } } public function offsetUnset($key) { if ($key instanceof UserInterface) { $this->users = array_filter($this->users, function ($v) use ($key) { return $v !== $key; }); } else if (isset($this->users[$key])) { unset($this->users[$key]); } } public function offsetGet($key) { if (isset($this->users[$key])) { return $this->users[$key]; } } public function offsetExists($key) { return ($key instanceof UserInterface) ? array_search($key, $this->users) : isset($this->users[$key]); } public function getIterator() { return new ArrayIterator($this->users); } }</code>
En fait, la mise en place de cet ensemble de tableaux dans les limites du modèle est complètement facultative, car l'utilisation d'un tableau normal peut produire à peu près les mêmes résultats. Cependant, dans ce cas, en s'appuyant sur des classes de collecte indépendantes, il est plus facile d'accéder à l'ensemble d'objets utilisateur extrait de la base de données via une API orientée objet. En outre, étant donné que le modèle de domaine doit ignorer complètement le stockage sous-jacent configuré dans l'infrastructure, la prochaine étape logique que nous devons franchir est d'implémenter une couche de mappage qui le sépare bien de la base de données. Voici les éléments qui composent ce calque:
<code class="language-php"><?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function setRole($role); public function getRole(); }</code>
<code class="language-php"><?php namespace Model; class User implements UserInterface { const ADMINISTRATOR_ROLE = "Administrator"; const GUEST_ROLE = "Guest"; protected $id; protected $name; protected $email; protected $role; public function __construct($name, $email, $role = self::GUEST_ROLE) { $this->setName($name); $this->setEmail($email); $this->setRole($role); } public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The ID for this user has been set already."); } if (!is_int($id) || $id throw new InvalidArgumentException( "The user ID is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = htmlspecialchars(trim($name), ENT_QUOTES); return $this; } public function getName() { return $this->name; } public function setEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( "The user email is invalid."); } $this->email = $email; return $this; } public function getEmail() { return $this->email; } public function setRole($role) { if ($role !== self::ADMINISTRATOR_ROLE && $role !== self::GUEST_ROLE) { throw new InvalidArgumentException( "The user role is invalid."); } $this->role = $role; return $this; } public function getRole() { return $this->role; } }</code>
Unbox, le lot de tâches effectué par UserMapper est assez simple, limité à l'exposition de quelques chercheurs généraux qui sont responsables de l'extraction des utilisateurs de la base de données et de la reconstruction de l'entité correspondante via la méthode CreateUser (). De plus, si vous avez déjà plongé dans certains mappeurs et que vous avez même écrit votre propre chef-d'œuvre de cartographie, ce qui précède est certainement facile à comprendre. Le seul détail subtil qui mérite d'être mis en évidence est probablement que l'UsercollectionInterface a été placé dans la couche de mappage, pas dans le modèle. Je le fais exprès parce que de cette manière, l'abstraction (protocole) dont dépend la collection d'utilisateurs est explicitement déclarée et détenue par un user-titulaire de niveau supérieur, ce qui est cohérent avec le guide promu par le principe d'inversion de dépendance. Avec le mappeur déjà configuré, nous pouvons l'utiliser directement hors de la boîte et extraire certains objets utilisateur du stockage pour permettre au modèle de s'hydrater immédiatement. Bien que cela semble être le bon chemin à première vue, nous entravons la logique d'application avec l'infrastructure inutile car les caractéristiques font en fait partie de l'infrastructure. Et si à l'avenir, il est nécessaire d'interroger des entités utilisateur sur la base de conditions plus granulaires et spécifiques au domaine (pas seulement des conditions courantes exposées par le chercheur du mappeur)? Dans ce cas, il doit placer une couche supplémentaire au-dessus de la couche de mappage, qui fournit non seulement un niveau d'accès aux données plus élevé, mais porte également le bloc de logique de requête par un seul point. En fin de compte, c'est l'énorme quantité d'avantages que nous attendons de l'entreposage.
Implémentez l'entreposage des utilisateurs
Dans un environnement de production, l'entreposage peut mettre en œuvre presque tout ce à quoi on peut penser à sa surface afin d'exposer l'illusion de l'ensemble de mémoire des racines agrégées au modèle. Cependant, dans ce cas, nous ne pouvons pas si naïvement nous attendre à profiter gratuitement de ce luxe cher, car l'entrepôt que nous allons construire sera une structure plutôt artificielle responsable de l'extraction des utilisateurs de la base de données:
<code class="language-php"><?php namespace ModelCollection; use MapperUserCollectionInterface, ModelUserInterface; class UserCollection implements UserCollectionInterface { protected $users = array(); public function add(UserInterface $user) { $this->offsetSet($user); } public function remove(UserInterface $user) { $this->offsetUnset($user); } public function get($key) { return $this->offsetGet($key); } public function exists($key) { return $this->offsetExists($key); } public function clear() { $this->users = array(); } public function toArray() { return $this->users; } public function count() { return count($this->users); } public function offsetSet($key, $value) { if (!$value instanceof UserInterface) { throw new InvalidArgumentException( "Could not add the user to the collection."); } if (!isset($key)) { $this->users[] = $value; } else { $this->users[$key] = $value; } } public function offsetUnset($key) { if ($key instanceof UserInterface) { $this->users = array_filter($this->users, function ($v) use ($key) { return $v !== $key; }); } else if (isset($this->users[$key])) { unset($this->users[$key]); } } public function offsetGet($key) { if (isset($this->users[$key])) { return $this->users[$key]; } } public function offsetExists($key) { return ($key instanceof UserInterface) ? array_search($key, $this->users) : isset($this->users[$key]); } public function getIterator() { return new ArrayIterator($this->users); } }</code>
<code class="language-php"><?php namespace Mapper; use ModelUserInterface; interface UserCollectionInterface extends Countable, ArrayAccess, IteratorAggregate { public function add(UserInterface $user); public function remove(UserInterface $user); public function get($key); public function exists($key); public function clear(); public function toArray(); }</code>
<code class="language-php"><?php namespace Mapper; use ModelRepositoryUserMapperInterface, ModelUser; class UserMapper implements UserMapperInterface { protected $entityTable = "users"; protected $collection; public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $collection) { $this->adapter = $adapter; $this->collection = $collection; } public function fetchById($id) { $this->adapter->select($this->entityTable, array("id" => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->createUser($row); } public function fetchAll(array $conditions = array()) { $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); return $this->createUserCollection($rows); } protected function createUser(array $row) { $user = new User($row["name"], $row["email"], $row["role"]); $user->setId($row["id"]); return $user; } protected function createUserCollection(array $rows) { $this->collection->clear(); if ($rows) { foreach ($rows as $row) { $this->collection[] = $this->createUser($row); } } return $this->collection; } }</code>
Bien qu'en plus d'une structure plutôt légère, l'implémentation de l'utilisateur est très intuitif car son API lui permet d'extraire une collection d'objets utilisateur d'un stockage conforme aux prédicats fins étroitement liés au langage du modèle. En outre, dans son état actuel, le référentiel expose uniquement des chercheurs simples au code client, qui à son tour utilise la fonctionnalité du mappeur de données pour accéder au stockage. Dans un environnement plus réaliste, l'entreposage devrait également être en mesure de persister des racines agrégées. Si vous souhaitez ajouter la méthode insert () ou une autre méthode similaire à UserRepository, n'hésitez pas à le faire. Dans les deux cas, un moyen efficace de capturer les avantages réels de l'utilisation de l'entreposage par des exemples est:
<code class="language-php"><?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function setRole($role); public function getRole(); }</code>
Comme mentionné précédemment, l'entreposage intervient efficacement les termes commerciaux avec le code client (le soi-disant "langage universel" créé par Eric Evans dans son livre "Drive Design") plutôt que des termes techniques de niveau inférieur. Contrairement à l'ambiguïté présente dans le Finder de Data Mapper, en revanche, la méthode de l'entreposage se décrit avec "nom", "e-mail" et "rôle", qui font sans aucun doute partie des propriétés de la modélisation des entités utilisateur. Cette abstraction de données plus fine et de niveau supérieur et un ensemble complet de fonctionnalités requis lorsque l'encapsulation de la logique de requête dans les systèmes complexes est sans aucun doute l'une des raisons les plus convaincantes pour lesquelles l'entreposage est plus attrayant dans les conceptions multicouches. Bien sûr, la plupart du temps, il existe un compromis implicite entre les tracas d'obtenir ces avantages à l'avance et de déployer des couches d'abstraction supplémentaires (qui peuvent être trop gonflées dans des applications plus simples).
Conclusion
En tant que l'un des concepts de base de la conception axée sur le domaine, l'entreposage peut être trouvé dans des applications écrites dans plusieurs autres langues telles que Java et C #, pour n'en nommer que quelques-unes. Cependant, en PHP, ils sont encore relativement inconnus, faisant juste le premier pas dans le monde. Pourtant, il existe des cadres de confiance comme Flow3 et bien sûr Doctrine 2.x qui peuvent vous aider à adopter le paradigme DDD. Comme pour toute approche de développement existante, vous n'avez pas à utiliser de référentiels dans votre application, ou même les casser avec le concept tas derrière DDD. Appliquez simplement le bon sens et choisissez-les uniquement si vous pensez qu'ils conviennent à vos besoins. C'est aussi simple que cela. Photos de Chance Agrella / Freerangestock.com
FAQS lors de la manipulation des collections de racines agrégées (FAQ)
Dans la conception du domaine (DDD), une racine agrégée est une collection d'objets associés qui sont considérés comme une unité. Ces objets sont liés ensemble par des entités racines (également appelées racines agrégées). La racine d'agrégation maintient la cohérence des modifications apportées à l'agrégation en interdisant aux objets externes de contenir des références à leurs membres.
La principale différence entre les racines agrégées et les entités ordinaires est leurs responsabilités. Les entités normales encapsulent le comportement et l'état, tandis que la racine d'agrégation assure également l'intégrité de toute l'agrégation en contrôlant l'accès à ses membres. C'est le seul membre d'une agrégation qui permet aux objets externes de leur contenir des références.
L'identification de la racine d'agrégation nécessite une compréhension approfondie du domaine des affaires. Il s'agit généralement d'une entité de haut niveau avec une identité globale et résume d'autres entités et valeur des objets. Par exemple, dans le monde du commerce électronique, une commande peut être une racine globale qui résume les éléments de ligne et les informations de livraison.
Le traitement d'une collection de racines agrégées peut être difficile. Il est important de se rappeler que chaque racine agrégée est une limite de cohérence, donc les modifications d'une racine agrégée ne doivent pas affecter d'autres racines agrégées. Par conséquent, lors du traitement d'une collection, il est généralement préférable de charger et de persister chaque racine agrégée séparément pour la cohérence.
Oui, une racine agrégée peut se référer à une autre racine agrégée, mais elle ne doit être référencée que par identification. Cela signifie qu'il ne doit pas contenir une référence directe à un autre objet racine agrégé, mais son ID. Cela aide à maintenir les limites de cohérence pour chaque racine agrégée.
Dans DDD, l'entreposage fournit des méthodes pour récupérer et stocker les racines agrégées. Il résume le mécanisme de stockage sous-jacent, permettant au modèle de domaine d'ignorer les détails de la persistance des données. Chaque racine agrégée a généralement son propre stockage.
Les racines d'agrégation jouent un rôle crucial dans l'exécution des règles commerciales. Il garantit que tous les changements à l'agrégation le mettent dans un état valide. Cela signifie que toutes les règles commerciales couvrant plusieurs entités ou objets de valeur doivent être appliquées par la racine agrégée.
Les racines d'agrégation aident à réduire la complexité des modèles de domaine en agissant comme des limites de cohérence et en contrôlant l'accès à leurs membres. Il simplifie le modèle en fournissant un seul point d'interaction pour chaque agrégation, ce qui facilite la compréhension du système.
Non, la racine agrégée ne doit pas faire partie de plusieurs agrégats. Cela violera les limites de cohérence des agrégats et peut entraîner des incohérences dans le modèle de domaine.
Diverses stratégies peuvent être utilisées pour faire face à des problèmes de concurrence dans les racines agrégées, telles que les verrous optimistes ou les serrures pessimistes. Le choix d'une politique dépend des exigences spécifiques de l'application et de la nature des problèmes de concurrence auxquels vous êtes confronté.
Cette sortie révisée maintient le formatage et l'emplacement d'image d'origine, paraphrase le texte pour éviter le plagiat et maintient le sens du noyau intact.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!