36
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angularアーキテクチャパターンと(スケールさせるのに役立つ)ベストプラクティス

Posted at

この記事はAngular Architecture Patterns and Best Practices (that helps scale)を和訳したものです。


スケーラブルなソフトウェアを構築することは困難な課題です。私たちがフロントエンドアプリケーションにおけるスケーラビリティについて考えるとき、複雑性の増加とますます多くのビジネスルール、アプリケーションに読み込まれる増え続けるデータ、しばしば世界中に分散される大規模のチームを考えることができます。高品質の配信を維持し技術的負債を防ぐための前述の要因を解決するために、堅牢で基礎のしっかりしたアーキテクチャは必要不可欠です。Angular自体は、開発者に適した方法を強制する、かなり強固な思想を持ったフレームワークですが、間違った方向に進みうることが多々あります。この記事では、ベストプラクティスと実践で実証済みのパターンに基づく、適切に設計されたAngularアプリケーションのアーキテクチャの推薦事項を示します。この記事における究極の目的は、長期間で持続可能な開発速度新機能追加の容易さを維持するためのAngularアプリケーションの設計の仕方を学ぶことです。これらの目的を達成するために私たちは以下を適用します。

  • アプリケーションのレイヤー間での適切な抽象化
  • 単方向データフロー
  • リアクティブな状態管理
  • モジュール設計
  • 賢いコンポーネントパターンと馬鹿なコンポーネントパターン

bullets

目次

フロントエンドにおけるスケーラビリティの問題

私たちがフロントエンドアプリケーションの開発で直面しうるスケーラビリティに関する問題について考えてみましょう。今日、フロントエンドアプリケーションはデータを「ただ単に表示する」だけでなくユーザーの入力の受け付けます。シングル・ページ・アプリケーション(SPA)はユーザーに豊富なインタラクションを与え、主にバックエンドをデータ永続化層として利用します。これはより多くの責務がソフトウェアシステムのフロントエンドの部分に移ったことを意味します。これは、私たちが対処する必要のある、フロントエンドロジックの複雑さの増大を引き起こします。必要条件の数が時間とともに増えるだけでなく、私たちがアプリケーションに読み込ませるデータ量も増えていきます。それに加えて、容易に傷つく可能性のあるアプリケーションパフォーマンスを私たちは維持する必要があります。最後に、私たちの開発チームは大きくなり (あるいは人の入流出で少なくとも循環し)、新しく入ってきた人にとっては、できるだけ早くスピードを上げることが重要になります。

meme

前述の問題を解決する方法の1つは堅実なシステムアーキテクチャです。しかしこれは初日からそのアーキテクチャに投資するコストがともないます。システムがまだ小さい規模のときに新機能を非常に素早く届けるためには、私たち開発者にとってとても魅力的なものです。この段階では、すべてが簡単で理解可能なため、開発は本当に早く進みます。しかし、私たちがアーキテクチャについて注意を払わない限り、数回の開発者の入れ替わりや扱いづらい機能、リファクタリング、いくつかの新しいモジュールの後、開発の速度は完全に遅くなります。下図は私の開発キャリアにおいて、通常どのように見えているかを示しています。これは科学的な研究ではなく、単に私がどのように見えているかというものです。

chart
縦軸: 速度、横軸: 時間、赤線: アーキテクチャに投資していない、青線: アーキテクチャに投資している

ソフトウェアアーキテクチャ

アーキテクチャのベストプラクティスとパターンを議論するために、まず私たちはソフトウェアアーキテクチャとは何かという質問について答える必要があります。Martin Fowlerはアーキテクチャを「システムの各部分への最高レベルの分解」と定義しています。それに加えて、ソフトウェアアーキテクチャは、ソフトウェアがどのようにその部分を構成しているか、それらの部分間のコミュニケーションのルール制約は何かを説明していると私は思います。通常、私たちのシステム開発において作るアーキテクチャの決定は、時間とともにシステムが大きくなるにつれて変更することが困難です。それが、特に私たちの構築するソフトウェアが長年プロダクションで運用される場合に、プロジェクトの非常に初期からアーキテクチャの決定に注意を払うことがとても重要な理由です。あるときRobert C. Martinは、ソフトウェアの真のコストはメンテナンスだと言いました。基礎がしっかりしたアーキテクチャを持つことはシステムのメンテナンスのコストを減らすことに役立ちます。

ソフトウェアアーキテクチャは、ソフトウェアがその部分と、それらの部間のコミュニケーションのルール制約で構成される方法です。

高度な抽象化レイヤー

私たちのシステムを分解する最初の方法は、抽象化レイヤー*※1を使用します。下の図は分解の一般的な概念を表現しています。その考えは適切な責務を、コアレイヤーや抽象レイヤー※1*、プレゼンテーションレイヤーといった、システムの適切なレイヤーに位置づけることです。私たちはそれぞれのレイヤーを独立して見て、責務を分析します。この分割もまたコミュニケーションルールを決定づけます。例えば、プレゼンテーションレイヤーは抽象レイヤーを通してのみでしかコアレイヤーと対話しません。あとで私たちはこの種の制約の恩恵が何なのかを学びます。

layers

訳者注 ※1: core layer, abstruction layer, presentation layerからなる全体としてのabstruction layersを抽象化レイヤー、その一部のAbstruction layerを抽象レイヤーと訳しています

プレゼンテーションレイヤー

プレゼンテーションレイヤーから私たちのシステムの分解の分析を始めましょう。これがすべての私たちのAngularコンポーネントが存在する場所です。このレイヤーの唯一の責務は表現することと委譲することです。言い換えると、プレゼンテーションレイヤーはUIを表現し、抽象レイヤーを通してユーザーのアクションをコアレイヤーへ委譲します。プレゼンテーションレイヤーは何を表示するかと何をするかは知っていますが、ユーザーのインタラクションをどのように扱うかは知りません。

以下のコードスニペットは、SettingsFacadeインスタンスを用いて抽象レイヤーから(addCategory()updateCategory()を介して) ユーザーのインタラクションを委譲し、(isUpdating$を介して) テンプレートでいくつかの状態を表示するCateroriesComponentを含みます。

categories.component.ts
@Component({
  selector: 'categories',
  templateUrl: './categories.component.html',
  styleUrls: ['categories.component.scss']
})
export class CategoriesComponent implements OnInit {
 
  @Input() cashflowCategories: CashflowCategoriy[];
  newCategory: CashflowCategory = new CashflowCategory();
  isUpdating$: Observable<boolean>;
  
  constructor(private settingsFacade: SettingsFacade) {
    this.isUpdating$ = settingsFacade.isUpdating$();
  }
   
  ngOnInit() {
    this.settingsFacade.loadCashflowCategories();
  }
   
  addCategory(category: CashflowCategory) {
    this.settingsFacade.addCashflowCategory(category);
  }
   
  updateCategory(category: CashflowCategory) {
    this.settingsFacade.updateCashflowCategory(category);
  }
   
}

抽象レイヤー

抽象レイヤーはコアレイヤーからプレゼンテーションレイヤーを切り離し、非常に独自の定義された責務を持っています。このレイヤーは、状態のストリームと、ファサードの役割を担う、プレゼンテーションレイヤーでのコンポーネント間のインターフェースを見えるようにします。この種のファサードは、システムでコンポーネントが表示及び実行できるものをサンドボックス化します。私たちは単にAngularのクラスプロバイダーを用いてファサードを実装することができます。このクラスは、例えばSettingsFacadeのようにFacade接尾辞で名前をつけることができます。以下はそのようなファサードの例です。

settings.facade.ts
@Injectable()
export class SettingsFacade {

  constructor(
    private cashflowCategoryApi: CashflowCategoryApi,
    private settingsState: SettingsState
  ) { }
  
  isUpdating$(): Observable<boolean> {
    return this.settingsState.isUpdating$();
  }
  
  getCashflowCategories$(): Observable<CashflowCategory[]> {
    // ここでは射影なしに単に状態を渡します
    // 2つ以上のストリームを組み合わてコンポーネントに公開する必要がある場合があります
    return this.settingsState.getCashflowCategories$();
  }
  
  loadCashflowCategories() {
    return this.cashflowCategoryApi.getCashflowCategories()
      .pipe(tap(categories => this.settingsState.setCashflowCategories(categories)));
  }
   
  // 楽観的な更新
  // 1. UIの状態を更新する
  // 2. APIを呼び出す
  addCashflowCategory(category: CashflowCategory) {
    this.settingsState.addCashflowCategory(category);
    this.cashflowCategoryApi.createCashflowCategory(category)
      .subscribe(
        (addedCategoryWithId: CashflowCategory) => {
          // 成功コールバック - サーバーから生成されたidがあります。状態を更新しましょう
          this.settingsState.updateCashflowCategoryId(category, addedCategoryWithId);
        },
        (error: any) => {
          // エラーコールバック - 状態の変化をロールバックする必要があります
          this.settingsState.removeCashflowCategory(category);
           console.log(error);
        }
      );
  }
   
  // 悲観的な更新
  // 1. APIを呼び出す
  // 2. UIの状態を更新する
  updateCashflowCategory(category: CashflowCategory) {
    this.settingsState.setUpdating(true);
    this.cashflowCategoryApi.updateCashflowCategory(category)
      .subscribe(
        () => this.settingsState.updateCashflowCategory(category),
        (error) => console.log(error),
        () => this.settingsState.setUpdating(false);
      );
  }
   
}

抽象インターフェース

コンポーネントのために状態のストリームとインターフェースを公開するという、このレイヤーの主な責務を私たちはすでに知っています。インターフェースから始めましょう。publicメソッドであるloadCashflowCategories()addCashCategory()updateCashflowCategory()は状態管理の詳細と外部API呼び出しをコンポーネントから取り除きます。(CashflowCategoryApiのような) APIプロバイダーはコアレイヤーに存在するため、コンポーネントでは直接使用しません。同様に、どのように状態が変化するかもコンポーネントの関心事ではありません。プレゼンテーションレイヤーは、物事がどのように行われるかに関心を持つべきではなく、コンポーネントは必要に応じて (委譲するときに) 抽象レイヤーからメソッドをただ単に呼び出すべきです。抽象レイヤーのpublicメソッドに注目すると、システムのこの構成部品の高度なユースケースに関する簡単な理解が得られるはずです。

しかし、抽象レイヤーはビジネスロジックを実装するべき場所ではないことを思い返してください。ここでは、接続方法を抽象化して、私たちはプレゼンテーションレイヤーをビジネスロジックにつなぎたいだけです。

状態

状態になると、抽象レイヤーによりコンポーネントは状態管理の解決策から独立されます。コンポーネントは (asyncパイプを用いて) テンプレート上に表示するデータをもつObservableが与えられ、データがどこからどのように来たかに関心を持ちません。状態を管理するために、私たちは (NgRxのような) RxJSをサポートする何かしらの状態管理ライブラリを選んだり、あるいは状態をモデル化するためにBehavirorSubjectを簡易的に用いることができます。

このような抽象化をもつことで、多くの柔軟性が得られ、プレゼンテーションレイヤーに触れなくても状態を管理する方法を変更できます。Firebaseなどのリアルタイムバックエンドにシームレスに移行して、アプリケーションをリアルタイムにすることも可能です。個人的には状態を管理するにはBehaviorSubjectから始めたいです。後で、システムの開発のある時点で、この種のアーキテクチャで何か他のものを使用する必要がある場合、リファクタリングは非常に簡単です。

同期化戦略

それでは、抽象レイヤーの他の実装箇所を詳しく見ていきましょう。私たちが選択した状態管理の解決策に関わらず、楽観的あるいは悲観的に更新するUIを実装することができます。いくつかのエンティティのコレクションに新しいレコードを作成したいとします。このコレクションはバックエンドから取得され、DOMに描画されました。悲観的な方法では、まずバックエンド側の (例えばHTTPリクエストを伴って) 状態を更新することを試みて、成功した場合にフロントエンド・アプリケーションの状態を更新します。その一方で、楽観的な楽観的な方法では、異なる順番で更新を行っています。まず、バックエンドの更新が成功したと仮定して直ちにフロントエンドの状態を更新します。その後サーバーの状態を更新するためにリクエストを送ります。成功の場合何もする必要はありませんが、失敗の場合にはフロントエンド・アプリケーションの変更をロールバックして、この状況についてユーザーに知らせる必要があります。

楽観的な更新はUIの状態を始めに変更しバックエンドの更新を試みます。ネットワークのレイテンシーのため、これはユーザーが遅延を感じないように良い体験をユーザーに与えます。もしバックエンドが失敗したらUIの変更は巻き戻さなければなりません。
悲観的な更新は始めにバックエンドの状態を変更し成功した場合にのみUIの状態を更新します。通常、ネットワークのレイテンシーのため、バックエンドのリクエストを実行している間は何かしらのスピナーやローディングバーを表示する必要があります。

キャッシング

時には、私たちがバックエンドから取得するデータがアプリケーションの状態の一部にはならないと判断することもあるでしょう。これは私たちが全く操作したくなく、(抽象レイヤーを介して) 単にコンポーネントへ渡したい読み取り専用のデータに役立ちます。この場合、ファサードでデータのキャッシングを適用することができます。これを達成する最も簡単な方法は、それぞれの新しいsubscriber間のストリームで最後の値を再送するshareReplay()RxJSオペレータを用いることです。RecordsApiを用いて、コンポーネントのためにデータを取得し、キャッシュし、フィルターするRecordsFacadeの以下のコードスニペットを見てみましょう。

records.facade.ts
@Injectable()
export class RecordsFacade {
   
  private records$: Observable<Record[]>;
   
  constructor(private recordApi: RecordApi) {
    this.records$ = this.recordApi
      .getRecords()
      .pipe(shareReplay(1)); // データをキャッシュする
  }
   
  getRecords() {
    return this.records$;
  }
   
  // コンポーネントのためにキャッシュしたデータを投影する
  getRecordsFromPeriod(period?: Period): Observable<Record[]> {
    return this.records$
      .pipe(map(records => records.filter(record => record.inPeriod(period))));
  }
   
  searchRecords(search: string): Observable<Record[]> {
    return this.recordApi.searchRecords(search);
  }
   
} 

まとめると、私たちが抽象レイヤーでできることは

  • コンポーネントのために以下のメソッドを公開する
    • コアレイヤーへロジックの実行を委譲する
    • データの同期化戦略について判断する (楽観的 vs 悲観的)
  • コンポーネントのために状態のストリームを公開する
    • 1つ以上のUIの状態のストリームを選ぶ (そして必要なら組み合わせる)
    • 外部APIからデータをキャッシュする

ご覧の通り、抽象レイヤーは階層化アーキテクチャで重要な役割を果たします。システムについてより良い理解と推論に役立つ責任を明確に定義しています。特殊な例に依存する場合、Angularモジュールごともしくはエンティティごとに1つファサードを作成することができます。例えば、モジュールがが肥大化しすぎていない場合には、SettingsModuleはただ一つのSettingsFacadeをもつことがあります。しかし時には、UserエンティティのためのUserFacadeのように、それぞれのエンティティで個別により粒度の細かい抽象的なファサードを作成するとよりよい場合があります。

コアレイヤー

最後のレイヤーはコアレイヤーです。ここに中核となるアプリケーションのロジックが実装されています。すべてのデータの操作外部とのコミュニケーションはここで行われます。状態管理のために、NgRxのような解決策を用いていた場合、ここには状態の定義やaction、reducerを配置します。今回の例ではBehaviorSubjectで状態のモデリングをしているため、私たちは便利な状態クラスでカプセル化することができます。以下はコアレイヤーからSettingsStateの例です。

settings.state.ts

@Injectable()
export class SettingsState {

  private updating$ = new BehaviorSubject<boolean>(false);
  private cashflowCategories$ = new BehaviorSubject<CashflowCategory[]>(null);
  
  isUpdating$() {
    return this.updating$.asObservable();
  }
  
  setUpdating(isUpdating: boolean) {
    this.updating$.next(isUpdating);
  }
  
  getCashflowCategories$() {
    return this.cashflowCategories$.asObservable();
  }
  
  setCashflowCategories(categories: CashflowCategory[]) {
    this.cashflowCategories$.next(categories);
  }
  
  addCashflowCategory(category: CashflowCategory) {
    const currentValue = this.cashflowCategories$.getValue();
    this.cashflowCategories$.next([...currentValue, category]);
  }
  
  updateCashflowCategory(updatedCategory: CashflowCategory) {
    const categories = this.cashflowCategories$.getValue();
    const indexOfUpdated = categories.findIndex(category => category.id === updatedCategory.id);
    categories[indexOfUpdated] = updatedCategory;
    this.cashflowCategories$.next([...categories]);
  }
  
  updateCashflowCategoryId(categoryToReplace: CashflowCategory, addedCategoryWithId: CashflowCategory) {
    const categories = this.cashflowCategories$.getValue();
    const updatedCategoryIndex = categories.findIndex(category => category === categoryToReplace);
    categories[updatedCategoryIndex] = addedCategoryWithId;
    this.cashflowCategories$.next([...categories]);
  }
  
  removeCashflowCategory(categoryRemove: CashflowCategory) {
    const currentValue = this.cashflowCategories$.getValue();
    this.cashflowCategories.next(currentValue.filter(category => category !== categoryRemove));
  }
  
}

コアレイヤーでは、クラスプロバイダーの形でHTTPクエリーを実装します。この種のクラスはApiもしくはServiceの名前の接尾辞を持ちます。APIサービスは、APIのエンドポイントと通信するだけでほかは何もしない、ただ1つの責務を持ちます。キャッシングやロジック、あるいはデータ操作はここでは避けるべきです。以下は簡単なAPIサービスの例です。

category.api.ts
@Injectable()
export class CashflowCategoryApi {

  readonly API = '/api/cashflowCategories';
  
  constructor(private http: HttpClient) {}
  
  getCashflowCategories(): Observable<CashflowCategory[]> {
    return this.http.get<CashflowCategory[]>(this.API);
  }
  
  createCashflowCategory(category: CashflowCategory): Observable<any> {
    return this.http.post(this.API, category);
  }
  
  updateCashflowCategory(category: CashflowCategory): Observable<any> {
    return this.http.put(`${this.API}/${category.id}`, category);
  }
  
}

このレイヤーでは、UI状態の多くのスライスの操作を必要とするバリデータ、マッパー、あるいはより高度なユースケースを配置することもできます。

私たちはフロントエンドアプリケーションで抽象化レイヤーのトピックを取り上げました。各レイヤーは明確に定義された境界と責務を持ちます。私たちはレイヤー間の厳格なコミュニケーションルールも定義しました。これはすべて、システムがますます複雑になるにつれて、時間とともにシステムに関するより良い理解と推論をするのに役立ちます。

単方向データフローとリアクティブな状態管理

私たちがシステムに導入したい次の原理はデータフローと変更の伝播についてです。Angular自体は (インプットバインディングを介して) プレゼンテーションレベルで単方向データフローを使いますが、私たちはアプリケーションレベルに同様の制約を課します。(ストリームに基づく) リアクティブな状態管理とともに、単方向データフローによりシステムのとても重要なプロパティであるデータの一貫性が与えられます。下の図は単方向データフローの一般的な考え方を表しています。

flow abstract

アプリケーションにおけるモデルの値が変わるたびに、Angularの変更検知システムは値変更の伝播を処理します。変更検知はインプットプロパティバインディングを介してコンポーネントツリー全体の上から下へ伝播を処理します。子コンポーネントは親コンポーネントのみに依存し、その逆には依存しないことを表します。これが単方向データフローと呼ぶ理由です。これにより、Angular はコンポーネント ツリーを (ツリー構造にサイクルがないので) 1 回だけ走査し、安定した状態を実現できるようにするため、バインディング内のすべての値が伝播されます。

前の章からわかるとおり、プレゼンテーションレイヤーの上にはコアレイヤーがあり、そこにはアプリケーションロジックが実装されています。データを操作するサービスとプロバイダーがあります。そのレベルで同様のデータ操作の原理を適用したらどうなるでしょうか。私たちはアプリケーションのデータ (状態) をコンポーネントの「上の」1つの場所に位置づけ、Observableストリームを介してコンポーネントに向かって値を伝播することができます (ReduxとNgRxはこの場所をstoreと呼びます) 。その状態は複数のコンポーネントに伝播され複数の場所に表示されますが、決してローカルに変更することはできません。変更は「上から」のみやってきて、下のコンポーネントはシステムの現在の状態のみを「反映」します。これにより前述のデータの一貫性という重要なシステムのプロパティが与えられ、状態オブジェクトが信頼できる唯一の情報源になります。事実上、私たちは同じデータを複数箇所に表示することができ、値が一致しないことを恐れることはありません。

状態オブジェクトは状態を操作するコアレイヤーのサービスのメソッドを公開します。状態を変更する必要があるときはいつでも、状態オブジェクトのメソッドを呼び出す (あるいはNgRxを用いる場合はactionをdispachする) ことによってのみ変更が起こります。その次に、変更はストリームを介してプレゼンテーションレイヤー (あるいは他のサービス) へ「下って」伝播されます。このように状態管理はリアクティブです。さらに、この手法を持ってすれば、アプリケーションの状態を操作し共有する厳密なルールにより、システムの予測可能性の度合いも上がります。下はBehavioorSubjectで状態をモデリングするコードスニペットです。

settings.state.ts

@Injectable()
export class SettingsState {

  private updating$ = new BehaviorSubject<boolean>(false);
  private cashflowCategories$ = new BehaviorSubject<CashflowCategory[]>(null);
  
  isUpdating$() {
    return this.updating$.asObservable();
  }
  
  setUpdating(isUpdating: boolean) {
    this.updating$.next(isUpdating);
  }
  
  getCashflowCategories$() {
    return this.cashflowCategories$.asObservable();
  }
  
  setCashflowCategories(categories: CashflowCategory[]) {
    this.cashflowCategories$.next(categories);
  }
  
  addCashflowCategory(category: CashflowCategory) {
    const currentValue = this.cashflowCategories$.getValue();
    this.cashflowCategories$.next([...currentValue, category]);
  }
  
  updateCashflowCategory(updatedCategory: CashflowCategory) {
    const categories = this.cashflowCategories$.getValue();
    const indexOfUpdated = categories.findIndex(category => category.id === updatedCategory.id);
    categories[indexOfUpdated] = updatedCategory;
    this.cashflowCategories$.next([...categories]);
  }
  
  updateCashflowCategoryId(categoryToReplace: CashflowCategory, addedCategoryWithId: CashflowCategory) {
    const categories = this.cashflowCategories$.getValue();
    const updatedCategoryIndex = categories.findIndex(category => category === categoryToReplace);
    categories[updatedCategoryIndex] = addedCategoryWithId;
    this.cashflowCategories$.next([...categories]);
  }
  
  removeCashflowCategory(categoryRemove: CashflowCategory) {
    const currentValue = this.cashflowCategories$.getValue();
    this.cashflowCategories.next(currentValue.filter(category => category !== categoryRemove));
  }
  
}

既に紹介した原理を考慮して、もう一度ユーザーインタラクションを扱う手順を復習しましょう。まず、プレゼンテーションレイヤーでいくつかのイベント (たとえばボタンクリック) があると想像してください。コンポーネントは、ファサードのメソッドsettingsFacade.addCategory()を呼び出して、実行を抽象レイヤーに委譲します。その次に、ファサードはコアレイヤーにおけるサービスのメソッドcategoryApi.create()settingsState.addCategory()を呼び出します。それら2つのメソッドの実行順は私たちの選ぶ同期化戦略 (悲観的または楽観的) によって決まります。最後に、アプリケーションの状態はObservableストリームを介してプレゼンテーションレイヤーに下って伝播されます。このプロセスは明確に定義されています。

flow

モジュール設計

システムの水平方向の分割と、システム全体のコミュニケーションパターンについて説明しました。さて次に、機能モジュールへの水平方向の分離を紹介します。その考え方はアプリケーションを様々なビジネス機能を表現する機能モジュールにスライスするということです。これはより良い保守性のためにシステムを小さい要素に分解するさらにもう一つの手順です。各機能モジュールはコアレイヤー、抽象レイヤー、プレゼンテーションレイヤーで同じ水平方向の分離を共有します。これらのモジュールは、アプリケーションで初回の読み込み時間のかかるブラウザに遅延読み込み (そして事前読み込み) される可能性があるのを留意することは重要です。以下は機能モジュールの分離を説明する図です

modules

私たちのアプリケーションはより技術的な理由で2つの追加モジュールを持っています。シングルトンサービスと設定、AppModuleで必要なサードパーティのモジュールを定義したCoreModuleを持っています。このモジュールはAppModule一度だけインポートされます。2つ目のモジュールは共通のコンポーネント/パイプ/ディレクティブを含むSharedModuleであり、(CommonModuleなど) 共通に使われるAngularモジュールもエクスポートします。下図はインポートの構造を表しています。

imports

モジュールのディレクトリ構造

図はディレクトリ内でSettingsModuleのすべての要素をどのように配置しているかを表しています。それらの機能を表す名前付きでフォルダーの中にファイルを置くことができます。

module

賢いコンポーネントと馬鹿なコンポーネント

紹介する最後のアーキテクチャパターンはコンポーネントそれ自身についてです。私たちはコンポーネントを責務に応じて2つのカテゴリに分割することを望みます。まず最初は賢いコンポーネント (またの名をコンテナーコンポーネント) です。これらのコンポーネントは通常、

  • ファサードと注入された他のサービスをもち
  • コアレイヤーとやり取りし
  • 馬鹿なコンポーネントへデータを渡し
  • 馬鹿なコンポーネントからのイベントに反応し
  • 最高階層でルーティング可能なコンポーネントです (ただし常にではありません!) 。

前述のCategoriesComponentは賢いコンポーネントになります。注入されたSettingsFacadeを持ち、それをアプリケーションのコアレイヤーとやり取りするために使います。

2つ目のカテゴリーには、馬鹿なコンポーネント (またの名をプレゼンテーショナルコンポーネント) があります。このコンポーネントの責務は、UI要素を表現しユーザーインタラクションをイベントを介して「上に向かって」賢いコンポーネントへ委譲するだけです。<button>Click me</button>のようなネイティブのHTML要素を考えましょう。その要素には特定のロジックは実装されていません。「Click me」というテキストは、このコンポーネントへの入力と考えることができます。クリックイベントのような、subscribe可能なイベントもあります。以下は1つのinputイベントを持ちoutputイベントを持たないシンプルなプレゼンテーショナルコンポーネントのコードスニペットです。

budget-progress.component.ts
@Component({
  selector: 'budget-progress',
  templateUrl: './budget-progress.component.html',
  styleUrls: ['./budget-progress.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BudgetProgressComponent {

  @Input()
  budget: Budget;
  today: string;
  
}

まとめ

Angularアプリケーションのアーキテクチャをどのように設計するかという2つの考え方を説明しました。これらの原理は、賢明に適用した場合、継続可能な開発速度を長期的に維持するのに役立ち、簡単に新機能を提供することが可能になります。いくつかの厳密なルールとしてではなく、理にかなったときに採用できる推奨事項として扱ってください。

私たちは抽象化レイヤーと単方向データフロー、リアクティブな状態管理、モジュール設計、賢い/馬鹿なコンポーネントパターンを詳しく見てきました。これらの概念があなたのプロジェクトに役立つことを願います。いつものように、もし質問があれば気軽に問い合わせてください。

この時点で、抽象レイヤーとファサードのアイデアを私に紹介してくれたこのブログ記事を書いたBrecht Billietに大きな賞賛を送りたいと思います。ありがとう、Brecht!階層型アーキテクチャに関する私の見解をレビューしてくれたTomekSułkowskiにも感謝します。

36
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?