@alveolus/arch 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
- package/dist/docs-DcFgskuN.mjs.map +1 -0
- package/dist/index.d.mts +53 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/docs/core/application/command-handlers.md +617 -0
- package/docs/core/application/event-publishers.md +234 -0
- package/docs/core/application/event-translators.md +329 -0
- package/docs/core/application/index.md +99 -0
- package/docs/core/application/integration-events.md +277 -0
- package/docs/core/application/outbox.md +416 -0
- package/docs/core/application/query-handlers.md +292 -0
- package/docs/core/application/unit-of-work.md +352 -0
- package/docs/core/domain/aggregates.md +822 -0
- package/docs/core/domain/domain-errors.md +251 -0
- package/docs/core/domain/domain-events.md +292 -0
- package/docs/core/domain/domain-services.md +249 -0
- package/docs/core/domain/entities.md +431 -0
- package/docs/core/domain/index.md +93 -0
- package/docs/core/domain/ports.md +284 -0
- package/docs/core/domain/repositories.md +335 -0
- package/docs/core/domain/value-objects.md +425 -0
- package/docs/core/domain/views.md +265 -0
- package/docs/core/index.md +108 -0
- package/docs/core/strategic/anti-corruption-layers.md +349 -0
- package/docs/core/strategic/index.md +83 -0
- package/docs/core/strategic/open-host-services.md +287 -0
- package/docs/core/strategic/published-language.md +265 -0
- package/docs/core/utilities/result.md +413 -0
- package/docs/guide/agents.md +68 -0
- package/docs/guide/existing-project.md +108 -0
- package/docs/guide/getting-started.md +286 -0
- package/docs/guide/learning-path.md +123 -0
- package/docs/guide/project-layout.md +324 -0
- package/docs/guide/versioning.md +42 -0
- package/docs/integrations/index.md +112 -0
- package/docs/integrations/nestjs.md +169 -0
- package/docs/rules/index.md +185 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +191 -0
- package/docs/rules/layers/no-outward-import.md +186 -0
- package/docs/rules/layers/no-portless-adapter.md +123 -0
- package/docs/rules/strategic/no-cross-context-import.md +140 -0
- package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
- package/docs/rules/strategic/no-leaky-host-service.md +107 -0
- package/docs/rules/strategic/no-unmapped-context.md +114 -0
- package/docs/rules/tactical/no-aggregate-reference.md +139 -0
- package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
- package/docs/rules/tactical/no-foreign-query-dependency.md +201 -0
- package/docs/rules/tactical/no-loose-code.md +171 -0
- package/docs/rules/tactical/no-misplaced-class.md +146 -0
- package/docs/rules/tactical/no-public-field.md +113 -0
- package/docs/rules/tactical/no-stateful-service.md +102 -0
- package/docs/rules/tactical/no-thrown-failure.md +162 -0
- package/docs/rules/tooling/no-loose-disable.md +98 -0
- package/package.json +4 -3
- package/dist/cli-P5PwH9OE.mjs.map +0 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Value objects in Domain-Driven Design with TypeScript: immutable values described only by their attributes, such as an amount, an email or an identifier."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Value objects
|
|
6
|
+
|
|
7
|
+
A value object is an immutable value described only by its attributes, such as an amount or an
|
|
8
|
+
identifier.
|
|
9
|
+
|
|
10
|
+
<dl class="al-glance">
|
|
11
|
+
<dt>Layer</dt><dd>Domain</dd>
|
|
12
|
+
<dt>File</dt><dd><code>domain/value-objects/money.value-object.ts</code>, <code>domain/value-objects/order-id.identifier.ts</code></dd>
|
|
13
|
+
<dt>Extends</dt><dd><a href="#api"><code>ValueObject<Props></code></a> or <a href="#identifier"><code>Identifier<T, Tag></code></a></dd>
|
|
14
|
+
<dt>Used by</dt><dd><a href="/core/domain/aggregates">Aggregates</a>, <a href="/core/domain/entities">entities</a>, <a href="/core/domain/domain-events">domain events</a>, <a href="/core/application/command-handlers">command handlers</a></dd>
|
|
15
|
+
<dt>Checked by</dt><dd><a href="/rules/tactical/no-misplaced-class"><code>tactical/no-misplaced-class</code></a>, <a href="/rules/tactical/no-loose-code"><code>tactical/no-loose-code</code></a>, <a href="/rules/tactical/no-aggregate-reference"><code>tactical/no-aggregate-reference</code></a>, <a href="/rules/tactical/no-public-field"><code>tactical/no-public-field</code></a></dd>
|
|
16
|
+
</dl>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A price is a `number` and a currency a `string`. Every function that takes them checks that the
|
|
21
|
+
amount is not negative, or forgets to. Adding 12 EUR and 5 USD compiles. A function that expects an
|
|
22
|
+
`orderId: string` happily receives a customer id.
|
|
23
|
+
|
|
24
|
+
::: tip The fix
|
|
25
|
+
`Money` holds the amount and the currency together, refuses a negative amount once, when it is
|
|
26
|
+
created, and only adds amounts of the same currency. `OrderId` and `CustomerId` are two types: the
|
|
27
|
+
compiler refuses one where the other is expected.
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
A value object has no identity: two amounts of 12 EUR are the same amount, and one can replace the
|
|
33
|
+
other. Three properties follow:
|
|
34
|
+
|
|
35
|
+
<div class="al-cards">
|
|
36
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Valid from the start</span>A static factory checks the input and returns a <a href="/core/utilities/result"><code>Result</code></a>. An invalid value never exists.</div>
|
|
37
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Never changes</span>Its attributes are frozen. An operation returns a new value object.</div>
|
|
38
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Equal by value</span>Two value objects of the same class with equal attributes are equal.</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const price = Money.of(12, "EUR");
|
|
43
|
+
if (!price.ok) {
|
|
44
|
+
return price;
|
|
45
|
+
}
|
|
46
|
+
const total = price.value.add(shipping);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
An [identifier](#identifier) is a value object that names an entity or an aggregate.
|
|
50
|
+
|
|
51
|
+
## Where it fits
|
|
52
|
+
|
|
53
|
+
Raw values come in at the edge, as strings and numbers. The
|
|
54
|
+
[command handler](../application/command-handlers.md) turns them into value objects, and from there
|
|
55
|
+
on the domain only sees typed values.
|
|
56
|
+
|
|
57
|
+
<div class="al-diagram">
|
|
58
|
+
<svg viewBox="0 0 680 120" role="img" aria-label="A controller receives orderId as a string. The command handler wraps it in an OrderId, passes it to the repository, which loads the Order.">
|
|
59
|
+
<defs>
|
|
60
|
+
<marker id="value-object-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
61
|
+
<path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
|
|
62
|
+
</marker>
|
|
63
|
+
</defs>
|
|
64
|
+
<rect class="box" x="8" y="32" width="148" height="56" rx="8" />
|
|
65
|
+
<text class="label" x="82" y="56" text-anchor="middle">Controller</text>
|
|
66
|
+
<text class="note" x="82" y="76" text-anchor="middle">orderId: string</text>
|
|
67
|
+
<path class="link" d="M 156 60 L 178 60" marker-end="url(#value-object-flow-arrow)" />
|
|
68
|
+
<rect class="boundary" x="180" y="32" width="148" height="56" rx="8" />
|
|
69
|
+
<text class="label" x="254" y="56" text-anchor="middle">OrderId</text>
|
|
70
|
+
<text class="note" x="254" y="76" text-anchor="middle">new OrderId(…)</text>
|
|
71
|
+
<path class="link" d="M 328 60 L 350 60" marker-end="url(#value-object-flow-arrow)" />
|
|
72
|
+
<rect class="box" x="352" y="32" width="148" height="56" rx="8" />
|
|
73
|
+
<text class="label" x="426" y="56" text-anchor="middle">Orders</text>
|
|
74
|
+
<text class="note" x="426" y="76" text-anchor="middle">findById(id)</text>
|
|
75
|
+
<path class="link" d="M 500 60 L 522 60" marker-end="url(#value-object-flow-arrow)" />
|
|
76
|
+
<rect class="box" x="524" y="32" width="148" height="56" rx="8" />
|
|
77
|
+
<text class="label" x="598" y="56" text-anchor="middle">Order</text>
|
|
78
|
+
<text class="note" x="598" y="76" text-anchor="middle">holds CustomerId</text>
|
|
79
|
+
</svg>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
::: tip
|
|
83
|
+
Value objects are saved as their raw values: `Money` becomes `{ amount, currency }` in the snapshot
|
|
84
|
+
of its aggregate, and `OrderId` becomes a string.
|
|
85
|
+
:::
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { Identifier, ValueObject } from "@alveolus/core";
|
|
91
|
+
// or: from "@alveolus/core/value-objects"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### ValueObject
|
|
95
|
+
|
|
96
|
+
#### Type parameters
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
abstract class ValueObject<Props extends object> { … }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Parameter | What it is | Constraint |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `Props` | The attributes of the value object. | an object |
|
|
105
|
+
|
|
106
|
+
`AnyValueObject` is the type of any value object.
|
|
107
|
+
|
|
108
|
+
#### `constructor(props)` <Badge type="info" text="protected" /> <Badge type="tip" text="you call it" />
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
protected constructor(props: Props)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Copies and freezes the attributes. Declare your own constructor `private` and call
|
|
115
|
+
`super(props)` from it: only your factories use it.
|
|
116
|
+
|
|
117
|
+
#### `of(…)` <Badge type="info" text="static · convention" /> <Badge type="tip" text="you implement it" />
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
static of(
|
|
121
|
+
amount: number,
|
|
122
|
+
currency: Currency,
|
|
123
|
+
): Result<Money, InvalidAmount>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A factory, or several. Checks the input and returns a `Result`.
|
|
127
|
+
|
|
128
|
+
#### `props` <Badge type="info" text="protected · readonly" /> <Badge type="tip" text="inside your methods" />
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
protected readonly props: Readonly<Props>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The attributes, copied and frozen. Expose what callers need through getters.
|
|
135
|
+
|
|
136
|
+
#### `equals(other)` <Badge type="tip" text="called by anyone" />
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
equals(other: ValueObject<object>): boolean
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`true` for the same object, or the same class with deeply equal attributes. Nested value objects
|
|
143
|
+
use their own `equals`; arrays, dates and plain objects are compared by content.
|
|
144
|
+
|
|
145
|
+
### Identifier
|
|
146
|
+
|
|
147
|
+
#### Type parameters
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
abstract class Identifier<
|
|
151
|
+
T extends IdentifierValue,
|
|
152
|
+
Tag extends string = string,
|
|
153
|
+
> { … }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Parameter | What it is | Constraint |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| `T` | The raw value. | `string`, `number` or `bigint` |
|
|
159
|
+
| `Tag` | A unique name that keeps identifier types apart at compile time. It does not exist at runtime. | a string literal; any string by default |
|
|
160
|
+
|
|
161
|
+
`AnyIdentifier` is the type of any identifier, and `IdentifierValue` the type of its raw value.
|
|
162
|
+
|
|
163
|
+
#### `constructor(value)` <Badge type="tip" text="you call it" />
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
constructor(value: T)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Wraps the raw value. The constructor is public and does not validate. An identifier has no body:
|
|
170
|
+
declare the class only.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
class OrderId extends Identifier<string, "OrderId"> {}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### `value` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by anyone" />
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
readonly value: T
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The raw value.
|
|
183
|
+
|
|
184
|
+
#### `equals(other)` <Badge type="tip" text="called by anyone" />
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
equals(other: AnyIdentifier): boolean
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`true` for the same class with the same value.
|
|
191
|
+
|
|
192
|
+
#### `toString()` <Badge type="tip" text="called by anyone" />
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
toString(): string
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The value as a string.
|
|
199
|
+
|
|
200
|
+
#### `toJSON()` <Badge type="tip" text="called by JSON.stringify" />
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
toJSON(): T
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The raw value, so that an identifier serializes as its value.
|
|
207
|
+
|
|
208
|
+
::: warning Caveats
|
|
209
|
+
- `props` is frozen one level deep: arrays and objects you pass in can still be changed from
|
|
210
|
+
outside. Do not keep references to them.
|
|
211
|
+
- `equals` requires the same concrete class.
|
|
212
|
+
- The constructor of an identifier is public and does not validate: when an identifier has a
|
|
213
|
+
format, check it where it enters, such as in a driving adapter.
|
|
214
|
+
- Without a `Tag`, two identifier classes with the same raw type are interchangeable for the
|
|
215
|
+
compiler.
|
|
216
|
+
:::
|
|
217
|
+
|
|
218
|
+
## Usage
|
|
219
|
+
|
|
220
|
+
Build `Money`, a value object of the running example, then `OrderId`, an identifier. Each step shows the whole file it changes: added lines are highlighted, replaced lines are struck out.
|
|
221
|
+
|
|
222
|
+
<div class="al-cards">
|
|
223
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-declare-it">Declare it</a></span>Attributes, copied and frozen.</div>
|
|
224
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-check-it-in-a-factory">Check it in a factory</a></span>No wrong value exists.</div>
|
|
225
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-expose-reads-as-getters">Expose reads as getters</a></span>Read without changing.</div>
|
|
226
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-return-a-new-value">Return a new value</a></span>Never change in place.</div>
|
|
227
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-name-an-identity">Name an identity</a></span>An identifier for an aggregate.</div>
|
|
228
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-use-them">Use them</a></span>Build, compare, combine.</div>
|
|
229
|
+
<div class="al-card"><span class="al-card-title"><span class="al-card-step">7</span><a href="#_7-check-it">Check it</a></span>Let the rules keep it that way.</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
### 1. Declare it
|
|
233
|
+
|
|
234
|
+
A value object is described by its attributes only: they are its type parameter, read-only, and
|
|
235
|
+
`ValueObject` stores them, copied and frozen, in `props`. An attribute can itself be a value
|
|
236
|
+
object, such as `Currency`. The constructor parameters are only
|
|
237
|
+
passed to `super`, so they stay plain.
|
|
238
|
+
|
|
239
|
+
```ts [src/ordering/domain/value-objects/money.value-object.ts]
|
|
240
|
+
import { ValueObject } from "@alveolus/core";
|
|
241
|
+
|
|
242
|
+
import { Currency } from "./currency.value-object";
|
|
243
|
+
|
|
244
|
+
export class Money extends ValueObject<{
|
|
245
|
+
readonly amount: number;
|
|
246
|
+
readonly currency: Currency;
|
|
247
|
+
}> {
|
|
248
|
+
private constructor(amount: number, currency: Currency) {
|
|
249
|
+
super({ amount, currency });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 2. Check it in a factory
|
|
255
|
+
|
|
256
|
+
So that no `Money` exists with a wrong amount, the constructor is private and a factory checks the
|
|
257
|
+
input once. A refusal is a [domain error](./domain-errors.md) in a `Result`.
|
|
258
|
+
|
|
259
|
+
```ts [src/ordering/domain/value-objects/money.value-object.ts]
|
|
260
|
+
import { ValueObject } from "@alveolus/core"; // [!code --]
|
|
261
|
+
import { err, ok, type Result, ValueObject } from "@alveolus/core"; // [!code ++]
|
|
262
|
+
|
|
263
|
+
import { InvalidAmount } from "../errors/invalid-amount.error"; // [!code ++]
|
|
264
|
+
import { Currency } from "./currency.value-object";
|
|
265
|
+
|
|
266
|
+
export class Money extends ValueObject<{
|
|
267
|
+
readonly amount: number;
|
|
268
|
+
readonly currency: Currency;
|
|
269
|
+
}> {
|
|
270
|
+
private constructor(amount: number, currency: Currency) {
|
|
271
|
+
super({ amount, currency });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
static of( // [!code ++]
|
|
275
|
+
amount: number, // [!code ++]
|
|
276
|
+
currency: Currency, // [!code ++]
|
|
277
|
+
): Result<Money, InvalidAmount> { // [!code ++]
|
|
278
|
+
if (!Number.isFinite(amount) || amount < 0) { // [!code ++]
|
|
279
|
+
return err(new InvalidAmount({ amount })); // [!code ++]
|
|
280
|
+
} // [!code ++]
|
|
281
|
+
return ok(new Money(amount, currency)); // [!code ++]
|
|
282
|
+
} // [!code ++]
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 3. Expose reads as getters
|
|
287
|
+
|
|
288
|
+
`props` is protected: callers read what they need through getters, and nothing can change it.
|
|
289
|
+
|
|
290
|
+
```ts [src/ordering/domain/value-objects/money.value-object.ts]
|
|
291
|
+
import { err, ok, type Result, ValueObject } from "@alveolus/core";
|
|
292
|
+
|
|
293
|
+
import { InvalidAmount } from "../errors/invalid-amount.error";
|
|
294
|
+
import { Currency } from "./currency.value-object";
|
|
295
|
+
|
|
296
|
+
export class Money extends ValueObject<{
|
|
297
|
+
readonly amount: number;
|
|
298
|
+
readonly currency: Currency;
|
|
299
|
+
}> {
|
|
300
|
+
private constructor(amount: number, currency: Currency) {
|
|
301
|
+
super({ amount, currency });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static of(
|
|
305
|
+
amount: number,
|
|
306
|
+
currency: Currency,
|
|
307
|
+
): Result<Money, InvalidAmount> {
|
|
308
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
309
|
+
return err(new InvalidAmount({ amount }));
|
|
310
|
+
}
|
|
311
|
+
return ok(new Money(amount, currency));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get amount(): number { // [!code ++]
|
|
315
|
+
return this.props.amount; // [!code ++]
|
|
316
|
+
} // [!code ++]
|
|
317
|
+
|
|
318
|
+
get currency(): Currency { // [!code ++]
|
|
319
|
+
return this.props.currency; // [!code ++]
|
|
320
|
+
} // [!code ++]
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 4. Return a new value
|
|
325
|
+
|
|
326
|
+
A value object never changes: an operation returns a new one. Two values with the same attributes
|
|
327
|
+
are `equals`, whichever instance you hold.
|
|
328
|
+
|
|
329
|
+
```ts [src/ordering/domain/value-objects/money.value-object.ts]
|
|
330
|
+
import { err, ok, type Result, ValueObject } from "@alveolus/core";
|
|
331
|
+
|
|
332
|
+
import { InvalidAmount } from "../errors/invalid-amount.error";
|
|
333
|
+
import { Currency } from "./currency.value-object";
|
|
334
|
+
|
|
335
|
+
export class Money extends ValueObject<{
|
|
336
|
+
readonly amount: number;
|
|
337
|
+
readonly currency: Currency;
|
|
338
|
+
}> {
|
|
339
|
+
private constructor(amount: number, currency: Currency) {
|
|
340
|
+
super({ amount, currency });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
static of(
|
|
344
|
+
amount: number,
|
|
345
|
+
currency: Currency,
|
|
346
|
+
): Result<Money, InvalidAmount> {
|
|
347
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
348
|
+
return err(new InvalidAmount({ amount }));
|
|
349
|
+
}
|
|
350
|
+
return ok(new Money(amount, currency));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
get amount(): number {
|
|
354
|
+
return this.props.amount;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
get currency(): Currency {
|
|
358
|
+
return this.props.currency;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
add(other: Money): Money { // [!code ++]
|
|
362
|
+
return new Money( // [!code ++]
|
|
363
|
+
this.props.amount + other.props.amount, // [!code ++]
|
|
364
|
+
this.props.currency, // [!code ++]
|
|
365
|
+
); // [!code ++]
|
|
366
|
+
} // [!code ++]
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 5. Name an identity
|
|
371
|
+
|
|
372
|
+
An identifier is a value object that names an entity or an aggregate. It has no body, and its tag
|
|
373
|
+
keeps an `OrderId` apart from a `CustomerId` at compile time.
|
|
374
|
+
|
|
375
|
+
```ts [src/ordering/domain/value-objects/order-id.identifier.ts]
|
|
376
|
+
import { Identifier } from "@alveolus/core";
|
|
377
|
+
|
|
378
|
+
export class OrderId extends Identifier<string, "OrderId"> {}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 6. Use them
|
|
382
|
+
|
|
383
|
+
Callers build a `Money` through `of` and handle its refusal; an identifier wraps a raw value
|
|
384
|
+
directly, and is compared by value.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
const price = Money.of(12.5, currency);
|
|
388
|
+
if (!price.ok) {
|
|
389
|
+
return price;
|
|
390
|
+
}
|
|
391
|
+
const total = price.value.add(shipping);
|
|
392
|
+
const unchanged = total.equals(price.value);
|
|
393
|
+
const orderId = new OrderId(command.orderId);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 7. Check it
|
|
397
|
+
|
|
398
|
+
Run the checks. Three rules keep the value objects the way they are now:
|
|
399
|
+
|
|
400
|
+
```sh
|
|
401
|
+
npx alveolus arch check
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
<div class="al-cards">
|
|
405
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-loose-code"><code>no-loose-code</code></a></span>Behaviour on a value lives in its class, not in a free function.</div>
|
|
406
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-misplaced-class"><code>no-misplaced-class</code></a></span><code>*.value-object.ts</code> and <code>*.identifier.ts</code>, in <code>domain/value-objects/</code>.</div>
|
|
407
|
+
<div class="al-card"><span class="al-card-title"><a href="../../rules/layers/no-impure-domain"><code>no-impure-domain</code></a></span>It imports the domain only, or a package listed in <code>domainDependencies</code>.</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
An operation written as a function is reported:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
src/ordering/domain/value-objects/money.ts
|
|
414
|
+
3 error tactical/no-loose-code: The function addMoney floats outside
|
|
415
|
+
any class: make it a method of a value object or of a
|
|
416
|
+
DomainService.
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## See also
|
|
420
|
+
|
|
421
|
+
- [Entities](./entities.md) and [Aggregates](./aggregates.md), identified by identifiers
|
|
422
|
+
- [Domain errors](./domain-errors.md), returned by factories
|
|
423
|
+
- [Result](../utilities/result.md), to combine several factories
|
|
424
|
+
- Rules: [`tactical/no-loose-code`](../../rules/tactical/no-loose-code.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
|
|
425
|
+
- Vaughn Vernon, *Implementing Domain-Driven Design*, chapter 6, "Value Objects"
|