-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdomain-services.html
182 lines (150 loc) · 11.5 KB
/
domain-services.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>Entities</title>
<link type="text/css" rel="stylesheet" href="bootstrap.min.css" />
</head>
<body>
<ul>
<li><a href="#DocIntro">Introduction</a></li>
<li><a href="#DocBases">L'interface IDomainService et la classe DomainService</a></li>
<li><a href="#DocExample">Exemple</a><ul>
<li><a href="#DocInterface">Création d'une interface</a></li>
<li><a href="#DocImpl">Implémentation d'un Service</a></li>
<li><a href="#DocUsingFromAppService">Utilisation depuis un service applicatif</a></li>
</ul>
</li>
<li><a href="#DocDiscussions">Discutons-en!</a><ul>
<li><a href="#DocWhyNotOnlyAppServices">Pourquoi pas uniquement des services applicatifs ?</a></li>
<li><a href="#DocHowToForceDomainService">Comment forcer l'utilisation des services métiers ?</a></li>
</ul>
</li>
</ul>
<h3 id="DocIntro">Introduction</h3>
<p>Les Services Métiers (ou Services tout court, en DDD) sont utiles à l'exécution des opérations métiers selon des règles métiers. Eric Evans définit un bon "Service" s'il possède trois caractéristiques (dans son livre sur le DDD):</p>
<ol>
<li>L'<strong>opération</strong> renvoit à un <strong>concept métier</strong> qui ne fait pas partie d'une composante naturelle d'une entité ou de la valeur d'un Objet.</li>
<li>L'<strong>interface</strong> définit des éléments différents que ceux déjà définis dans l'<strong>entité métier</strong>.</li>
<li>L'opération ne gère pas d'état. <strong>(stateless)</strong>.</li>
</ol>
<p>Alors que les <a href="/Pages/Documents/Application-Services">Services Applicatifs</a> ne retournent que des DTOs (<a href="/Pages/Documents/Data-Transfer-Objects">Data
Transfer Objects</a>), un Service Métier (Domaine Service) retourne des <strong>objets métiers</strong>
(comme les <a href="/Pages/Documents/Entities">entitiés</a> ou d'autres types de valeurs). </p>
<p>Un Service Métier peut être utilisé par les Services Applicatifs et les autres Services Métiers, mais jamais directement par la couche présentation (les services applicatifs sont là pour ça)</p>
<h3 id="DocBases">L'interface IDomainService et la classe DomainService</h3>
<p>ASP.NET Boilerplate définit l'interface <strong>IDomainService </strong>qui est implémentée par tous les services métiers, par convention. Lorsqu'elle est implémentée, le service métier est <strong>automatiquement enregistré</strong> dans le gestionnaire d'<a href="/Pages/Documents/Dependency-Injection">Injection des dépendances</a> comme <strong>transient</strong>.</p>
<p>Un service métier peut (éventuellement) hériter de la classe <strong>DomainService</strong>. De cette manière, elle bénéficie de fonctionnalités intéressantes à travers certaines propriétés héritées permettant le logging,
la localisation, et d'autres choses... Il est bien entendu possible d'injecter ces différentes propriétés distinctement si vous ne souhaitez pas hériter de DomainService.</p>
<h3 id="DocExample">Exemple</h3>
<p>Imaginons que nous avons un système de gestion de tâches et que nous avons des règles métiers à appliquer lorsqu'une tâche est assignée à une personne.</p>
<h4 id="DocInterface">Création d'une interface</h4>
<p>Nous devons d'abord définir une interface pour le service (non obligatoire mais c'est une bonne pratique):</p>
<pre lang="cs">public interface ITaskManager : IDomainService
{
void AssignTaskToPerson(Task task, Person person);
}</pre>
<p>Comme vous le voyez, le service <strong>TaskManager</strong> manipule des objets métiers : <strong>Task</strong> et <strong>Person</strong>. Il y a quelques convention à observer dans le nommage des services métiers. Cela peut être TaskManager, TaskService ou
TaskDomainService...</p>
<h4 id="DocImpl">Implémentation du Service</h4>
<p>Voyons l'implémentation suivante:</p>
<pre lang="cs">public class TaskManager : DomainService, ITaskManager
{
public const int MaxActiveTaskCountForAPerson = 3;
private readonly ITaskRepository _taskRepository;
public TaskManager(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
public void AssignTaskToPerson(Task task, Person person)
{
if (task.AssignedPersonId == person.Id)
{
return;
}
if (task.State != TaskState.Active)
{
throw new ApplicationException("Can not assign a task to a person when task is not active!");
}
if (HasPersonMaximumAssignedTask(person))
{
throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name));
}
task.AssignedPersonId = person.Id;
}
private bool HasPersonMaximumAssignedTask(Person person)
{
var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id);
return assignedTaskCount >= MaxActiveTaskCountForAPerson;
}
}</pre>
<p>Nous avons deux règles métiers ici:</p>
<ul>
<li>Une tâche devrait être en <strong>Etat Actif</strong> pour pouvoir être assignée à une nouvelle personne.</li>
<li>Une personne peut avoir <strong>3 taches actives au maximum</strong>.</li>
</ul>
<p>Vous vous demandez peut être pourquoi je déclenche une erreur de type <strong>ApplicationException</strong> lors de la première vérification et une erreur <strong>UserFriendlyException</strong> pour la seconde vérification. (reportez vous à la documentation <a href="/Pages/Documents/Handling-Exceptions">gestion des erreurs</a>). Il n'y a pas de rapport avec les services métiers. Je les ai juste ajoutés pour que l'exemple soit plus intéressant. j'ai fait en sorte que cette interface utilisateur puisse vérifier l'état de la tâche pour ne pas l'assigner si son état n'est pas cohérent. Je pense qu'il s'agit d'une erreur applicative et que nous devrions la cacher à l'utilisateur. La seconde est plus difficile à vérifier pour l'IHM et il est donc préférable de montrer un message approprié à l'utilisateur. Quoi qu'il en soit, cette digression est juste un exemple!</p>
<h4 id="DocUsingFromAppService">L'utilisation depuis le service applicatif</h4>
<p>Voyons maintenant comment utiliser TaskManager depuis le service applicatif:</p>
<pre lang="cs">public class TaskAppService : ApplicationService, ITaskAppService
{
private readonly IRepository<Task, long> _taskRepository;
private readonly IRepository<Person> _personRepository;
private readonly ITaskManager _taskManager;
public TaskAppService(IRepository<Task, long> taskRepository, IRepository<Person> personRepository , ITaskManager taskManager)
{
_taskRepository = taskRepository;
_personRepository = personRepository;
_taskManager = taskManager;
}
public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
var person = _personRepository.Get(input.PersonId);
_taskManager.AssignTaskToPerson(task, person);
}
}</pre>
<p>Le <strong>Service Applicatif</strong> Task travaille à partir d'un <strong>DTO</strong> (input) et des
<strong>entrepôts,</strong> <strong>task</strong> et <strong>person,</strong> et les transmets au <strong>Task Manager</strong> (le service métier).</p>
<h3 id="DocDiscussions">Discutons-en!</h3>
<p>Après avoir lu les exemples précédents, vous pourriez vous poser quelques questions.</p>
<h4 id="DocWhyNotOnlyAppServices">Pourquoi pas uniquement des services applicatifs?</h4>
<p>Vous pouvez vous demander pourquoi on ne se satisferait pas uniquement des services métiers puisque les services applicatifs n'implémentent pas la logique métier? </p>
<p>Nous pouvons simplement répondre que ce n'est pas une tâche de service applicatif. Parce que ce n'est pas un <strong>cas d'usage</strong>, mais plutôt une <strong>opération métier</strong>.
Nous pourrions souhaiter utiliser la même tâche 'assigner une tâche à un utilisateur' <strong>mais dans un cas d'usage différent</strong>
. Imaginons que nous pourrions avoir une <strong>autre zone de l'IHM</strong> pour mettre à jour une tâche et que cette mise à jour permette d'assigner la tâche à une autre personne. Nous avons donc deux cas d'usage qui utilise la même logique métier. De la même maniètre nous pourrions avoir <strong> 2 IHM différentes</strong> (une application mobile et une application web, par exemple) qui partagent la même logique métier.</p>
<p>Si votre logique métier est simple et qu'elle ne met en oeuvre qu'une IHM par usage (comme l'assignation d'une tâche à une personne qui ne serait possible qu'à un seul endroit), vous pouvez alors vous passer d'implémenter des services métiers et coder directement la logique métier dans les services applicatifs. Ce n'est pas une bonne pratique de DDD, mais ASP.NET
Boilerplate ne vous force pas à appliquer obligatoirement ce choix d'architecture.</p>
<h4 id="DocHowToForceDomainService">Comment forcer l'utilisation des services métiers?</h4>
<p>Voyez ce qu'un service applicatif peut faire:</p>
<pre lang="cs">public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
task.AssignedPersonId = input.PersonId;
}</pre>
<p>Le développeur qui a écrit ce service applicatif pourrait ne pas avoir connaissance de ce qu'est <strong>TaskManager</strong> et affecter directement le <strong>PersonId</strong> à la tâche, via
<strong>AssignedPersonId</strong>. Est-il possible de <strong>l'éviter</strong>? Il y a différents débats entre spécialistes du DDD sur les différentes manières de gérer cela. Nous n'entrerons pas dans le détail. Mais, nous utiliserons une façon simple de résoudre ce problème.</p>
<p>Nous allons en effet modifier l'Entité <strong>Task</strong>:</p>
<pre lang="cs">public class Task : Entity<long>
{
public virtual int? AssignedPersonId { get; protected set; }
//...other members and codes of Task entity
public void AssignToPerson(Person person, ITaskPolicy taskPolicy)
{
taskPolicy.CheckIfCanAssignTaskToPerson(this, person);
AssignedPersonId = person.Id;
}
}</pre>
<p>Nous avons modifé le setter d'<strong>AssignedPersonId</strong> en <strong>protected</strong>.
De cette manière, il ne peut pas être modifié en dehors de cette classe mais seulement depuis la méthode ajoutée<strong>
AssignToPerson</strong> prennant une <em>person</em> et une <em>task policy </em>en arguments. La méthode <strong>
CheckIfCanAssignTaskToPerson</strong> vérifie que l'affectation respecte la logique métier et déclenchera une erreur explicite le cas échéant. La méthode du service applicatif sera donc modifiée comme suit:</p>
<pre lang="cs">public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
var person = _personRepository.Get(input.PersonId);
task.AssignToPerson(person, _taskPolicy);
}</pre>
<p>Voilà, il n'est plus possible d'affecter directement une tâche à une personne. Nous devrions toujours utiliser AssignToPerson afin de ne pas s'affrachir de la logique métier.</p>
</body>
</html>