NGRXによるドメイン駆動型設計


ドメイン駆動型設計とは


ドメイン駆動設計は、ドメインモデルと呼ばれる構造体の周りにソフトウェアを実装する方法です.これらのモデルは特定のドメインのビジネスルールとプロセスを定義します.この方法は、開発チームをビジネスやステークホルダーに直接公開することで他の方法とは異なります.

NGRXとは


NGRXは、アプリケーション内の状態を管理するためにReduxパターンの角度の実装です.NGRXには3つの主要な部分があります.
  • アクション
  • アクションは、アプリケーションの状態を更新するために派遣されるイベントです.
  • 還元剤
  • 還元剤は、アプリケーションの現在の状態へのポインタです.我々は、アプリケーションの現在の状態の正確な表現を得るために、我々の減速者(または州の店)をステッチすることができなければなりません.
  • エフェクト
  • これらは、アプリケーションの状態を変異(または副作用を引き起こす)コードを実行するアクション(またはイベント)へのリスナーです.規範的な例は、アクションがデータをロードするためにディスパッチされたとき、そのアクションに対する効果がリッスンし、HTTPコールがデータを取得し、HTTP呼び出しが正常に終了したか失敗したことを示す別のアクションを送信し、アプリケーションの状態を更新します.
  • 効果は観測可能である(それがそれを聞いていたことを派遣した)、そして、もう一つの観測可能な(状態を更新するペイロードデータによる行動)を返します.
  • イベントストーム


    ドメイン駆動設計はイベントストーミングの概念を持つ.イベントストーミングのアイディアは、ビジネスと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,
        });
      }
    }
    
    
    注意事項
  • これは、スクリーンコンポーネントを取得する項目のHTMLテンプレートを示します.コンポーネントには、ローカライズされたサービスがあります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 }>()
    );
    
    
    
    注意事項
  • 私たちは2つの状態ストア(現在のリストのための1つと取得するアイテムの1つ)を作成しました.これは、アクション、エフェクト、および還元器を分離している間、我々はまだEffectsModule は既に登録されています.
  • 私たちは1つのアクションは、各店舗では、1つの項目を追加するには、現在の項目のリストには、1つのアイテムからアイテムを削除するリストを取得します.
  • 現在のアイテム効果:
    @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)
            );
          });
      }
    
    }
    
    
    注意事項
  • utilサービスを効果に注入します.Utilサービスでは、ストアとストレージサービスの両方を入力します.ストアでは、アプリケーションの現在の状態のストアを問い合わせることができます.ストレージは、デバイスのストレージに項目を格納します.
  • その効果は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 });
          })
        )
      );
    }
    
    
    注意事項
  • つのアクションを聞くために2つの効果を作成します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)に類似していることも確認できます.