DUICUO

コードに抽象化レイヤーが必要なのはなぜですか?

[[434567]]

[51CTO.com クイック翻訳]抽象化は、適切に設計されたソフトウェアを書く上で最も重要な側面の 1 つです。

この基本的な概念を理解すると、体系的なアプローチと明確なメンタルモデルが得られ、適切な抽象化を作成する方法を理解することができます。

優れた抽象化は複雑さを軽減し、開発者がコードを変更しやすくし、エラーを減らすことを可能にします。しかし、抽象化の構築は簡単ではありません。では、具体的にどのように、そしてどのような手順で実現するのでしょうか?

抽象化とは何でしょうか?

コード内の抽象化レイヤーについて説明する前に、抽象化とは何かについて簡単に説明しましょう。

抽象化は、次のようにエンティティを単純化するプロセスとして定義できます。

1. 重要でない詳細は省略する。

2. インターフェースを公開します。

この点では、すべての抽象化はほぼ同じです。

自動運転車はこの抽象化の実例です。この場合、クラッチは抽象化されており、ドライバーはより簡単にギアチェンジを行うことができます。

抽象化には欠点もあります。例えば、ドライバーにとってギアシフトは容易になりますが、同時に車の制御性も低下します。そのため、レーシングドライバーにとってクラッチを抽象化するのは賢明ではないかもしれません。

著者の John Ousterhout は、著書「ソフトウェア設計哲学」の中で、抽象化が失敗する 2 つの方法について説明しています。

1. 重要でない詳細を含める: 重要でない詳細を含めることで抽象化が複雑になりすぎて、開発者の認知的負担が増加します。

2. 重要な詳細の省略: 開発者が抽象化を見ても必要な情報がすべて得られないため、Ousterhout はこの種の抽象化を「誤った抽象化」と呼んでいます。

したがって、適切な抽象化には考慮とトレードオフが必要です。

コードの抽象化

抽象化についてはすでに知っていますが、コードにはどのように適用されるのでしょうか?

すべてのコードは、戦略または詳細に分類できます。

  • 戦略: これらはエンティティとビジネス ロジックです。
  • 詳細: これは戦略の実装です。戦略実行の詳細です。

Userエンティティがあるとします。Userには特定のインターフェースとビジネスロジックがあります。このUserエンティティにはグループも含まれており、すべてのユーザーグループを取得するコードを書くように指示されています。

ここで、戦略はユーザー自体です。これはエンティティだからです。しかし、それは getUserGroups 関数でもあります。これはそのエンティティに関連付けられたビジネス ロジックだからです。

実装方法、使用するデータベース、使用する ORM (オブジェクト リレーショナル マッピング)、使用するライブラリ、コードの記述方法、さまざまな実装など、これらはすべてコードの詳細です。

コードでは、戦略を公開しつつ詳細を隠蔽する必要があります。戦略と詳細を分離することで、実装を切り替えたり、簡単にリファクタリングしたりできるようになります。

戦略と詳細が結合されている場合、それらは絡み合っており、変更が一方から他方に伝播するため、リファクタリングが難しくなります。

適切に設計されたシステムでは、戦略と詳細を分離することが重要です。

では、これを抽象化レイヤーにどのように適用できるでしょうか?

抽象化レイヤー

抽象化レイヤーはインターフェースを公開し、その背後にある実装の詳細を隠します。

抽象化レイヤーの目的は、抽象化を実現することです。レイヤー内のメソッドとプロパティは公開インターフェースとして実装されるべきであり、これらのメソッド内の実装は詳細レイヤーですべて行われます。

抽象化レイヤーを作成すると、主に 3 つの利点があります。

1. 集中化​​:1つのレイヤーに抽象化を構築することで、それに関連するすべてのものが一元化され、変更も1か所で行うことができます。集中化は「Don't Repeat Yourself」(DRY)原則に関連していますが、これは誤解されやすい原則です。

DRY(重複知識複製)は、コードの複製だけでなく、知識の複製も含みます。場合によっては、2つの異なるエンティティが同じコードを複製することがあります。これは、分離を可能にし、将来的にこれらのエンティティが独立して進化できるようにするためです。

2. 簡素化:抽象化レイヤーを作成することで、特定の機能を公開し、実装の詳細を隠すことができます。これにより、コードはインターフェースと直接やり取りできるようになり、無関係な実装の詳細を扱う必要がなくなります。これによりコードの可読性が向上し、コードを読む開発者の認知的負担が軽減されます。なぜでしょうか?

戦略は詳細ほど複雑ではないため、戦略とのやり取りはより直接的になります。

3. テスト: 抽象化レイヤーは、詳細を別の詳細セットに置き換えることができるため、テストに最適です。これにより、テスト対象の領域を分離し、テスト ダブルを適切に作成できます。

コードをテストする際、開発者は特定の機能をテストし、実際のデータベースなどのオブジェクトの呼び出しを回避するために、一部の機能についてテスト代替案を作成する必要があります。テスト代替案の過剰使用は、戦略と詳細が複雑に絡み合った場合によく発生し、カバレッジの低下やテストの有用性の大幅な低下につながります。

データベース実装オブジェクトの抽象化レイヤーを作成する場合、開発者はそのレイヤーを置き換えて、残りの機能をテストするときにデータベース応答のみが置き換えられるようにすることができます。

抽象化レイヤーの作成例

グループの API を作成するためのコードを書いているとします。

  1. 関数createUserGroup(グループ, ユーザーID) {
  2. logger.info( 'ユーザー ${userId} のグループを作成しています' )
  3. db.startTransaction();
  4. const isValidGroup = 検証グループ (グループ) ;
  5. if (!isValidGroup) throw new Error( '無効なグループ' );
  6. db.addDoc( 'グループ' ,グループ)
  7. dc.addDoc( 'クォータ/グループ' , 1)
  8. }

上記の例からわかるように、関数のロジックは戦略と詳細と混在しており、抽象化レイヤーを一切使用せずに、様々な機能を処理します。

これは抽象化レイヤーを使用するコードです。

  1. クラス GroupsService {
  2. GROUPS_COLLECTION = 'グループ' ;
  3. グループを作成() {
  4. db.startTransaction();
  5.          
  6. const isValid = this.validateGroup();
  7. if (!isValid) throw new Error( '無効なグループ' )
  8.          
  9. db.addDoc(GROUPS_COLLECTION、グループ)
  10. quotasService.setQuota( '/groups' , 1);
  11.          
  12. db.finishTransaction();
  13. }
  14.      
  15. 検証グループ()
  16.      
  17. グループを削除します。
  18. }
  19. クラスQuotasService {
  20. setQuota(コレクション: 文字列、値:任意) {
  21. dc.addDoc(`quotas/${collection}`, 値)
  22. }
  23. }
  24. 関数createUserGroup(グループ, ユーザーID) {
  25. logger.info(`グループを作成しています のために ユーザー${userId}`)
  26. groupsService.createGroup();
  27.      
  28. 戻る{
  29. ステータス: 200,
  30. メッセージ: 「グループが正常に作成されました」  
  31. }
  32. }

2 番目の実装には多くの利点があります。

1. 実装の詳細が抽象化されているため、理解しやすくなります。読み取るのは、戦略と対話するコードです。

2. すべてが1つのサービスに集中化されている。グループ関連のコードがアプリケーション全体に散在していると想像してみてください。すべての変更はすべての場所に反映される必要があり、率直に言って、これは問題を引き起こします。

3. コードがよりカプセル化されました。`createUserGroup` コントローラはグループの作成のみを認識し、クォータについては認識しなくなりました。クォータは関係なくなったためです。

4. テスト実装に集中し、詳細なレイヤーのみをテスト置換に置き換えることで、テストを容易にします。統合テストに関しては、QuotaServiceとGroupServiceを置き換え、その特定のコントローラーによって実装された実装をテストできます。

考えられる用途

抽象化レイヤーはさまざまな方法で実装できますが、最も一般的な使用例は次のとおりです。

1. 戦略と詳細を分離することで、よりスリムなコンポーネントを作成する:変更とリファクタリングが容易であれば、コードは長期間にわたって耐えられます。戦略と詳細を分離し、コンポーネント間のインタラクションをインターフェースのみで維持することで、将来のコード進化に必要なインフラストラクチャが確保されます。

2. サードパーティ ライブラリのラップ: コード内の古いサードパーティ ライブラリによって他の依存関係をアップグレードできなくなると、特にそれらの依存関係がセキュリティ上のリスクをもたらす場合には、悪夢になります。

独自のインターフェースを使用してサードパーティのライブラリを中央の抽象化レイヤーにラップするのは、インターフェースが公開されている場所でのみ実行する必要があるため、簡単になります。

3. ユーティリティ サービスを作成する: ユーティリティ サービスは、開発速度を向上させ、共通のコード スニペットを再利用するための重要な方法です。

たとえば、多数の異なる時刻と日付を処理する機能を開発している場合は、作業に役立つユーティリティ関数をいくつか作成し、それらを 1 か所にまとめて、後で再利用できるようにしてみてはいかがでしょうか。

まとめ

抽象化レイヤーを作成すると、集中化、簡素化、テストの改善という 3 つの主な利点が得られ、コードが大幅に改善されます。

抽象化レイヤーや抽象化全般は最終目標ではなく、それを達成するための手段であることを覚えておいてください。抽象化には欠点もあります。よくある例として、一部の抽象化はパフォーマンスに悪影響を与える可能性があります。したがって、事前に制限事項を理解することが常に重要です。

原題: Why Your Code Needs Abstraction Layers、著者: Yair Cohen

[この記事は51CTOによって翻訳されました。提携サイトへの転載の際は、元の翻訳者と出典を51CTO.comとして明記してください。]