NXを用いたUIプレゼンロジックからのビジネスロジックの分離


NXを用いたUIプレゼンロジックからのビジネスロジックの分離


説明


この記事では、UIのプレゼンロジックからアプリケーションのビジネスロジックを分離する方法について説明します.我々は、NXを使用して、これは、プレゼンスコンポーネントがどこにあるアプリを作成するデザインを利用して、それを実現し、ビジネスのロジックがどこにあるlibs.

なぜですか?


私は、与えられたコンポーネントのためにコードの1000 +線を持っている多くのコードベースでいました.この問題は、コンポーネントがどのようにしているかを分離することができないということです.
しかし、ちょっと待って、なぜ我々はそれらのことを分離する必要がありますか?以下は重要な理由です.
  • これは、テストが容易になります.すべてのビジネスロジックが注入されたサービスで発生すると、サービスが戻るときに、UIが示すpresenation(何)をテストするのは簡単です.たとえば、クロスフォームの妥当性検査に基づいて[送信]ボタンを無効にするフォームの検証ロジックがある場合は、ボタンの状態をテストするために模擬することができるBoolean(またはBoolean型のObjectable/subject)を返すサービスにメソッドを使用できます.また、フォームの入力を処理するメソッドを公開することもできます.ユニットテストでは、入力の変更がサービスを呼び出して検証を実行することをテストできます.サービスの単体テストでは、そのメソッドをテストして検証が正しいかどうかを検証できます.
  • これは、より宣言/反応プログラミングを可能にします.あなたのコンポーネントは単にデータを表示し、UIの相互作用を認識しています.あなたのサービス/sは、あなたのコンポーネントとUI相互作用の処理を渡すために、データOrchrestationをしています.
  • これは、コードの再利用可能です.あなたのチームがWebアプリケーションを作成することを求められている状況を考えてください.ヵ月後、ビジネスは、ネイティブのWebビューのハイブリッドを介していずれかのいずれかの携帯電話を作成する必要があるか、単にそれをより敏感に、あなたのコンポーネントを構築した場合のみ、あなたは本当に別の方法でジャガイモをむく必要があります構築した.領収書は同じままです.つまり、コンポーネントがどのように動作するかのロジックに多くの変更を加える必要はありません.
  • アプローチ


    我々のNX Monorepoでは、我々のコンポーネントに必要とされるインターフェース、タイプ、および必要に応じて必要なサービスをエクスポートします.また、アプリケーション内の状態ストアを初期化できるように、状態ストアをエクスポートします.
    最後にこれについては、アプリはイオンのアプリだということです.これはこの記事には関係がない.

    現在のリストモジュール


    コンポーネント

    component.html
    
        <pmt-mobile-toolbar class="header" title="Current Items">
        </pmt-mobile-toolbar>
    
        <ion-content *ngIf="viewModel$ | async as viewModel">
            <ion-list *ngIf="viewModel.currentItems?.length; else noItemText">
                <ion-item-sliding *ngFor="let item of viewModel.currentItems;">
                    <ion-item-options side="start">
                        <ion-item-option color="danger">
                            <ion-icon name="trash-sharp"></ion-icon>
                        </ion-item-option>
                    </ion-item-options>
                    <ion-item-options side="end">
                        <ion-item-option (click)="currentListStateSvc.markItemAsUsed(item)">
                            <ion-icon name="checkmark-sharp"></ion-icon>
                        </ion-item-option>
                        <ion-item-option (click)="currentListStateSvc.decrementItem(item)" *ngIf="item.qty > 1"><ion-icon name="remove-sharp"></ion-icon></ion-item-option>
                    </ion-item-options>
                    <ion-item lines="full">
                        <div class="grocery-item-container">
                            <span class="item-name">{{item.name}}</span>
                            <div class="item-details">
                                <div class="details-container">
                                    <span class="label">Date Purchased:</span>
                                    <span>{{item.datePurchased}}</span>
                                </div>
                                <div class="details-container">
                                    <span class="label">Qty Left:</span>
                                    <span class="qty">{{item.qty}}</span>
                                </div>
                            </div>
                        </div>
                    </ion-item>
    
                </ion-item-sliding>
            </ion-list>
            <ng-template #noItemText>
                <main class="no-item-section">
                    <div>
                        {{viewModel.noItemsText}}
                    </div>
    
                </main>
            </ng-template>
        </ion-content>
    
    
    注意事項
  • 私たちはpmt-mobile-toolbar コンポーネント.これは、イオンのツールバーのコンポーネントのラッパーである私たちのmonorepoの別のライブラリです.
  • 変数として使用する変数viewModel$ . これは、このコンポーネントに必要なすべてのデータを含む観測可能です.使用するasync 角のアプリケーションのための最良の練習としてここでパイプ.
  • 我々はいくつかの要素のクリックハンドラーにバインドします.
  • component.ts
    import { Component, OnInit } from '@angular/core';
    import {
      CurrentListStateService,
      CurrentListViewModel,
    } from '@pmt/grocery-list-organizer-business-logic-current-grocery-items';
    import { Observable } from 'rxjs';
    
    @Component({
      selector: 'pmt-current-list',
      templateUrl: './current-list.component.html',
      styleUrls: ['./current-list.component.scss'],
      providers: [CurrentListStateService],
    })
    export class CurrentListComponent implements OnInit {
      viewModel$!: Observable<CurrentListViewModel>;
    
      constructor(public currentListStateSvc: CurrentListStateService) {}
    
      ngOnInit(): void {
        this.viewModel$ = this.currentListStateSvc.getViewModel();
      }
    }
    
    
    注意事項
  • 我々は、からアイテムをインポート@pmt/grocery-list-organizer-business-logic-current-grocery-items . これはmonorepoで作成したライブラリです.このライブラリは、この特定のコンポーネントを含むモジュールへのマップの1つです.また、インポートする項目はサービスとビューモデルの両方です.
  • 我々は、我々の州のサービスを我々のコンポーネントに直接注入します.私たちは後で、サービスでは、我々は使用しないで表示されますprovidedIn: root 使用する場合@Injectable 注釈.これは、このコンポーネントが作成され破棄されると、このサービスが作成および破棄されることを意味します.
  • これは本当にサービスからデータを受け取る非常にリーンなコンポーネントです.
  • app.module.ts
    import { NgModule } from '@angular/core';
    import { ReactiveFormsModule } from '@angular/forms';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { RouteReuseStrategy } from '@angular/router';
    
    import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
    import { IonicStorageModule } from '@ionic/storage-angular';
    import { StoreModule } from '@ngrx/store';
    import { StoreDevtoolsModule } from '@ngrx/store-devtools';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent } from './app.component';
    
    import {
      GlobalEffects,
      globalReducer,
    } from '@pmt/grocery-list-organizer-shared-business-logic';
    import { EffectsModule } from '@ngrx/effects';
    import {
      CurrentGroceryItemsEffects,
      currentGroceryItemsReducer,
    } from '@pmt/grocery-list-organizer-business-logic-current-grocery-items';
    
    @NgModule({
      declarations: [AppComponent],
      entryComponents: [],
      imports: [
        BrowserAnimationsModule,
        IonicModule.forRoot(),
        IonicStorageModule.forRoot(),
        StoreModule.forRoot({
          app: globalReducer,
          'current-list': currentGroceryItemsReducer,
        }),
        EffectsModule.forRoot([GlobalEffects, CurrentGroceryItemsEffects]),
        StoreDevtoolsModule.instrument({}),
        AppRoutingModule,
        ReactiveFormsModule,
      ],
      providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    
    
    注意事項
  • これは、アプリケーションモジュールファイルです.デフォルトの画面は現在のリストビューですので、ここでの状態のエクスポートをインポートします.currentGroceryItemsReducer and CurrentGroceryItemsEffects ). 他の怠惰なロードされたモジュールでは、特にモジュール内の状態のエクスポートをインポートできます.
  • @pmt/grocery-list-organizer-business-logic-current-items current-list-state service
    import { Injectable } from '@angular/core';
    import { Store } from '@ngrx/store';
    import { CurrentGroceryItem } from '@pmt/grocery-list-organizer-shared-business-logic';
    import { map, Observable } from 'rxjs';
    import { getCurrentItems } from '..';
    import {
      decrementItemQty,
      markItemAsUsed,
    } from '../actions/current-grocery-items.actions';
    import {
      CurrentListState,
      CurrentListViewModel,
    } from '../models/current-list.interface';
    
    @Injectable()
    export class CurrentListStateService {
      constructor(private _store: Store<CurrentListState>) {}
    
      getViewModel(): Observable<CurrentListViewModel> {
        const viewModel$ = this._store.select(getCurrentItems).pipe(
          map((currentItems) => {
            const itemsToReturn: CurrentGroceryItem[] = currentItems ?? [];
            const viewModel: CurrentListViewModel = {
              currentItems: itemsToReturn,
              noItemsText: 'You currently have no items.',
            };
            return viewModel;
          })
        );
        return viewModel$;
      }
    
      markItemAsUsed(usedItem: CurrentGroceryItem): void {
        this._store.dispatch(markItemAsUsed({ usedItem }));
      }
    
      decrementItem(itemToDecrement: CurrentGroceryItem): void {
        this._store.dispatch(decrementItemQty({ itemToDecrement }));
      }
    }
    
    
    注意事項
  • 使用しないprovidedIn: root@Injectable 私たちが以前議論したように、ここで注釈.
  • このコンポーネントに直接ストアを入力します.
  • これはストレートフォワードサービスですgetViewModel コンポーネントに渡すデータをオーケストレーションし、markItemAsUsed and decrementItem UIの相互作用を処理するだけで、ストアにアクションを送信します.
  • actions.ts
    import { createAction, props } from '@ngrx/store';
    import { CurrentGroceryItem } from '@pmt/grocery-list-organizer-shared-business-logic';
    
    export enum CurrentItemActionType {
      LOAD_CURRENT_ITEMS = '[Current] Load Current Items',
      LOAD_CURRENT_ITEMS_SUCCESS = '[Current] Load Current Items Success',
      ADD_ITEM_TO_CURRENT_LIST = '[Current] Add Item to Current List',
      MARK_ITEM_AS_USED = '[Current] Mark Item As Used',
      DECREMENT_ITEM_QTY = '[Current] Decrement Item Qty',
    }
    
    export const loadCurrentItems = createAction(
      CurrentItemActionType.LOAD_CURRENT_ITEMS
    );
    
    export const loadCurrentItemsSuccess = createAction(
      CurrentItemActionType.LOAD_CURRENT_ITEMS_SUCCESS,
      props<{ currentItems: CurrentGroceryItem[] }>()
    );
    
    export const addItemToCurrentList = createAction(
      CurrentItemActionType.ADD_ITEM_TO_CURRENT_LIST,
      props<{ itemToAdd: CurrentGroceryItem }>()
    );
    
    export const markItemAsUsed = createAction(
      CurrentItemActionType.MARK_ITEM_AS_USED,
      props<{ usedItem: CurrentGroceryItem }>()
    );
    
    export const decrementItemQty = createAction(
      CurrentItemActionType.DECREMENT_ITEM_QTY,
      props<{ itemToDecrement: CurrentGroceryItem }>()
    );
    
    
    reducer.ts
    import { createReducer, on } from '@ngrx/store';
    import {
      addItemToCurrentList,
      decrementItemQty,
      loadCurrentItemsSuccess,
      markItemAsUsed,
    } from '../actions/current-grocery-items.actions';
    import { CurrentListState } from '../models/current-list.interface';
    
    const initialState: CurrentListState = {
      currentItems: undefined,
    };
    
    export const currentGroceryItemsReducer = createReducer(
      initialState,
      on(loadCurrentItemsSuccess, (state, { currentItems }) => ({
        ...state,
        currentItems,
      })),
      on(addItemToCurrentList, (state, { itemToAdd }) => {
        const updatedItems = [...(state.currentItems ?? []), itemToAdd];
        return { ...state, currentItems: updatedItems };
      }),
      on(markItemAsUsed, (state, { usedItem }) => {
        const currentItems = state.currentItems?.filter(
          (item) => item.id !== usedItem.id
        );
        return { ...state, currentItems };
      }),
      on(decrementItemQty, (state, { itemToDecrement }) => {
        const updatedItems = state.currentItems?.map((item) => {
          if (item.id === itemToDecrement.id) {
            const updatedItem = { ...item, qty: itemToDecrement.qty - 1 };
            return updatedItem;
          }
          return item;
        });
    
        return { ...state, currentItems: updatedItems };
      })
    );
    
    
    effects.ts
    import { Injectable } from '@angular/core';
    import { Actions, createEffect, ofType } from '@ngrx/effects';
    import { initializeApp } from '@pmt/grocery-list-organizer-shared-business-logic';
    import { tap } from 'rxjs';
    import {
      addItemToCurrentList,
      decrementItemQty,
      markItemAsUsed,
    } from '../actions/current-grocery-items.actions';
    import { CurrentGroceryItemsUtilService } from '../services/current-grocery-items-util.service';
    @Injectable()
    export class CurrentGroceryItemsEffects {
      constructor(
        private _actions$: Actions,
        private _currentItemsUtilSvc: CurrentGroceryItemsUtilService
      ) {}
    
      initAppLoadItems$ = createEffect(
        () =>
          this._actions$.pipe(
            ofType(initializeApp),
            tap(() => this._currentItemsUtilSvc.loadItemsFromStorage())
          ),
        { dispatch: false }
      );
    
      addItemToCurrentListUpdateStorage$ = createEffect(
        () =>
          this._actions$.pipe(
            ofType(addItemToCurrentList),
            tap((action) => {
              this._currentItemsUtilSvc.addItemToCurrentListOnStorage(
                action.itemToAdd
              );
            })
          ),
        { dispatch: false }
      );
    
      markItemAsUsed$ = createEffect(
        () =>
          this._actions$.pipe(
            ofType(markItemAsUsed),
            tap((action) => {
              this._currentItemsUtilSvc.updateStorageAfterItemMarkedAsUsed(
                action.usedItem
              );
            })
          ),
        { dispatch: false }
      );
    
      decrementItemUpdateStorage$ = createEffect(
        () =>
          this._actions$.pipe(
            ofType(decrementItemQty),
            tap((action) => {
              this._currentItemsUtilSvc.updateStoargeAfterDecrementItem(
                action.itemToDecrement
              );
            })
          ),
        { dispatch: false }
      );
    }
    
    
    注意事項
  • このアクションと減速ファイルはまっすぐ進むと指摘する何も注目している.
  • エフェクトファイルでは、ライブラリの一部としてエクスポートされないUtilサービスを入力します.このライブラリ内からそのサービスへのアクセスを許可します.
  • 我々は、別の記事になる私たちの効果で聞くイベントを介してUIの状態を管理しています.
  • index.ts
    export * from './lib/actions/current-grocery-items.actions';
    export * from './lib/reducer/current-grocery-items.reducer';
    export * from './lib/effects/current-grocery-items.effects';
    export * from './lib/index';
    
    export { CurrentListStateService } from './lib/services/current-list-state.service';
    export * from './lib/models/current-list.interface';
    
    
    注意事項
  • これが図書館の契約です.我々は私たちのモデル、州のサービスをエクスポートし、ストアarifacts、我々はutilサービスをエクスポートしないことがわかります.そのサービスはこの図書館の内部です.
  • 結論


    私は、私たちのアプリケーションのビジネスロジックからUIの部分を分離するためにNXを使用する私のアプローチにこの記事を楽しんだことを願っています.うまくいけば、すべてのそれを試してみて、私はそれがあなたのためにどのように動作するかをお知らせください.あなたはTwitter経由で私に到達することができます@paulmojicatech . ハッピーコーディング!