@vived/core 2.0.0 → 2.0.2
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 +275 -65
- package/dist/cjs/AppObject/AppObjectEntityRepo.js +16 -2
- package/dist/cjs/AppObject/AppObjectEntityRepo.js.map +1 -1
- package/dist/cjs/ExampleFeature/Entities/ExampleRepo.js +3 -4
- package/dist/cjs/ExampleFeature/Entities/ExampleRepo.js.map +1 -1
- package/dist/cjs/ExampleFeature/index.js +23 -0
- package/dist/cjs/ExampleFeature/index.js.map +1 -0
- package/dist/esm/AppObject/AppObjectEntityRepo.js +16 -2
- package/dist/esm/AppObject/AppObjectEntityRepo.js.map +1 -1
- package/dist/esm/ExampleFeature/Entities/ExampleRepo.js +3 -4
- package/dist/esm/ExampleFeature/Entities/ExampleRepo.js.map +1 -1
- package/dist/esm/ExampleFeature/index.js +7 -0
- package/dist/esm/ExampleFeature/index.js.map +1 -0
- package/dist/types/AppObject/AppObjectEntityRepo.d.ts +9 -1
- package/dist/types/AppObject/AppObjectEntityRepo.d.ts.map +1 -1
- package/dist/types/ExampleFeature/Entities/ExampleRepo.d.ts +1 -1
- package/dist/types/ExampleFeature/Entities/ExampleRepo.d.ts.map +1 -1
- package/dist/types/ExampleFeature/index.d.ts +5 -0
- package/dist/types/ExampleFeature/index.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/AppObject/README.md +476 -0
- package/src/DomainFactories/README.md +154 -0
- package/src/Entities/README.md +340 -0
- package/src/ExampleFeature/README.md +804 -0
- package/src/Types/README.md +549 -0
- package/src/Utilities/README.md +478 -0
- package/src/ValueObjects/README.md +552 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
# ExampleFeature
|
|
2
|
+
|
|
3
|
+
The ExampleFeature folder provides a complete, working example of how to implement a domain feature using the AppObject architecture and Clean Code dependency rules. This example demonstrates the proper structure, patterns, and practices for building maintainable, testable features.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This feature demonstrates:
|
|
8
|
+
- **Clean Architecture layers** - Entities, Use Cases, Presentation Managers, Controllers, and Adapters
|
|
9
|
+
- **AppObject pattern** - Component-based architecture with dependency injection
|
|
10
|
+
- **Singleton vs Instance patterns** - When to use global vs per-instance components
|
|
11
|
+
- **Observable pattern** - Automatic UI updates when data changes
|
|
12
|
+
- **Domain-driven design** - Organized by feature, not by technical layer
|
|
13
|
+
- **Test-driven development** - Comprehensive test coverage for all components
|
|
14
|
+
|
|
15
|
+
## Architecture Layers
|
|
16
|
+
|
|
17
|
+
### Dependency Flow (Clean Architecture)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
UI Layer (React/Vue/etc.)
|
|
21
|
+
↓ (depends on)
|
|
22
|
+
Adapters Layer
|
|
23
|
+
↓ (depends on)
|
|
24
|
+
Controllers Layer
|
|
25
|
+
↓ (depends on)
|
|
26
|
+
Presentation Managers (PMs)
|
|
27
|
+
↓ (depends on)
|
|
28
|
+
Use Cases (UCs)
|
|
29
|
+
↓ (depends on)
|
|
30
|
+
Entities Layer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Key Rule:** Dependencies only flow inward. Inner layers never depend on outer layers.
|
|
34
|
+
|
|
35
|
+
## Folder Structure
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
ExampleFeature/
|
|
39
|
+
├── index.ts # Barrel file exporting all controllers and adapters
|
|
40
|
+
├── Entities/ # Domain models (innermost layer)
|
|
41
|
+
├── UCs/ # Business logic (Use Cases)
|
|
42
|
+
├── PMs/ # Presentation logic (View Models)
|
|
43
|
+
├── Controllers/ # Simplified API for UI
|
|
44
|
+
├── Adapters/ # UI framework integration
|
|
45
|
+
├── Factory/ # Domain initialization
|
|
46
|
+
└── Mocks/ # Test doubles
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Note:** The root `index.ts` barrel file exports all controllers and adapters, providing a single import point for consuming code.
|
|
50
|
+
|
|
51
|
+
## Layer-by-Layer Guide
|
|
52
|
+
|
|
53
|
+
### 1. Entities Layer
|
|
54
|
+
|
|
55
|
+
Entities store and manage domain data, tracking state changes and notifying observers.
|
|
56
|
+
|
|
57
|
+
**ExampleEntity.ts** - Instance-based entity (multiple instances possible):
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { AppObject, AppObjectEntity } from "@vived/core";
|
|
61
|
+
import { MemoizedString } from "@vived/core";
|
|
62
|
+
|
|
63
|
+
export abstract class ExampleEntity extends AppObjectEntity
|
|
64
|
+
{
|
|
65
|
+
static readonly type = "ExampleEntityType";
|
|
66
|
+
|
|
67
|
+
abstract get aStringProperty(): string;
|
|
68
|
+
abstract set aStringProperty(val: string);
|
|
69
|
+
|
|
70
|
+
static getById(id: string, appObjects: AppObjectRepo): ExampleEntity | undefined
|
|
71
|
+
{
|
|
72
|
+
return appObjects.get(id)?.getComponent<ExampleEntity>(this.type);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function makeExampleEntity(appObject: AppObject): ExampleEntity
|
|
77
|
+
{
|
|
78
|
+
return new ExampleEntityImp(appObject);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class ExampleEntityImp extends ExampleEntity
|
|
82
|
+
{
|
|
83
|
+
private memoizedString = new MemoizedString("", this.notifyOnChange);
|
|
84
|
+
|
|
85
|
+
get aStringProperty()
|
|
86
|
+
{
|
|
87
|
+
return this.memoizedString.val;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set aStringProperty(val: string)
|
|
91
|
+
{
|
|
92
|
+
this.memoizedString.val = val; // Automatically notifies observers
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
constructor(appObject: AppObject)
|
|
96
|
+
{
|
|
97
|
+
super(appObject, ExampleEntity.type);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**ExampleSingletonEntity.ts** - Singleton entity (only one instance globally):
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { AppObjectSingletonEntity, getSingletonComponent } from "@vived/core";
|
|
106
|
+
import { MemoizedBoolean } from "@vived/core";
|
|
107
|
+
|
|
108
|
+
export abstract class SingletonEntityExample extends AppObjectSingletonEntity
|
|
109
|
+
{
|
|
110
|
+
static readonly type = "SingletonEntityExampleType";
|
|
111
|
+
|
|
112
|
+
abstract get aBoolProperty(): boolean;
|
|
113
|
+
abstract set aBoolProperty(val: boolean);
|
|
114
|
+
|
|
115
|
+
static get(appObjects: AppObjectRepo): SingletonEntityExample | undefined
|
|
116
|
+
{
|
|
117
|
+
return getSingletonComponent(SingletonEntityExample.type, appObjects);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class SingletonEntityExampleImp extends SingletonEntityExample
|
|
122
|
+
{
|
|
123
|
+
private memoizedBoolean = new MemoizedBoolean(false, this.notifyOnChange);
|
|
124
|
+
|
|
125
|
+
get aBoolProperty()
|
|
126
|
+
{
|
|
127
|
+
return this.memoizedBoolean.val;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set aBoolProperty(val: boolean)
|
|
131
|
+
{
|
|
132
|
+
this.memoizedBoolean.val = val;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
constructor(appObject: AppObject)
|
|
136
|
+
{
|
|
137
|
+
super(appObject, SingletonEntityExample.type);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**ExampleRepo.ts** - Repository for managing collections:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { AppObjectEntityRepo } from "@vived/core";
|
|
146
|
+
|
|
147
|
+
export abstract class ExampleRepo extends AppObjectEntityRepo<ExampleEntity>
|
|
148
|
+
{
|
|
149
|
+
static readonly type = "ExampleRepoType";
|
|
150
|
+
abstract deleteExampleEntity(id: string): void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class ExampleRepoImp extends ExampleRepo
|
|
154
|
+
{
|
|
155
|
+
entityFactory(appObject: AppObject): ExampleEntity
|
|
156
|
+
{
|
|
157
|
+
return makeExampleEntity(appObject);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
deleteExampleEntity(id: string): void
|
|
161
|
+
{
|
|
162
|
+
this.remove(id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
constructor(appObject: AppObject)
|
|
166
|
+
{
|
|
167
|
+
super(appObject, ExampleRepo.type);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Key Concepts:**
|
|
173
|
+
- Use `MemoizedString`, `MemoizedBoolean`, etc. for automatic change detection
|
|
174
|
+
- Extend `AppObjectEntity` for instance-based entities
|
|
175
|
+
- Extend `AppObjectSingletonEntity` for global entities
|
|
176
|
+
- Use repositories to manage collections of entities
|
|
177
|
+
|
|
178
|
+
### 2. Use Cases (UCs) Layer
|
|
179
|
+
|
|
180
|
+
UCs encapsulate business logic and operations that modify entities.
|
|
181
|
+
|
|
182
|
+
**EditExampleStringUC.ts** - Instance-based UC:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { AppObjectUC } from "@vived/core";
|
|
186
|
+
|
|
187
|
+
export abstract class EditExampleStringUC extends AppObjectUC
|
|
188
|
+
{
|
|
189
|
+
static readonly type = "EditExampleStringUCType";
|
|
190
|
+
abstract editExampleString(text: string): void;
|
|
191
|
+
|
|
192
|
+
static getById(id: string, appObjects: AppObjectRepo): EditExampleStringUC | undefined
|
|
193
|
+
{
|
|
194
|
+
return appObjects.get(id)?.getComponent<EditExampleStringUC>(this.type);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class EditSlideTextUCImp extends EditExampleStringUC
|
|
199
|
+
{
|
|
200
|
+
private get exampleEntity()
|
|
201
|
+
{
|
|
202
|
+
return this.getCachedLocalComponent<ExampleEntity>(ExampleEntity.type);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
editExampleString = (text: string) => {
|
|
206
|
+
this.exampleEntity.aStringProperty = text;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
constructor(appObject: AppObject)
|
|
210
|
+
{
|
|
211
|
+
super(appObject, EditExampleStringUC.type);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**ToggleExampleBooleanUC.ts** - Singleton UC:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { AppObjectSingletonUC, getSingletonComponent } from "@vived/core";
|
|
220
|
+
|
|
221
|
+
export abstract class ToggleExampleBooleanUC extends AppObjectSingletonUC
|
|
222
|
+
{
|
|
223
|
+
static readonly type = "ToggleExampleBooleanUCType";
|
|
224
|
+
abstract toggleExampleBoolean(): void;
|
|
225
|
+
|
|
226
|
+
static get(appObjects: AppObjectRepo): ToggleExampleBooleanUC | undefined
|
|
227
|
+
{
|
|
228
|
+
return getSingletonComponent(ToggleExampleBooleanUC.type, appObjects);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
class ToggleExampleBooleanUCImp extends ToggleExampleBooleanUC
|
|
233
|
+
{
|
|
234
|
+
private get singletonEntityExample()
|
|
235
|
+
{
|
|
236
|
+
return this.getCachedSingleton<SingletonEntityExample>(
|
|
237
|
+
SingletonEntityExample.type
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
toggleExampleBoolean = () => {
|
|
242
|
+
this.singletonEntityExample.aBoolProperty =
|
|
243
|
+
!this.singletonEntityExample.aBoolProperty;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
constructor(appObject: AppObject)
|
|
247
|
+
{
|
|
248
|
+
super(appObject, ToggleExampleBooleanUC.type);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Key Concepts:**
|
|
254
|
+
- UCs contain business logic, not presentation logic
|
|
255
|
+
- Use `getCachedLocalComponent()` for components on the same AppObject
|
|
256
|
+
- Use `getCachedSingleton()` for singleton components
|
|
257
|
+
- Methods should be named after the action they perform
|
|
258
|
+
|
|
259
|
+
### 3. Presentation Managers (PMs) Layer
|
|
260
|
+
|
|
261
|
+
PMs transform entity data into view models for the UI.
|
|
262
|
+
|
|
263
|
+
**ExamplePM.ts** - Instance-based PM:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { AppObjectPM } from "@vived/core";
|
|
267
|
+
|
|
268
|
+
export abstract class ExamplePM extends AppObjectPM<string>
|
|
269
|
+
{
|
|
270
|
+
static readonly type = "ExamplePMType";
|
|
271
|
+
|
|
272
|
+
static getById(id: string, appObjects: AppObjectRepo): ExamplePM | undefined
|
|
273
|
+
{
|
|
274
|
+
return appObjects.get(id)?.getComponent<ExamplePM>(ExamplePM.type);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
class ExamplePMImp extends ExamplePM
|
|
279
|
+
{
|
|
280
|
+
readonly defaultVM = "";
|
|
281
|
+
|
|
282
|
+
private get exampleEntity()
|
|
283
|
+
{
|
|
284
|
+
return this.getCachedLocalComponent<ExampleEntity>(ExampleEntity.type);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
vmsAreEqual(a: string, b: string): boolean
|
|
288
|
+
{
|
|
289
|
+
return a === b;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
formVM = () => {
|
|
293
|
+
// Transform entity data into view model
|
|
294
|
+
this.doUpdateView(this.exampleEntity.aStringProperty);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
constructor(appObject: AppObject)
|
|
298
|
+
{
|
|
299
|
+
super(appObject, ExamplePM.type);
|
|
300
|
+
this.observeEntity(this.exampleEntity); // Subscribe to changes
|
|
301
|
+
this.formVM(); // Initial view model
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**ExampleSingletonPM.ts** - Singleton PM:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { AppObjectSingletonPM, getSingletonComponent } from "@vived/core";
|
|
310
|
+
|
|
311
|
+
export interface ExampleVM
|
|
312
|
+
{
|
|
313
|
+
aBoolProperty: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export abstract class ExampleSingletonPM extends AppObjectSingletonPM<ExampleVM>
|
|
317
|
+
{
|
|
318
|
+
static readonly type = "ExampleSingletonPMType";
|
|
319
|
+
|
|
320
|
+
static get(appObjects: AppObjectRepo): ExampleSingletonPM | undefined
|
|
321
|
+
{
|
|
322
|
+
return getSingletonComponent(ExampleSingletonPM.type, appObjects);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const defaultExampleVM: ExampleVM = {
|
|
327
|
+
aBoolProperty: true
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
class ExampleSingletonPMImp extends ExampleSingletonPM
|
|
331
|
+
{
|
|
332
|
+
private get exampleEntity()
|
|
333
|
+
{
|
|
334
|
+
return this.getCachedSingleton<SingletonEntityExample>(
|
|
335
|
+
SingletonEntityExample.type
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
formVM = () => {
|
|
340
|
+
const vm: ExampleVM = {
|
|
341
|
+
aBoolProperty: this.exampleEntity.aBoolProperty
|
|
342
|
+
};
|
|
343
|
+
this.doUpdateView(vm);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
vmsAreEqual(a: ExampleVM, b: ExampleVM): boolean
|
|
347
|
+
{
|
|
348
|
+
return a.aBoolProperty === b.aBoolProperty;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
constructor(appObject: AppObject)
|
|
352
|
+
{
|
|
353
|
+
super(appObject, ExampleSingletonPM.type);
|
|
354
|
+
this.observeEntity(this.exampleEntity);
|
|
355
|
+
this.formVM();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Key Concepts:**
|
|
361
|
+
- PMs observe entities and call `formVM()` when they change
|
|
362
|
+
- Define view model interfaces for complex data structures
|
|
363
|
+
- Implement `vmsAreEqual()` to prevent unnecessary UI updates
|
|
364
|
+
- Call `doUpdateView()` to notify UI components
|
|
365
|
+
|
|
366
|
+
### 4. Controllers Layer
|
|
367
|
+
|
|
368
|
+
Controllers provide a simplified API for UI components.
|
|
369
|
+
|
|
370
|
+
**setExampleText.ts** - Instance-based controller:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { AppObjectRepo } from "@vived/core";
|
|
374
|
+
import { EditExampleStringUC } from "../UCs/EditExampleStringUC";
|
|
375
|
+
|
|
376
|
+
export function setExampleText(
|
|
377
|
+
text: string,
|
|
378
|
+
id: string,
|
|
379
|
+
appObjects: AppObjectRepo
|
|
380
|
+
)
|
|
381
|
+
{
|
|
382
|
+
const uc = EditExampleStringUC.getById(id, appObjects);
|
|
383
|
+
|
|
384
|
+
if (!uc)
|
|
385
|
+
{
|
|
386
|
+
appObjects.submitWarning(
|
|
387
|
+
"setExampleText",
|
|
388
|
+
"Unable to find EditExampleStringUC"
|
|
389
|
+
);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
uc.editExampleString(text);
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**toggleExampleBoolean.ts** - Singleton controller:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import { AppObjectRepo } from "@vived/core";
|
|
401
|
+
import { ToggleExampleBooleanUC } from "../UCs/ToggleExampleBooleanUC";
|
|
402
|
+
|
|
403
|
+
export function toggleExampleBoolean(appObjects: AppObjectRepo)
|
|
404
|
+
{
|
|
405
|
+
const uc = ToggleExampleBooleanUC.get(appObjects);
|
|
406
|
+
|
|
407
|
+
if (!uc)
|
|
408
|
+
{
|
|
409
|
+
appObjects.submitWarning(
|
|
410
|
+
"toggleExampleBoolean",
|
|
411
|
+
"Unable to find ToggleExampleBooleanUC"
|
|
412
|
+
);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
uc.toggleExampleBoolean();
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Key Concepts:**
|
|
421
|
+
- Controllers are simple functions, not classes
|
|
422
|
+
- They find the appropriate UC and call its methods
|
|
423
|
+
- They handle errors gracefully with warnings
|
|
424
|
+
- They simplify the API for UI components
|
|
425
|
+
|
|
426
|
+
### 5. Adapters Layer
|
|
427
|
+
|
|
428
|
+
Adapters connect UI frameworks to PMs.
|
|
429
|
+
|
|
430
|
+
**examplePmAdapter.ts** - Instance-based adapter:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { PmAdapter } from "@vived/core";
|
|
434
|
+
import { ExamplePM } from "../PMs/ExamplePM";
|
|
435
|
+
|
|
436
|
+
export const examplePmAdapter: PmAdapter<string> = {
|
|
437
|
+
defaultVM: "",
|
|
438
|
+
|
|
439
|
+
subscribe: (id: string, appObjects: AppObjectRepo, setVM: (vm: string) => void) => {
|
|
440
|
+
if (!id) return;
|
|
441
|
+
|
|
442
|
+
const pm = ExamplePM.getById(id, appObjects);
|
|
443
|
+
if (!pm)
|
|
444
|
+
{
|
|
445
|
+
appObjects.submitWarning("examplePmAdapter", "Unable to find ExamplePM");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
pm.addView(setVM);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
unsubscribe: (id: string, appObjects: AppObjectRepo, setVM: (vm: string) => void) => {
|
|
453
|
+
ExamplePM.getById(id, appObjects)?.removeView(setVM);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**exampleSingletonPmAdapter.ts** - Singleton adapter:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { SingletonPmAdapter } from "@vived/core";
|
|
462
|
+
import { ExampleSingletonPM, ExampleVM, defaultExampleVM } from "../PMs/ExampleSingletonPM";
|
|
463
|
+
|
|
464
|
+
export const exampleSingletonPmAdapter: SingletonPmAdapter<ExampleVM> = {
|
|
465
|
+
defaultVM: defaultExampleVM,
|
|
466
|
+
|
|
467
|
+
subscribe: (appObjects: AppObjectRepo, setVM: (vm: ExampleVM) => void) => {
|
|
468
|
+
const pm = ExampleSingletonPM.get(appObjects);
|
|
469
|
+
if (!pm)
|
|
470
|
+
{
|
|
471
|
+
appObjects.submitWarning(
|
|
472
|
+
"exampleSingletonPmAdapter",
|
|
473
|
+
"Unable to find ExampleSingletonPM"
|
|
474
|
+
);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
pm.addView(setVM);
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
unsubscribe: (appObjects: AppObjectRepo, setVM: (vm: ExampleVM) => void) => {
|
|
482
|
+
ExampleSingletonPM.get(appObjects)?.removeView(setVM);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Key Concepts:**
|
|
488
|
+
- Adapters implement `PmAdapter<VM>` or `SingletonPmAdapter<VM>`
|
|
489
|
+
- They provide a `defaultVM` for initial rendering
|
|
490
|
+
- They handle subscription and unsubscription lifecycle
|
|
491
|
+
|
|
492
|
+
### 6. Factory
|
|
493
|
+
|
|
494
|
+
The factory initializes all components in the correct order.
|
|
495
|
+
|
|
496
|
+
**ExampleFeatureFactory.ts**:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { DomainFactory } from "@vived/core";
|
|
500
|
+
|
|
501
|
+
export class ExampleFeatureFactory extends DomainFactory
|
|
502
|
+
{
|
|
503
|
+
factoryName = "ExampleFeatureFactory";
|
|
504
|
+
|
|
505
|
+
setupEntities(): void
|
|
506
|
+
{
|
|
507
|
+
// Create singleton entities
|
|
508
|
+
makeSingletonEntityExample(this.appObject);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
setupUCs(): void
|
|
512
|
+
{
|
|
513
|
+
// Create use cases
|
|
514
|
+
makeToggleExampleBooleanUC(this.appObject);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
setupPMs(): void
|
|
518
|
+
{
|
|
519
|
+
// Create presentation managers
|
|
520
|
+
makeExampleSingletonPM(this.appObject);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
finalSetup(): void
|
|
524
|
+
{
|
|
525
|
+
// Any final initialization
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function makeExampleFeatureFactory(appObjects: AppObjectRepo)
|
|
530
|
+
{
|
|
531
|
+
const appObject = appObjects.getOrCreate("ExampleFeature");
|
|
532
|
+
return new ExampleFeatureFactory(appObject);
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Key Concepts:**
|
|
537
|
+
- Factories initialize components in order: Entities → UCs → PMs → Final
|
|
538
|
+
- This ensures dependencies are available when needed
|
|
539
|
+
- Each feature has its own factory
|
|
540
|
+
|
|
541
|
+
## React Integration Example
|
|
542
|
+
|
|
543
|
+
### Using Instance-Based Components
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { useEffect, useState } from "react";
|
|
547
|
+
import { examplePmAdapter, setExampleText } from "../ExampleFeature";
|
|
548
|
+
|
|
549
|
+
function ExampleTextInput({ id, appObjects })
|
|
550
|
+
{
|
|
551
|
+
const [viewModel, setViewModel] = useState(examplePmAdapter.defaultVM);
|
|
552
|
+
|
|
553
|
+
useEffect(() => {
|
|
554
|
+
// Subscribe to PM updates
|
|
555
|
+
examplePmAdapter.subscribe(id, appObjects, setViewModel);
|
|
556
|
+
|
|
557
|
+
// Cleanup on unmount
|
|
558
|
+
return () => {
|
|
559
|
+
examplePmAdapter.unsubscribe(id, appObjects, setViewModel);
|
|
560
|
+
};
|
|
561
|
+
}, [id]);
|
|
562
|
+
|
|
563
|
+
const handleChange = (e) => {
|
|
564
|
+
setExampleText(e.target.value, id, appObjects);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<input
|
|
569
|
+
type="text"
|
|
570
|
+
value={viewModel}
|
|
571
|
+
onChange={handleChange}
|
|
572
|
+
/>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Using Singleton Components
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { useEffect, useState } from "react";
|
|
581
|
+
import { exampleSingletonPmAdapter } from "../Adapters";
|
|
582
|
+
import { toggleExampleBoolean } fr, toggleExampleBoolean } from "../ExampleFeature
|
|
583
|
+
function ExampleToggle({ appObjects })
|
|
584
|
+
{
|
|
585
|
+
const [viewModel, setViewModel] = useState(
|
|
586
|
+
exampleSingletonPmAdapter.defaultVM
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
// Subscribe to singleton PM updates (no ID needed)
|
|
591
|
+
exampleSingletonPmAdapter.subscribe(appObjects, setViewModel);
|
|
592
|
+
|
|
593
|
+
// Cleanup on unmount
|
|
594
|
+
return () => {
|
|
595
|
+
exampleSingletonPmAdapter.unsubscribe(appObjects, setViewModel);
|
|
596
|
+
};
|
|
597
|
+
}, []);
|
|
598
|
+
|
|
599
|
+
const handleToggle = () => {
|
|
600
|
+
toggleExampleBoolean(appObjects);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<div>
|
|
605
|
+
<p>Boolean value: {viewModel.aBoolProperty ? "True" : "False"}</p>
|
|
606
|
+
<button onClick={handleToggle}>Toggle</button>
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Testing Examples
|
|
613
|
+
|
|
614
|
+
### Testing Entities
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
describe("ExampleEntity", () => {
|
|
618
|
+
it("notifies observers when property changes", () => {
|
|
619
|
+
const appObject = appObjects.getOrCreate("test");
|
|
620
|
+
const entity = makeExampleEntity(appObject);
|
|
621
|
+
const observer = jest.fn();
|
|
622
|
+
|
|
623
|
+
entity.addObserver(observer);
|
|
624
|
+
entity.aStringProperty = "new value";
|
|
625
|
+
|
|
626
|
+
expect(observer).toHaveBeenCalled();
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Testing Use Cases
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
describe("EditExampleStringUC", () => {
|
|
635
|
+
it("updates entity string property", () => {
|
|
636
|
+
const appObject = appObjects.getOrCreate("test");
|
|
637
|
+
const entity = makeExampleEntity(appObject);
|
|
638
|
+
const uc = makeEditSlideTextUC(appObject);
|
|
639
|
+
|
|
640
|
+
uc.editExampleString("test text");
|
|
641
|
+
|
|
642
|
+
expect(entity.aStringProperty).toBe("test text");
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Testing Presentation Managers
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
describe("ExamplePM", () => {
|
|
651
|
+
it("provides updated view model when entity changes", () => {
|
|
652
|
+
const appObject = appObjects.getOrCreate("test");
|
|
653
|
+
const entity = makeExampleEntity(appObject);
|
|
654
|
+
const pm = makeExamplePM(appObject);
|
|
655
|
+
const viewCallback = jest.fn();
|
|
656
|
+
|
|
657
|
+
pm.addView(viewCallback);
|
|
658
|
+
entity.aStringProperty = "new value";
|
|
659
|
+
|
|
660
|
+
expect(viewCallback).toHaveBeenCalledWith("new value");
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## Design Patterns Used
|
|
666
|
+
|
|
667
|
+
### 1. Clean Architecture
|
|
668
|
+
- Dependency inversion: inner layers don't depend on outer layers
|
|
669
|
+
- Separation of concerns: each layer has a specific responsibility
|
|
670
|
+
|
|
671
|
+
### 2. Observer Pattern
|
|
672
|
+
- Entities notify observers when they change
|
|
673
|
+
- PMs observe entities and update views
|
|
674
|
+
- UI components observe PMs and update themselves
|
|
675
|
+
|
|
676
|
+
### 3. Repository Pattern
|
|
677
|
+
- Repositories manage collections of entities
|
|
678
|
+
- Provide creation, retrieval, and deletion operations
|
|
679
|
+
|
|
680
|
+
### 4. Factory Pattern
|
|
681
|
+
- Factories create and initialize domain components
|
|
682
|
+
- Ensure proper setup order and dependencies
|
|
683
|
+
|
|
684
|
+
### 5. Adapter Pattern
|
|
685
|
+
- Adapters connect UI frameworks to the application
|
|
686
|
+
- Provide framework-agnostic integration
|
|
687
|
+
|
|
688
|
+
## Best Practices
|
|
689
|
+
|
|
690
|
+
### When to Use Singleton vs Instance
|
|
691
|
+
|
|
692
|
+
**Use Singleton When:**
|
|
693
|
+
- Only one instance needed globally (e.g., app settings, user session)
|
|
694
|
+
- Accessed from multiple unrelated parts of the app
|
|
695
|
+
- Represents global application state
|
|
696
|
+
|
|
697
|
+
**Use Instance When:**
|
|
698
|
+
- Multiple instances needed (e.g., multiple items in a list)
|
|
699
|
+
- Each instance has its own data and lifecycle
|
|
700
|
+
- Represents domain objects with identity
|
|
701
|
+
|
|
702
|
+
### Component Organization
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
// ✅ Good: Abstract class + Factory + Private implementation
|
|
706
|
+
export abstract class MyEntity extends AppObjectEntity { }
|
|
707
|
+
export function makeMyEntity(appObject: AppObject): MyEntity { }
|
|
708
|
+
class MyEntityImp extends MyEntity { }
|
|
709
|
+
|
|
710
|
+
// ❌ Bad: Public implementation class
|
|
711
|
+
export class MyEntity extends AppObjectEntity { }
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Dependency Access
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
// ✅ Good: Use cached getters
|
|
718
|
+
private get myEntity()
|
|
719
|
+
{
|
|
720
|
+
return this.getCachedLocalComponent<MyEntity>(MyEntity.type);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ❌ Bad: Repeated lookups
|
|
724
|
+
myMethod()
|
|
725
|
+
{
|
|
726
|
+
const entity = this.appObject.getComponent<MyEntity>(MyEntity.type);
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Error Handling
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
// ✅ Good: Graceful error handling with warnings
|
|
734
|
+
if (!uc)
|
|
735
|
+
{
|
|
736
|
+
appObjects.submitWarning("controllerName", "UC not found");
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ❌ Bad: Throwing errors or failing silently
|
|
741
|
+
uc.doSomething(); // Crashes if uc is undefined
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Common Pitfalls to Avoid
|
|
745
|
+
|
|
746
|
+
1. **Violating dependency rules** - Don't make entities depend on UCs or PMs
|
|
747
|
+
2. **Forgetting to observe entities** - PMs won't update if they don't observe entities
|
|
748
|
+
3. **Not implementing vmsAreEqual()** - Can cause unnecessary UI re-renders
|
|
749
|
+
4. **Memory leaks** - Always unsubscribe views when components unmount
|
|
750
|
+
5. **Missing error handling** - Always check if components exist before using them
|
|
751
|
+
|
|
752
|
+
## Barrel File (index.ts)
|
|
753
|
+
|
|
754
|
+
The ExampleFeature includes a root barrel file that exports all controllers and adapters. This provides a single import point and clear public API:
|
|
755
|
+
|
|
756
|
+
**ExampleFeature/index.ts:**
|
|
757
|
+
```typescript
|
|
758
|
+
// Controllers
|
|
759
|
+
export * from "./Controllers/setExampleText";
|
|
760
|
+
export * from "./Controllers/toggleExampleBoolean";
|
|
761
|
+
|
|
762
|
+
// Adapters
|
|
763
|
+
export * from "./Adapters/examplePmAdapter";
|
|
764
|
+
export * from "./Adapters/exampleSingletonPmAdapter";
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
**Benefits:**
|
|
768
|
+
- Single import statement for all feature exports
|
|
769
|
+
- Clear public API surface for the entire feature
|
|
770
|
+
- Easier refactoring (internal structure can change without affecting consumers)
|
|
771
|
+
- Better code organization and encapsulation
|
|
772
|
+
|
|
773
|
+
**Usage:**
|
|
774
|
+
```typescript
|
|
775
|
+
// ✅ Good: Import from feature barrel file
|
|
776
|
+
import {
|
|
777
|
+
setExampleText,
|
|
778
|
+
toggleExampleBoolean,
|
|
779
|
+
examplePmAdapter,
|
|
780
|
+
exampleSingletonPmAdapter
|
|
781
|
+
} from "../ExampleFeature";
|
|
782
|
+
|
|
783
|
+
// ❌ Avoid: Direct file imports
|
|
784
|
+
import { setExampleText } from "../ExampleFeature/Controllers/setExampleText";
|
|
785
|
+
import { examplePmAdapter } from "../ExampleFeature/Adapters/examplePmAdapter";
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
## Creating Your Own Feature
|
|
789
|
+
|
|
790
|
+
Follow these steps to create a new feature based on this example:
|
|
791
|
+
|
|
792
|
+
1. **Create folder structure** matching ExampleFeature
|
|
793
|
+
2. **Define entities** representing your domain data
|
|
794
|
+
3. **Create repositories** if managing collections
|
|
795
|
+
4. **Implement use cases** for business operations
|
|
796
|
+
5. **Build presentation managers** to transform data for UI
|
|
797
|
+
6. **Add controllers** to simplify the API
|
|
798
|
+
7. **Create adapters** for UI framework integration
|
|
799
|
+
8. **Add root barrel file** (`index.ts`) exporting all controllers and adapters
|
|
800
|
+
9. **Write factory** to initialize everything
|
|
801
|
+
10. **Add tests** for all components
|
|
802
|
+
11. **Integrate** into your application via DomainFactoryRepo
|
|
803
|
+
|
|
804
|
+
This example provides a complete, production-ready pattern for building scalable, maintainable features using Clean Architecture principles.
|