デジタル環境が進化するにつれて、最新の Web サイトの複雑さも増しています。より優れたユーザー エクスペリエンスと高度な機能に対する需要が高まる中、フロントエンド開発者は、スケーラブルで保守可能で効率的なアーキテクチャを作成するという課題に直面しています。
フロントエンド アーキテクチャに関して利用可能な大量の記事とリソースの中で、かなりの数がクリーン アーキテクチャとその適応に焦点を当てています。実際、調査対象の約 70 件の記事の 50% 以上が、フロントエンド開発のコンテキストでクリーン アーキテクチャについて論じています。
豊富な情報にもかかわらず、明らかな問題が残っています。提案されたアーキテクチャのアイデアの多くは、実際の運用環境では実装されていない可能性があります。これにより、実際のシナリオでの有効性と適用可能性について疑問が生じます。
この懸念に駆り立てられて、私はフロントエンドにクリーン アーキテクチャを実装するための 6 か月の旅に乗り出し、これらのアイデアの現実に立ち向かい、小麦と籾殻を分離できるようにしました。
この記事では、この旅の経験と洞察を共有し、フロントエンドにクリーン アーキテクチャをうまく実装する方法に関する包括的なガイドを提供します。
この記事では、課題、ベスト プラクティス、および実際のソリューションに光を当てることで、進化し続ける Web サイト開発の世界をナビゲートするために必要なツールをフロントエンド開発者に提供することを目的としています。
今日の急速に進化するデジタル エコシステムでは、開発者はフロントエンド フレームワークに関して選択の余地がありません。この豊富なオプションは、多くの問題に対処し、開発プロセスを簡素化します。
しかし、それはまた、開発者の間で際限のない議論を引き起こし、それぞれが自分の好むフレームワークが他のフレームワークよりも優れていると主張しています。実際、ペースの速いこの世界では、新しい JavaScript ライブラリが毎日登場し、フレームワークがほぼ毎月導入されています。
このようなダイナミックな環境で柔軟性と適応性を維持するには、特定のフレームワークやテクノロジーを超えるアーキテクチャが必要です。
これは、トレンドの変化や技術の進歩に対応する必要がある、メンテナンスを伴う製品会社や長期契約にとって特に重要です。
フレームワークなどの詳細から独立しているため、取り組んでいる製品に集中し、そのライフサイクル中に発生する可能性のある変更に備えることができます。
恐れるな;この記事は、このジレンマに対する答えを提供することを目的としています。
フロントエンドにクリーン アーキテクチャを実装するために、私は数人のフルスタックおよびバックエンドの開発者と緊密に協力して、フロントエンドの経験が最小限であっても、アーキテクチャが理解しやすく保守しやすいものになるようにしました。
そのため、私たちのアーキテクチャの主な要件の 1 つは、フロントエンドの複雑さに精通していない可能性があるバックエンド開発者や、広範なフロントエンドの専門知識を持たない可能性があるフルスタック開発者にとってのアクセシビリティです。
フロントエンドとバックエンドのチーム間のシームレスな協力を促進することにより、アーキテクチャはギャップを埋め、統一された開発エクスペリエンスを作成することを目指しています。
残念ながら、素晴らしいものを構築するには、背景知識を得る必要があります。基礎となる原則を明確に理解すると、実装プロセスが容易になるだけでなく、アーキテクチャがソフトウェア開発のベスト プラクティスに確実に準拠するようになります。
このセクションでは、アーキテクチャ アプローチの基礎を形成する 3 つの重要な概念、 SOLID 原則、クリーン アーキテクチャ(実際には SOLID 原則に由来する)、およびAtomic Designを紹介します。これらの領域について強く感じる場合は、このセクションをスキップできます。
SOLID は、開発者がスケーラブルで保守可能なモジュラー ソフトウェアを作成する際の指針となる 5 つの設計原則を表す頭字語です。
このトピックをさらに深く掘り下げたい場合は、ぜひお試しください。問題ありません。しかし、今のところ、私が提示したことは、さらに先に進むのに十分です。
この記事に関して、SOLID は何を提供してくれるのでしょうか?
Robert C. Martin は、SOLID の原則と、さまざまなアプリケーションの開発における豊富な経験に基づいて、Clean Architecture の概念を提案しました。この概念について議論するとき、その構造を視覚的に表すために下の図がよく参照されます。
したがって、クリーン アーキテクチャは新しい概念ではありません。関数型プログラミングやバックエンド開発など、さまざまなプログラミング パラダイムで広く使用されています。
Lodash などのライブラリや多数のバックエンド フレームワークは、SOLID 原則に根ざしたこのアーキテクチャ アプローチを採用しています。
クリーン アーキテクチャは、関心の分離と、アプリケーション内の独立したテスト可能なレイヤーの作成を強調し、システムの理解、保守、および変更を容易にすることを主な目標としています。
アーキテクチャは、同心円またはレイヤーに編成されています。それぞれが明確な境界、依存関係、および責任を持っています。
クリーン アーキテクチャは、外側のレイヤーから内側のレイヤーへの依存関係の流れを促進し、コア ビジネス ロジックが使用される特定のテクノロジやフレームワークから独立したままであることを保証します。
これにより、変化する要件やテクノロジー スタックに簡単に適応できる、柔軟で保守可能でテスト可能なコードベースが実現します。
Atomic Design は、インターフェイスを最も基本的な要素に分解し、それらをより複雑な構造に再構築することによって、UI コンポーネントを編成する方法論です。 Brad Frost は、2008 年に「Atomic Design Methodology」というタイトルの記事でこの概念を初めて紹介しました。
以下は、Atomic Design の概念を示す図です。
これは、5 つの異なるレベルで構成されています。
Atomic Design を採用することで、開発者はモジュール性、再利用性、UI コンポーネントの明確な構造など、いくつかの利点を得ることができます。これは、Design System のアプローチに従う必要があるためですが、これはこの記事のトピックではないので、次に進みます。
フロントエンド開発のためのクリーン アーキテクチャに関する十分な情報に基づいた視点を開発するために、私はアプリケーションを作成する旅に乗り出しました。 6 か月間、このプロジェクトに取り組みながら貴重な洞察と経験を得ることができました。
そのため、この記事全体で提供されている例は、アプリケーションでの実地経験に基づいています。透明性を維持するために、すべての例は公開されているコードから派生しています。
次のリポジトリにアクセスして、最終結果を調べることができます。
前述のように、オンラインで入手できるクリーン アーキテクチャの実装は多数あります。ただし、これらの実装全体でいくつかの共通要素を特定できます。
これらの共通点を理解することで、クリーン アーキテクチャの基本構造を理解し、特定のニーズに適応させることができます。
アプリケーションのコア部分には以下が含まれます。
ユース ケース: ユース ケースでは、データの保存、更新、フェッチなど、さまざまな操作のビジネス ルールについて説明します。たとえば、ユース ケースには、Notion から単語のリストをフェッチすることや、学習した単語に対するユーザーの毎日の連続記録を増やすことが含まれる場合があります。
基本的に、ユースケースは、ビジネスの観点からアプリケーションのタスクとプロセスを処理し、システムが目的に応じて機能するようにします。
モデル: モデルは、アプリケーション内のビジネス エンティティを表します。これらは、TypeScript インターフェイスを使用して定義できるため、ニーズやビジネス要件に確実に合わせることができます。
たとえば、ユースケースで Notion から単語のリストをフェッチする場合、適切なビジネス ルールと制約に従って、そのリストのデータ構造を正確に記述するモデルが必要になります。
操作: 特定のタスクをユース ケースとして定義することが現実的でない場合や、ドメインの複数の部分で使用できる再利用可能な機能を作成したい場合があります。たとえば、Notion の単語を名前で検索する関数を作成する必要がある場合、そのような操作はここに配置する必要があります。
操作は、アプリケーション内のさまざまなコンテキストで共有および利用できるドメイン固有のロジックをカプセル化するのに役立ちます。
リポジトリ インターフェイス: ユース ケースには、データにアクセスする手段が必要です。依存性逆転の原則に従って、ドメイン層は他の層に依存すべきではありません (他の層はそれに依存しています)。したがって、この層はリポジトリのインターフェースを定義します。
実装の詳細ではなく、インターフェイスを指定することに注意することが重要です。リポジトリ自体は、実際のデータ ソースに依存しないリポジトリ パターンを利用し、それらのソースとの間でデータをフェッチまたは送信するためのロジックを強調します。
1 つのリポジトリで複数の API を実装でき、1 つのユース ケースで複数のリポジトリを利用できることに注意してください。
この層はデータ アクセスを担当し、必要に応じてさまざまなソースと通信できます。フロントエンド アプリケーションを開発していることを考えると、このレイヤーは主にブラウザー API のラッパーとして機能します。
これには、REST、ローカル ストレージ、IndexedDB、音声合成などの API が含まれます。
OpenAPI タイプと HTTP クライアントを生成する場合、API レイヤーはそれらを配置するのに理想的な場所であることに注意することが重要です。このレイヤー内には、次のものがあります。
API アダプター: API アダプターは、アプリケーションで使用されるブラウザー API に特化したアダプターです。このコンポーネントは、REST 呼び出しと、アプリのメモリまたは使用するその他のデータ ソースとの通信を管理します。
必要に応じて、独自のオブジェクト ストレージ システムを作成して実装することもできます。専用の API アダプターを使用することで、さまざまなデータ ソースと対話するための一貫したインターフェイスを維持できるため、必要に応じてデータ ソースを簡単に更新または変更できます。
リポジトリ レイヤーは、複数の API の統合を管理し、API 固有の型をドメイン型にマッピングし、データを変換するための操作を組み込むことにより、アプリケーションのアーキテクチャで重要な役割を果たします。
たとえば、音声合成 API をローカル ストレージと組み合わせたい場合、これは最適な場所です。このレイヤーには以下が含まれます。
アダプター層は、これらの層の間の相互作用を調整し、それらを結び付ける役割を果たします。このレイヤーには、以下を担当するモジュールのみが含まれます。
プレゼンテーション層は、ユーザー インターフェイス (UI) のレンダリングと、アプリケーションとのユーザー インタラクションの処理を担当します。アダプター、ドメイン、および共有レイヤーを活用して、機能的でインタラクティブな UI を作成します。
プレゼンテーション レイヤーは、アトミック デザイン手法を使用してそのコンポーネントを編成し、スケーラブルで保守可能なアプリケーションを実現します。ただし、このレイヤーは、クリーン アーキテクチャの実装に関する主要な主題ではないため、この記事の主な焦点にはなりません。
集中ユーティリティ、構成、共有ロジックなど、すべての共通要素には指定された場所が必要です。ただし、この記事では、この層について深く掘り下げることはしません。
共通コンポーネントがアプリケーション全体でどのように管理および共有されるかを理解するためだけに言及する価値があります。
さて、コーディングに入る前に、テストについて議論することが不可欠です。アプリケーションの信頼性と正確性を確保することは非常に重要であり、アーキテクチャの各レイヤーに対して堅牢なテスト戦略を実装することが重要です。
アーキテクチャの各レイヤーに包括的なテスト戦略を実装することで、アプリケーションの信頼性、正確性、保守性を確保しながら、開発中にバグが発生する可能性を減らすことができます。
ただし、小さなアプリケーションを構築している場合は、アダプター レイヤーでの統合テストで十分です。
さて、Clean Architecture についてしっかりと理解して、おそらくそれについて自分の意見を形成したところで、もう少し深く掘り下げて実際のコードを調べてみましょう。
ここでは単純な例のみを紹介することに注意してください。ただし、より詳細な例に興味がある場合は、この記事の冒頭で述べた私の GitHub リポジトリを自由に調べてください。
「実生活」では、クリーン アーキテクチャは大規模なエンタープライズ レベルのアプリケーションで真価を発揮しますが、小規模なプロジェクトではやり過ぎかもしれません。ということで、本題に入りましょう。
私のアプリケーションを例として、API 呼び出しを実行して特定の単語の辞書候補を取得する方法を示します。この特定の API エンドポイントは、2 つの Web サイトを Web スクレイピングして、意味と例のリストを取得します。
ビジネスの観点から、このエンドポイントは、ユーザーが特定の単語を検索できるようにする「単語の検索」ビューにとって重要です。ユーザーが単語を見つけてログインすると、Web スクレイピングされた情報を自分の Notion データベースに追加できます。
まず、前に説明したレイヤーを正確に反映するフォルダー構造を確立する必要があります。構造は次のようになります。
client ├── adapter ├── api ├── domain ├── presentation ├── repository └── shared
クライアント ディレクトリは、多くのプロジェクトで「src」フォルダと同様の目的を果たします。この特定の Next.js プロジェクトでは、フロントエンド フォルダーを「クライアント」、バックエンド フォルダーを「サーバー」と命名する規則を採用しました。
このアプローチにより、アプリケーションの 2 つの主要コンポーネントを明確に区別できます。
プロジェクトに適したフォルダー構造を選択することは、開発プロセスの早い段階で行うべき重要な決定です。リソースの編成に関しては、さまざまな開発者が独自の好みとアプローチを持っています。
リソースをページ名でグループ化する人もいれば、OpenAPI によって生成されたサブディレクトリの命名規則に従う人もいれば、アプリケーションが小さすぎてこれらのソリューションのいずれかを保証できないと考える人もいます。
重要なのは、プロジェクトの特定のニーズと規模に最も適した構造を選択すると同時に、明確で保守可能なリソースの編成を維持することです。
私は 3 番目のグループに属しているため、構造は次のようになります。
client ├── adapter │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── api │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── domain │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ ├── supabase └── repository ├── local-storage ├── rest ├── speech-synthesis └── supabase
この記事では、共有レイヤーとプレゼンテーション レイヤーを省略することにしました。これは、より深く掘り下げたい人は、詳細について私のリポジトリを参照できると信じているからです。それでは、いくつかのコード例を見て、クリーン アーキテクチャをフロントエンド アプリケーションに適用する方法を説明しましょう。
私たちの要件を考えてみましょう。ユーザーとして、意味と例を含む提案のリストを受け取りたいです。したがって、単一の辞書の提案は次のようにモデル化できます。
interface DictionarySuggestion { example: string; meaning: string; }
1 つの辞書の提案について説明したので、Web スクレイピングによって取得された単語が、ユーザーが入力したものとは異なる場合や修正されている場合があることに言及することが重要です。これに対応するために、後で修正したバージョンをアプリで使用します。
したがって、辞書の提案と単語の修正のリストを含むインターフェイスを定義する必要があります。最終的なインターフェースは次のようになります。
export interface DictionarySuggestions { suggestions: DictionarySuggestion[]; word: string; }
このインターフェイスをエクスポートしているため、 export
キーワードが含まれています。
モデルができたので、次はそれを使用します。
import { DictionarySuggestions } from './rest.models'; export interface RestRepository { getDictionarySuggestions: (word: string) => Promise<DictionarySuggestions | null>; }
この時点で、すべてが明確になるはずです。ここでは API についてはまったく説明していないことに注意してください。リポジトリ自体の構造は非常に単純です。各メソッドが特定の型のデータを非同期的に返すいくつかのメソッドを持つオブジェクトだけです。
リポジトリは常にドメイン モデル形式でデータを返すことに注意してください。
それでは、ビジネス ルールをユース ケースとして定義しましょう。コードは次のようになります。
export type GetDictionarySuggestionsUseCaseUseCase = UseCaseWithSingleParamAndPromiseResult< string, DictionarySuggestions | null >; export const getDictionarySuggestionsUseCase = ( restRepository: RestRepository, ): GetDictionarySuggestionsUseCaseUseCase => ({ execute: (word) => restRepository.getDictionarySuggestions(word), });
最初に注意すべきことは、ユースケースを定義するために使用される一般的なタイプのリストです。これを実現するために、ドメイン ディレクトリにuse-cases.types.ts
ファイルを作成しました。
domain ├── local-storage ├── rest ├── speech-synthesis ├── supabase └── use-cases.types.ts
これにより、サブディレクトリ間でユース ケースの型を簡単に共有できます。 UseCaseWithSingleParamAndPromiseResult
の定義は次のようになります。
export interface UseCaseWithSingleParamAndPromiseResult<TParam, TResult> { execute: (param: TParam) => Promise<TResult>; }
このアプローチは、ドメイン レイヤー全体でユース ケース タイプの一貫性と再利用性を維持するのに役立ちます。
なぜexecute
関数が必要なのか疑問に思われるかもしれません。ここには、実際のユース ケースを返すファクトリがあります。
この設計上の選択は、リポジトリの実装をユース ケース コードで直接参照したくないという事実と、リポジトリをインポートで使用したくないという事実によるものです。このアプローチにより、後で依存性注入を簡単に適用できます。
ファクトリ パターンとexecute
機能を使用することで、リポジトリの実装の詳細をユース ケース コードとは別に保持できるため、アプリケーションのモジュール性と保守性が向上します。
このアプローチは、ドメイン層が他の層に依存しない依存関係逆転の原則に従い、異なるリポジトリ実装を交換したり、アプリケーションのアーキテクチャを変更したりする際の柔軟性を高めます。
まず、インターフェースを定義しましょう。
export interface RestApi { getDictionarySuggestions: (word: string) => Promise<AxiosResponse<DictionarySuggestions>>; }
ご覧のとおり、インターフェイスでのこの関数の定義は、リポジトリでの定義とよく似ています。ドメイン タイプは既に応答を記述しているため、同じタイプを再作成する必要はありません。
API が生データを返すことに注意することが重要です。これが、完全なAxiosResponse<DictionarySuggestions>
を返す理由です。そうすることで、API レイヤーとドメイン レイヤーを明確に分離し、データの処理と変換をより柔軟に行うことができます。
この API の実装は次のようになります。
export const getRestApi = (axiosInstance: AxiosInstance): RestApi => ({ getDictionarySuggestions: async (word: string) => { const encodedCurrentDate = encodeURIComponent(word); const response = await axiosInstance.get( `${RestEndpoints.GET_DICTIONARY_SUGGESTIONS}?word=${encodedCurrentDate}`, ); return response; } });
この時点で、物事はより興味深いものになります。議論する最初の重要な側面は、 axiosInstance
の注入です。これにより、コードが非常に柔軟になり、堅実なテストを簡単に作成できます。これは、クエリ パラメーターのエンコードまたは解析を処理する場所でもあります。
ただし、入力文字列のトリミングなど、他のアクションをここで実行することもできます。 axiosInstance
を注入することで、懸念事項を明確に分離し、API 実装がさまざまなシナリオや外部サービスの変更に適応できるようにします。
インターフェイスはすでにドメインによって定義されているため、リポジトリを実装するだけです。したがって、最終的な実装は次のようになります。
export const getRestRepository = (restApi: RestApi): RestRepository => ({ getDictionarySuggestions: async (word) => { const { data } = await restApi.getDictionarySuggestions(word); if (!data?.suggestions?.length) { return null; } return formatDictionarySuggestions(data); } });
言及すべき重要な側面は、API に関連しています。 getRestRepository
と、以前に定義したrestApi
を渡すことができます。前述のように、テストが容易になるため、これは有利です。簡単にformatDictionarySuggestions
を調べることができます:
export const formatDictionarySuggestions = ({ suggestions, word, }: DictionarySuggestions): DictionarySuggestions => { const cleanedWord = cleanUpString(word); const cleanedSuggestions = suggestions.map((_suggestion) => { const cleanedMeaning = cleanUpString(_suggestion.meaning); const cleanedExample = cleanUpString(_suggestion.example); return { meaning: cleanedMeaning, example: cleanedExample, }; }); return { word: cleanedWord, suggestions: cleanedSuggestions, }; };
この操作は、ドメインのDictionarySuggestions
モデルを引数として取り、文字列のクリーンアップを実行します。つまり、不要なスペース、改行、タブ、および大文字を削除します。それは非常に簡単で、隠れた複雑さはありません。
注意すべき重要なことは、この時点では、API の実装について心配する必要がないということです。念のために言っておきますが、リポジトリは常にドメイン モデルでデータを返します。そうしないと、依存関係の逆転の原則が破られるため、そうする必要はありません。
そして今のところ、ドメイン層はその外側で定義されたものに依存していません。
この時点で、すべてが実装され、依存性注入の準備ができているはずです。 rest モジュールの最終的な実装は次のとおりです。
import { getRestRepository } from '@repository/rest/rest.repository'; import { getRestApi } from '@api/rest/rest.api'; import { getDictionarySuggestionsUseCase } from '@domain/rest/rest.use-cases'; import { axiosInstance } from '@shared/axios.instance'; const restApi = getRestApi(axiosInstance); const restRepository = getRestRepository(restApi); export const restModule = { getDictionarySuggestions: getDictionarySuggestionsUseCase(restRepository).execute, };
それは正しい!私たちは、特定のフレームワークに縛られることなく、クリーン アーキテクチャの原則を実装するプロセスを経てきました。このアプローチにより、コードが適応可能になり、必要に応じてフレームワークやライブラリを簡単に切り替えることができます。
テストに関して言えば、リポジトリをチェックアウトすることは、このアーキテクチャでテストがどのように実装および編成されているかを理解するための優れた方法です。
クリーン アーキテクチャの強固な基盤により、さまざまなシナリオをカバーする包括的なテストを記述して、アプリケーションをより堅牢で信頼性の高いものにすることができます。
実証されているように、クリーン アーキテクチャの原則に従い、懸念事項を分離することで、保守可能、スケーラブル、およびテスト可能なアプリケーション構造が実現します。
このアプローチにより、新しい機能の追加、コードのリファクタリング、プロジェクトでのチームとの作業が最終的に容易になり、アプリケーションの長期的な成功が保証されます。
サンプル アプリケーションでは、React がプレゼンテーション層に使用されます。アダプター ディレクトリには、残りのモジュールとの対話を処理するhooks.ts
という追加のファイルがあります。このファイルの内容は次のとおりです。
import { restModule } from '@adapter/rest/rest.module'; import { useAxios } from '@shared/hooks'; export const useDictionarySuggestions = () => { const { data, error, isLoading, mutate } = useAxios(restModule.getDictionarySuggestions); return { dictionarySuggestions: data, getDictionarySuggestions: mutate, dictionarySuggestionsError: error, isDictionarySuggestionsLoading: isLoading, }; };
この実装により、プレゼンテーション層の操作が非常に簡単になります。 useDictionarySuggestions
フックを使用することにより、プレゼンテーション レイヤーは、データ マッピングの管理や、主要な機能とは関係のないその他の責任について心配する必要がなくなります。
この関心の分離は、クリーン アーキテクチャの原則を維持するのに役立ち、より管理しやすく保守しやすいコードにつながります。
何よりもまず、提供されている GitHub リポジトリからコードに飛び込んで、その構造を調べることをお勧めします。
他に何ができますか?空は限界です!それはすべて、特定の設計ニーズに依存します。たとえば、データ ストア (Redux、MobX、またはカスタムのものでも構いません) を組み込むことで、データ レイヤーを実装することを検討できます。
または、RxJS を使用してバックエンドとの非同期通信を処理するなど、レイヤー間のさまざまな通信方法を試すこともできます。これには、ポーリング、プッシュ通知、またはソケット (基本的には、あらゆるデータ ソースに対応する準備) が含まれる可能性があります。
基本的に、階層化されたアーキテクチャを維持し、逆依存の原則を順守している限り、自由に探索して実験してください。ドメインが設計の中心にあることを常に確認してください。
そうすることで、さまざまなシナリオや要件に適応できる、柔軟で保守しやすいアプリケーション構造を作成できます。
この記事では、React を使用して構築された言語学習アプリケーションのコンテキスト内で、クリーン アーキテクチャの概念を掘り下げました。
階層化されたアーキテクチャを維持し、逆依存の原則を順守することの重要性と、関心を分離することの利点を強調しました。
クリーン アーキテクチャの大きな利点は、特定のフレームワークに縛られることなく、アプリケーションのエンジニアリング面に集中できることです。この柔軟性により、アプリケーションをさまざまなシナリオや要件に適応させることができます。
ただし、このアプローチにはいくつかの欠点があります。場合によっては、厳密なアーキテクチャ パターンに従うと、ボイラープレート コードが増えたり、プロジェクト構造が複雑になったりする可能性があります。
さらに、ドキュメントへの依存を減らすことは、長所と短所の両方になる可能性があります。これにより、自由と創造性が向上しますが、チーム メンバー間の混乱や誤解が生じる可能性もあります。
これらの潜在的な課題にもかかわらず、クリーン アーキテクチャの実装は、特に広く受け入れられているアーキテクチャ パターンが存在しない React のコンテキストでは、非常に有益です。
何年にもわたる苦労の後でアーキテクチャに対処するのではなく、プロジェクトの開始時にアーキテクチャを検討することが不可欠です。
実際のクリーン アーキテクチャの例を調べるには、次の URL にある私のリポジトリを自由にチェックしてください。
うわー、これはおそらく私が今まで書いた中で最も長い記事です。それは信じられないほど感じます!