format court de la question
Est-ce conforme aux meilleures pratiques de DDD et OOP pour injecter des services lors d'appels de méthode d'entité?
exemple de format long
Disons que nous avons le cas classique Order-LineItems dans DDD, où nous avons une entité de domaine appelée une commande, qui agit également comme racine agrégée, et cette entité est composée non seulement de ses objets de valeur, mais également d'une collection d'éléments de ligne Entités.
Supposons que nous voulons une syntaxe fluide dans notre application, afin de pouvoir faire quelque chose comme ça (en notant la syntaxe à la ligne 2, où nous appelons la méthode getLineItems
):
$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
...
}
Nous ne voulons pas injecter aucune sorte de LineItemRepository dans OrderEntity, car c'est une violation de plusieurs principes auxquels je peux penser. Mais, la maîtrise de la syntaxe est quelque chose que nous voulons vraiment, car il est facile à lire et à maintenir, ainsi qu'à tester.
Considérez le code suivant, en notant la méthode getLineItems
dans OrderEntity
:
interface IOrderService {
public function getOrderByID($orderID) : OrderEntity;
public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}
class OrderService implements IOrderService {
private $orderRepository;
private $lineItemRepository;
public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
$this->orderRepository = $orderRepository;
$this->lineItemRepository = $lineItemRepository;
}
public function getOrderByID($orderID) : OrderEntity {
return $this->orderRepository->getByID($orderID);
}
public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
}
}
class OrderEntity {
private $ID;
private $lineItems;
public function getLineItems(IOrderServiceInternal $orderService) {
if(!is_null($this->lineItems)) {
$this->lineItems = $orderService->getLineItems($this);
}
return $this->lineItems;
}
}
Est-ce la façon acceptée de mettre en œuvre une syntaxe fluide dans les entités sans violer les principes fondamentaux de DDD et OOP? Pour moi, cela semble bien, car nous exposons uniquement la couche service, pas la couche infrastructure (qui est imbriquée dans le service)
C'est totalement bien pour passer un service de domaine dans un appel d'entité. Disons, nous devons calculer une somme de facture avec un algorithme compliqué qui peut dépendre, par exemple, d'un type de client. Voici à quoi cela pourrait ressembler:
class Invoice
{
private $currency;
private $customerId;
public function __construct()
{
}
public function sum(InvoiceCalculator $calculator)
{
$sum =
new SumRecord(
$calculator->calculate($this)
)
;
if ($sum->isZero()) {
$this->events->add(new ZeroSumCalculated());
}
return $sum;
}
}
Une autre approche consiste cependant à séparer une logique métier située dans le service de domaine via événements de domaine . Gardez à l'esprit que cette approche implique uniquement des services d'application différents, mais la même portée de transaction de base de données.
La troisième approche est celle que je suis en faveur: si je me retrouve à utiliser un service de domaine, cela signifie probablement que j'ai manqué un concept de domaine, car je modélise mes concepts principalement avec des noms , pas des verbes. Donc, idéalement, je pas besoin d'un service de domaine du tout et une bonne partie de toute ma logique métier réside dans décorateurs .
Je suis choqué de lire certaines des réponses ici.
Il est parfaitement valide de passer des services de domaine dans des méthodes d'entité dans DDD pour déléguer certains calculs commerciaux. Par exemple, imaginez que votre racine agrégée (une entité) a besoin d'accéder à une ressource externe via http afin de faire une logique métier et de déclencher un événement. Si vous n'injectez pas le service par la méthode commerciale de l'entité, comment le feriez-vous autrement? Souhaitez-vous instancier un client http au sein de votre entité? Cela ressemble à une idée terrible.
Ce qui est incorrect, c'est d'injecter des services dans des agrégats via son constructeur. Mais grâce à une méthode commerciale, c'est ok et parfaitement normal.
Est-ce conforme aux meilleures pratiques de DDD et OOP pour injecter des services lors d'appels de méthode d'entité?
Non, vous ne devez rien injecter dans votre couche de domaine (cela inclut les entités, les objets de valeur, les usines et les services de domaine). Cette couche doit être indépendante de tout framework, bibliothèque ou technologie tierce et ne doit effectuer aucun appel IO.
$order->getLineItems($orderService)
Cela est faux car l'agrégat ne devrait avoir besoin que d'autre chose que lui-même pour retourner les articles de la commande. L'agrégat entier doit être déjà chargé avant son appel de méthode. Si vous pensez que cela devrait être chargé paresseusement, il existe deux possibilités:
Vos limites d'agrégats sont erronées, elles sont trop grandes.
Dans ce cas d'utilisation, vous utilisez l'agrégat uniquement pour la lecture. La meilleure solution consiste à séparer le modèle d'écriture du modèle de lecture (c'est-à-dire utiliser CQRS ). Dans cette architecture plus propre , vous n'êtes pas autorisé à interroger l'agrégat mais un modèle de lecture.
L'idée clé des schémas tactiques DDD: l'application accède à toutes les données de l'application en agissant sur une racine agrégée. Cela implique que les seules entités accessibles en dehors du modèle de domaine sont les racines agrégées.
La racine d'agrégat Order ne fournirait jamais une référence à sa collection lineitem qui vous permettrait de modifier la collection, ni ne donnerait une collection de références à un élément de ligne qui vous permettrait de la modifier. Si vous souhaitez modifier l'agrégat Order, le principe hollywoodien s'applique: "Dites, ne demandez pas".
Renvoyer des valeurs de l'intérieur de l'agrégat est correct, car les valeurs sont intrinsèquement immuables; vous ne pouvez pas modifier mes données en changeant votre copie.
L'utilisation d'un service de domaine comme argument pour aider l'agrégat à fournir les valeurs correctes est une chose parfaitement raisonnable à faire.
Normalement, vous n'utiliseriez pas un service de domaine pour fournir un accès aux données qui se trouvent à l'intérieur de l'agrégat, car l'agrégat devrait déjà y avoir accès.
$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
...
}
L'orthographe est donc bizarre si nous essayons d'accéder à la collection de valeurs de l'élément de campagne de cette campagne. L'orthographe la plus naturelle serait
$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
...
}
Bien sûr, cela présuppose que les éléments de campagne ont déjà été chargés.
Le schéma habituel est que la charge de l'agrégat comprendra tout l'état requis pour le cas d'utilisation particulier. En d'autres termes, vous pouvez avoir plusieurs façons différentes de charger le même agrégat; vos méthodes de référentiel sont adaptées à l'usage .
Cette approche n'est pas quelque chose que vous trouverez dans l'Evans original, où il supposait qu'un agrégat aurait un seul modèle de données associé. Il sort plus naturellement du CQRS.
De manière générale, les objets de valeur appartenant à l'agrégat n'ont pas de référentiel par eux-mêmes. C'est la responsabilité globale de la racine de les remplir. Dans votre cas, il est de la responsabilité de votre OrderRepository de remplir à la fois les objets d'entité Order et de valeurs OrderLine.
En ce qui concerne l'implémentation de l'infrastructure de OrderRepository, dans le cas ORM, c'est une relation un-à-plusieurs, et vous pouvez choisir de charger la OrderLine avec impatience ou paresseux.
Je ne sais pas exactement ce que signifient vos services. C'est assez proche de "Application Service". Si tel est le cas, ce n'est généralement pas une bonne idée d'injecter les services à la racine agrégée/entité/objet de valeur. Le service d'application doit être le client du service racine/entité/valeur objet agrégé et service de domaine. Une autre chose à propos de vos services est que l'exposition d'objets de valeur dans Application Service n'est pas non plus une bonne idée. Ils doivent être accessibles par racine agrégée.
La réponse est: certainement NON, évitez de passer des services dans des méthodes d'entité.
La solution est simple: il suffit de laisser le référentiel Order retourner la commande avec tous ses LineItems. Dans votre cas, l'agrégat est Order + LineItems, donc si le référentiel ne renvoie pas un agrégat complet, il ne fait pas son travail.
Le principe plus large est le suivant: garder les bits fonctionnels (par exemple, la logique de domaine) séparés des bits non fonctionnels (par exemple, la persistance).
Encore une chose: si vous le pouvez, essayez d'éviter de faire cela:
$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
...
}
Faites cela à la place
$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();
Dans la conception orientée objet, nous essayons d'éviter de pêcher dans les données d'un objet. Nous préférons demander à l'objet de faire ce que nous voulons.