保守性と拡張性を高めるためのSOLID原則とシステム設計
第13話:抽象度と関数設計(クリーンアーキテクチャー)
(最終更新日:2024.12.09)
(絵が小さい場合はスマホを横に)
抽象度とは?
今回は、保守性と拡張性を高めるための「抽象度」と「関数設計」の重要性を解説する。
ソフトウェアのシステム設計において、抽象度の異なる要素が適切に整理されていないと、変更や拡張が困難になる。
そこで、異なる抽象度を持つコンポーネント間の関係性を整理し、関数設計の際に抽象度の整合性を保つ方法を紹介する。
また、良い設計と悪い設計の具体例を通じて、理解を深めるとともに、リファクタリングを通じて改善する手法も解説する。
[目次]
1.抽象度の階層化
本セクションでは、異なる抽象度を持つコンポーネント間のインタラクションをどのように設計すべきかについて議論し、その重要性と実践方法を解説する。
1.1 抽象度とは何か
抽象度とは、システムの中で扱う情報や操作の具体性のレベルを指す。
高い抽象度は、一般的な概念やビジネスロジックを扱うのに対し、低い抽象度は具体的な実装や技術的な詳細を扱う。
例えば「ユーザーを保存する」という操作は高い抽象度であり、
「SQLクエリを実行してデータベースにレコードを挿入する」という操作は低い抽象度に該当する。
1.2 抽象度の階層化の必要性
システムを抽象度ごとに階層化することで、各層の役割が明確になり、システム全体の理解が容易になる。
また、変更やバグ修正の際に影響範囲を限定できるため、保守性が向上する。
各層が適切な抽象度に応じた役割を果たすことで、設計の一貫性が保たれる。
これにより、システム全体がより直感的に理解でき、他の開発者も簡単にコードを追跡できるようになる。
抽象度の階層化を作り、それぞれでコンポーネント化する
1.3 抽象度の階層化の具体的な方法
システムを以下のような階層に分けることが一般的である。
- ビジネスロジック層:高い抽象度を持ち、ビジネスルールやユースケースを扱う。 この層では、技術的な詳細には依存せず、システムの核心的な機能を定義する。
- アプリケーションロジック層:ビジネスロジック層とインフラ層をつなぐ役割を果たし、少し低い抽象度で具体的な操作を扱う。 例えば、ユーザーインターフェースからの入力をビジネスロジックに伝える、APIリクエストを処理するなどがそれにあたる。
- インフラストラクチャ層:最も低い抽象度を持ち、技術的な詳細を扱う。 具体的なデータベースアクセス、ネットワーク通信、ファイル操作などが、ここに該当する。
また、異なる抽象度を持つコンポーネント間でのやり取りは、以下の原則に基づいて設計されるべきだ。
高い抽象度を持つコンポーネントが低い抽象度のコンポーネントに依存することは避け、
高い抽象度を持つインターフェースを介して依存関係を制御する。
そして、異なる抽象度のコンポーネント間では、インターフェースや抽象クラスを使用して依存関係を逆転させる。
これにより、具体的な実装を変更しても、高い抽象度のビジネスロジックに影響を与えないようになる。
1.4 サービス層とリポジトリ層の設計(具体例)
例えば、Webアプリケーションでユーザー管理を行う際、ビジネスロジック(サービス層)はユーザーデータの管理を担当し、
リポジトリ層はデータベースとのやり取りを担当する。この時、サービス層はユーザー管理のビジネスロジックを担当し、リポジトリに依存する。
しかし、その依存はIUserRepositoryインターフェースを通じて行う。
サービス層はリポジトリ層に直接依存しない
1.5 メリットと注意点
抽象度を適切に階層化することで、システムの一部を変更しても他の部分に影響を与えることなく、拡張や保守が容易になる。
そして、共通のビジネスロジックやインターフェースを他のプロジェクトでも再利用しやすくなる。
しかしながら、抽象度を細分化しすぎると、システムが複雑になり保守しにくくなる可能性があるので、適度なバランスが重要だ。
2.関数の貸し借りはしない
このセクションでは、クラス、コンポーネントをまたぐ関数呼び出しを避けるべき理由と、それを実現するための設計手法を解説する。
2.1 抽象度の同じコンポーネントからの関数の貸し借りはしない
関数設計において重要なのは、同じ抽象度を持つクラス内、コンポーネント内で関数を呼び出すようにし、
異なるクラス、コンポーネントからの関数の貸し借りを避けることが重要だ。
異なるクラスの関数に対して、安易に呼び出しを行ってしまうと、
システムが拡大した際、その関数がどこまで影響しているかが分からなくなる。
一か所にまとめて、そこから呼び出すか、
インターフェースを挟み、それぞれで関数定義することが大事だ。
これにより、関数を変更したとしても、影響範囲が限定される。
関数の貸し借りを避ける
2.2 抽象度の異なるコンポーネントからの関数呼び出しを避ける理由
また、抽象度の異なるコンポーネントからの関数呼び出しも避けた方が良い。理由としては以下の通りである。
設計の一貫性の消失:異なる抽象度を持つコンポーネントからの関数呼び出しが発生すると、
設計の一貫性が損なわれ、コードが混乱しやすくなる。
例えば、ビジネスロジック(高い抽象度)から直接データベースアクセス(低い抽象度)をすると、
システムの理解が難しくなり、変更に対する脆弱性が増す。
保守性の低下:異なる抽象度の関数を混在させると、システムのある部分を変更する際に、予期しない箇所に影響を与える可能性が高まる。
これにより、修正や拡張にかかる時間が増加し、保守性が低下する。
依存関係の複雑化:高い抽象度のコンポーネントが低い抽象度の関数に直接依存することで、依存関係が複雑化し、テストやデバッグが困難になる。
2.3 適切な関数呼び出しの設計方法
抽象度ごとのモジュール分割:システムを設計する際、抽象度に応じてモジュールやクラスを分割する。
例えば、ビジネスロジックを扱うモジュール、データアクセスを扱うモジュール、
ユーザーインターフェースを扱うモジュールなど、役割に応じて明確に分離する。
ファサードパターンの利用:ファサードパターンを使用して、低い抽象度の機能をまとめたインターフェースを提供し、
高い抽象度のコンポーネントからはそのインターフェースを通じてアクセスするようにする。
これにより、低い抽象度の詳細を隠し、設計の一貫性を保つ。
依存性注入の活用:依存性注入を使用して、抽象度の異なるコンポーネント間の依存関係を制御する。
これにより、高い抽象度のコンポーネントは低い抽象度の実装に直接依存せず、インターフェースを介して間接的に利用することができる。
2.4 具体例: レポート生成システムにおける抽象度の分離
例えば、企業の財務報告書を生成するシステムを考える。
高い抽象度のビジネスロジックは、レポートの生成を指示し、低い抽象度のデータアクセス層はデータベースから必要なデータを取得する。
この際、ビジネスロジックは、どのデータベースから情報が取得されるかを意識せず、レポート生成を指示する。
この層では、IReportGeneratorというインターフェースを利用する。
一方、データアクセス層では、データベースからデータを取得し、レポートを生成する具体的な実装を行う。 ビジネスロジックからは直接呼び出されることはなく、IReportGeneratorインターフェースを介して利用される。
2.5 同じ抽象度の関数間での設計手法
モジュール内での責任分割: 同じ抽象度の関数間では、責任を明確に分割し、単一責任の原則に基づいて設計する。
これにより、関数の再利用性が高まり、メンテナンスが容易になる。
リファクタリングの実施:異なる抽象度の関数が混在している場合は、リファクタリングを行い、
関数の役割を再定義し、適切な抽象度の層に移動する。
2.6 メリットと注意点
この設計手法により、システムの保守性と拡張性が向上する。コードの理解が容易になり、
新たな開発者がプロジェクトに参加した場合でも、スムーズに理解できるようになる。
一方、抽象度を過度に分割しすぎると、逆にシステムが複雑になる可能性がある。
バランスを取りながら、必要な部分でのみ適用することが重要だ。
3.まとめ
今回は、保守性と拡張性を確保するために、抽象度と関数設計を適切に整理する重要性を学んだ。 異なる抽象度を持つコンポーネント間の関係性を明確にし、それらはインターフェースに依存させる。 関数設計では同じ抽象度のモジュールをまとめることで、変更や拡張がしやすい設計が可能になる。 このような設計原則を守ることで、システム全体の整合性が向上し、長期的なメンテナンスコストの削減につながる。