NGRXによるドメイン駆動型設計
40751 ワード
ドメイン駆動型設計とは
ドメイン駆動設計は、ドメインモデルと呼ばれる構造体の周りにソフトウェアを実装する方法です.これらのモデルは特定のドメインのビジネスルールとプロセスを定義します.この方法は、開発チームをビジネスやステークホルダーに直接公開することで他の方法とは異なります.
NGRXとは
NGRXは、アプリケーション内の状態を管理するためにReduxパターンの角度の実装です.NGRXには3つの主要な部分があります.
イベントストーム
ドメイン駆動設計はイベントストーミングの概念を持つ.イベントストーミングのアイディアは、ビジネスとdevチームを一緒に持ってきて、システムで起こっているドメインイベントの用語でビジネスルールを記述するアーティファクトを作成します.イベントは、ボード(物理的またはデジタルのいずれか)の線形、時間ベースのシーケンスに配置されます.これはイベントストーミング会議の終了時に配信されるアーティファクトです.
NGRXはどのようにフィットしますか?
NGRXはRXJSの上で(名前でさえ)重く頼ります.RXJSは、反応プログラミングパターンのJavaScript実装です.このパターンは、イベントストリームがシステムを通過し、これらのイベントコードに基づいて実行されるコーディングソフトウェアへの宣言的アプローチを提供します.つは、うまくいけば、どのようにドメインイベントと反応性プログラミングパターンが互いに補完することができるかを見ることができます.以下の要件を考えてください.
要件
我々は、我々は我々のチームのWebスキルを活用することができますので、イオンを使用してネイティブアプリケーションを構築している.我々のアプリケーションは、両方のユーザーがアイテムの食料品のリストを取得し、また、購入されている家にいる食料品のリストの項目を追跡することができます.
インターネットがアプリケーションを使用するために必要なので、リストは、デバイスのストレージに格納されるようにしたい.
以下は、イベントストーミングの間に作成されたアーティファクトの一部であり、ユーザーがアイテムをアイテムから移動してリストを取得し、リスト内の現在のアイテムに移動させたい場合を説明します.
イベントストーミングから実装へ
ドメイン駆動型設計はCQRSと呼ばれるプログラミングパラダイムを使用する
Command Query Responsibility Separation
. これは、システムに既に存在している読み込み(または問い合わせ)から、システムの更新(または追加または削除)の責任を分離するパターンです.私にとって、これは我々の影響が我々のものであるNGRXにかなり明確なマッピングをします
update models
そして、我々の減速者/セレクタは、我々のものですread models
. 送信されるアクションは、任意の時点で発生するドメインイベントであり、ユーザーの相互作用を介して送信されます.アクションを送信します.
<pmt-mobile-toolbar title="Things to Get" [actionItems]="['create-outline']"
(actionItemEvent)="itemsToGetStateSvc.setIsModalOpen(true)">
</pmt-mobile-toolbar>
<ion-content *ngIf="viewModel$ | async as viewModel">
<pmt-input placeholder="Search" [isMobile]="true" (changeEvent)="itemsToGetStateSvc.handleSearchValueUpdated($event)"></pmt-input>
<ion-list *ngIf="viewModel.itemsNeeded?.length; else noItemText">
<ion-item *ngFor="let item of viewModel.itemsNeeded | pmtSearchValue : 'name' : viewModel.searchValue!" lines="full">
<div class="grocery-item-container">
<ion-checkbox (ionChange)="itemsToGetStateSvc.removeItemFromItemsToGet(item)" class="checkbox"></ion-checkbox>
<span class="item-name">{{item.name}}</span>
<span class="qty">Qty: {{item.qty}}</span>
</div>
</ion-item>
</ion-list>
<ion-modal #ionModal [isOpen]="viewModel.isModalOpen">
<ng-template>
<pmt-mobile-toolbar title="Add item" [actionItems]="['close-outline']" (actionItemEvent)="itemsToGetStateSvc.setIsModalOpen(false)"></pmt-mobile-toolbar>
<div class="form-container">
<form novalidate [formGroup]="itemsToGetForm">
<div class="autocomplete-container">
<pmt-autocomplete (valueChangedEv)="handleAutocompleteChangeEv($event)" [allItems]="viewModel.allAvailableItems" label="Enter an item"></pmt-autocomplete>
</div>
<pmt-input formControlName="qty" label="Qty"></pmt-input>
<div class="action-container">
<button [disabled]="itemsToGetForm.invalid" mat-raised-button color="primary" (click)="addItem()">Add Item</button>
</div>
</form>
</div>
</ng-template>
</ion-modal>
<ng-template #noItemText>
<main class="no-item-section">
<div>
{{viewModel.noItemsText}}
</div>
</main>
</ng-template>
</ion-content>
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import {
CurrentGroceryItem,
GroceryItem,
} from '@pmt/grocery-list-organizer-shared-business-logic';
import {
BehaviorSubject,
ignoreElements,
map,
merge,
Observable,
tap,
} from 'rxjs';
import {
addItemToGet,
loadItemsToGet,
removeItemToGet,
setIsItemsToGetModalOpen,
} from './actions/items-to-get.actions';
import {
getAllAvailableItems,
getIsAddItemsToGetModelOpen,
getItemsToGet,
} from './index';
import { ItemsToGetState } from './models/items-to-get-state.interface';
import { ItemsToGetViewModel } from './models/items-to-get.interface';
@Injectable()
export class ItemsToGetStateService {
readonly INITIAL_STATE: ItemsToGetViewModel = {
itemsNeeded: [],
noItemsText: 'You currently do not have any items on your grocery list.',
isModalOpen: false,
allAvailableItems: [],
searchValue: undefined,
};
private _viewModelSub$ = new BehaviorSubject<ItemsToGetViewModel>(
this.INITIAL_STATE
);
viewModel$ = this._viewModelSub$.asObservable();
constructor(private _store: Store<ItemsToGetState>) {}
getViewModel(): Observable<ItemsToGetViewModel> {
this._store.dispatch(loadItemsToGet());
const items$ = this._store.select(getItemsToGet).pipe(
tap((items) => {
this._viewModelSub$.next({
...this._viewModelSub$.getValue(),
itemsNeeded: items,
});
}),
ignoreElements()
);
const isModalOpen$ = this._store.select(getIsAddItemsToGetModelOpen).pipe(
tap((isOpen) => {
this._viewModelSub$.next({
...this._viewModelSub$.getValue(),
isModalOpen: isOpen,
});
}),
ignoreElements()
);
const allAvailableItems$ = this._store.select(getAllAvailableItems).pipe(
map((allAvailableItems) => {
return allAvailableItems.map((item) => item.name);
}),
tap((allAvailableItems) => {
this._viewModelSub$.next({
...this._viewModelSub$.getValue(),
allAvailableItems,
});
}),
ignoreElements()
);
return merge(this.viewModel$, items$, isModalOpen$, allAvailableItems$);
}
setIsModalOpen(isOpen: boolean): void {
this._store.dispatch(setIsItemsToGetModalOpen({ isOpen }));
}
addItem(itemToAdd: GroceryItem): void {
this._store.dispatch(addItemToGet({ item: itemToAdd }));
this._store.dispatch(setIsItemsToGetModalOpen({ isOpen: false }));
}
removeItemFromItemsToGet(itemToRemove: CurrentGroceryItem): void {
this._store.dispatch(removeItemToGet({ itemToRemove }));
}
handleSearchValueUpdated(searchValue: string): void {
this._viewModelSub$.next({
...this._viewModelSub$.getValue(),
searchValue,
});
}
}
注意事項ItemsToGetStateService
) これはコンポーネントのビジネスロジックを扱います.テンプレートはremoveItemFromItemsToGet
チェックボックス(ion-checkbox
) をチェックする.そのメソッドの実装は単にremoveItemToGet
アクション.
export enum CurrentItemActionType {
ADD_ITEM_TO_CURRENT_LIST = '[Current] Add Item to Current List'
}
export const addItemToCurrentList = createAction(
CurrentItemActionType.ADD_ITEM_TO_CURRENT_LIST,
props<{ itemToAdd: CurrentGroceryItem }>()
);
export enum ItemsToGetActionType {
REMOVE_ITEM_TO_GET = '[Items to Get] Remove Item to Get',
}
export const removeItemToGet = createAction(
ItemsToGetActionType.REMOVE_ITEM_TO_GET,
props<{ itemToRemove: GroceryItem }>()
);
注意事項EffectsModule
は既に登録されています.@Injectable()
export class CurrentGroceryItemsEffects {
constructor(
private _actions$: Actions,
private _currentItemsUtilSvc: CurrentGroceryItemsUtilService
) {}
addItemToCurrentListUpdateStorage$ = createEffect(
() =>
this._actions$.pipe(
ofType(addItemToCurrentList),
tap((action) => {
this._currentItemsUtilSvc.addItemToCurrentListOnStorage(
action.itemToAdd
);
})
),
{ dispatch: false }
);
}
現在のアイテムUtilサービス
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import {
CurrentGroceryItem,
IonicStorageService,
IonicStorageType,
} from '@pmt/grocery-list-organizer-shared-business-logic';
import { filter, forkJoin, from, map, take } from 'rxjs';
import { loadCurrentItemsSuccess } from '../actions/current-grocery-items.actions';
import { CurrentListState } from '../models/current-list.interface';
@Injectable({
providedIn: 'root',
})
export class CurrentGroceryItemsUtilService {
constructor(
private _store: Store<CurrentListState>,
private _storageSvc: IonicStorageService
) {}
addItemToCurrentListOnStorage(itemToAdd: CurrentGroceryItem): void {
this._storageSvc
.getItem(IonicStorageType.CURRENT_ITEMS)
.pipe(take(1))
.subscribe((itemsStr) => {
const currentItems = itemsStr
? [...JSON.parse(itemsStr), itemToAdd]
: [itemToAdd];
this._storageSvc.setItem(
IonicStorageType.CURRENT_ITEMS,
JSON.stringify(currentItems)
);
});
}
}
注意事項addItemToCurrentList
次に、Utilサービスを呼び出してコードを実行します.また、その効果を指定します{dispatch: false}
. 効果は観測可能であり、指定できなかった場合は観測可能である{dispatch: false}
, 我々は無限ループで自分自身を見つけるだろう.
@Injectable()
export class ItemsToGetEffects {
constructor(
private _actions$: Actions,
private _storageSvc: IonicStorageService,
private _itemsToGetUtilSvc: ItemsToGetUtilService
) {}
removeItemFromItemsToGetUpdateStorage$ = createEffect(
() =>
this._actions$.pipe(
ofType(removeItemToGet),
switchMap((action) =>
this._storageSvc.getItem(IonicStorageType.ITEMS_TO_GET).pipe(
tap((itemsStr) => {
const itemsToGet = (
JSON.parse(itemsStr) as CurrentGroceryItem[]
).filter((item) => item.name !== action.itemToRemove.name);
this._storageSvc.setItem(
IonicStorageType.ITEMS_TO_GET,
JSON.stringify(itemsToGet)
);
})
)
)
),
{ dispatch: false }
);
removeItemFromItemsToGetAddItemToCurrentList$ = createEffect(() =>
this._actions$.pipe(
ofType(removeItemToGet),
map((action) => {
const itemToAdd: CurrentGroceryItem = {
...action.itemToRemove,
id: `${new Date()}_${action.itemToRemove.name}`,
datePurchased: new Date().toDateString(),
};
return addItemToCurrentList({ itemToAdd });
})
)
);
}
注意事項removeItemToGet
). このアクションが送られるとき、我々は我々が使うところで1つの影響を持ちます{dispatch: false}
デバイスのストレージを更新します.addItemToCurrentList
我々が我々が上で議論した我々の影響で、我々が聞く行動.
const initialState: CurrentListState = {
currentItems: undefined,
};
export const currentGroceryItemsReducer = createReducer(
initialState,
on(addItemToCurrentList, (state, { itemToAdd }) => {
const updatedItems = [...(state.currentItems ?? []), itemToAdd];
return { ...state, currentItems: updatedItems };
})
);
const initialState: ItemsToGetState = {
itemsToGet: [],
isLoaded: false,
isAddItemModalVisible: false,
allAvailableItems: [],
};
export const itemsToGetReducer = createReducer(
initialState,
on(removeItemToGet, (state, { itemToRemove }) => {
const itemsToGet = state.itemsToGet.filter(
(item) => item.name !== itemToRemove.name
);
return { ...state, itemsToGet };
})
);
注意事項我々は2つのリダーズを2つの店(またはCQRSの人々のためのモデルを読む)を更新するときに2つのアクションが派遣されている.
結論
この記事では、NGRXへの実装がドメイン駆動設計の実装とどのように似ているかについてどう考えるかを示しました.NGRXと領域駆動型設計は、システム/アプリケーションの状態を引き出すためにシステムで起こっているイベントに大いに依存します.また、NGRXがドメイン駆動設計のテナントであるCQRS(Command Query County Detection)に類似していることも確認できます.
Reference
この問題について(NGRXによるドメイン駆動型設計), 我々は、より多くの情報をここで見つけました https://dev.to/paulmojicatech/domain-driven-design-with-ngrx-104cテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol