💻

依存性逆転の原則を用いたレイヤードアーキテクチャについて

2025/02/19に公開
2

はじめに

前稿で説明したレイヤードアーキテクチャは、ソフトウェア開発において広く採用されている設計パターンですが、いくつかの欠点も存在します。
主な欠点として、層間の高い結合度や変更の波及が挙げられます。特にドメイン層がデータアクセス層に依存している場合は、データアクセス層の変更がドメイン層に波及してしまいます。
ビジネスロジック(コアな部分)を実装しているドメイン層への影響を小さくしたい場合は、依存性逆転の原則(DIP)を導入することで、ドメイン層の保守性を高めることができます。
このような(ドメイン層を中心に考える)設計パターンはドメイン駆動設計と呼称されています。

記事の対象者

  • これからシステム設計を始める方
  • これまでアーキテクチャを意識してこなかった方

概要

依存性逆転の原則を用いたレイヤードアーキテクチャは、従来のレイヤード構造SOLID原則の依存性逆転の原則を組み合わせた設計手法です。
これにより、レイヤー間の結合度を低減し、システム全体の柔軟性と保守性を向上させます。
本記事では、このアーキテクチャの全体像とメリット/デメリットと実際の具体例について解説します。

依存性逆転の原則の基本原則

依存性逆転の原則は、SOLID原則の1原則であり、以下の2つの基本原則から成り立っています。

  1. 高水準モジュールは低水準モジュールに依存してはならない。両者とも抽象に依存すべきである。
  2. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。

これらの原則により、システムの高水準のドメイン層の実装が低水準の実装(例えば、データベースアクセス層など)に直接依存することなく、抽象化されたインターフェースを通じて依存するようになります。これにより、具体的な実装の変更が高水準モジュールに影響を与えにくくなります。

https://zenn.dev/tsutani2828/articles/solid_principle

レイヤードアーキテクチャでの依存性逆転の適用

一般的なレイヤードアーキテクチャでは、上位層が下位層に依存する構造ですが、これにはいくつかの問題が伴います。依存性逆転の原則を適用することで、以下のように依存関係を管理します:

  • 抽象層の導入:各レイヤー間にインターフェースや抽象クラスを設け、具体的な実装ではなくこれらの抽象に依存するようにします。
  • 依存関係の方向転換:下位層が上位層に依存するように変更します。

このアプローチにより、各レイヤーは他のレイヤーの具体的な実装に依存せず、変更が容易になります。
以下の図では、ドメイン層 ⇔ データアクセス層の依存関係を逆転させた例となっています。


DIP適用前のレイヤードアーキテクチャ


DIP適用後のレイヤードアーキテクチャ

メリットとデメリット

メリット

  1. 結合度の低減
    • 抽象に依存することで、具体的な実装に対する依存が減少し、各レイヤーの独立性が高まります。
  2. テストの容易さ
    • モックを使用したユニットテストが容易になります。高水準モジュールを低水準モジュールから切り離してテストできるため、テストの効率が向上します。
  3. 柔軟性の向上
    • 実装の変更や拡張が容易になります。例えば、データベースの種類を変更する際も、インターフェースを実装する新しいクラスを作成するだけで済みます。
  4. 再利用性の向上
    • 抽象インターフェースに基づいた設計により、異なるコンテキストでも同じインターフェースを再利用できます。

デメリット

  1. 初期設計の複雑さ
    • 抽象層やインターフェースを導入するため、初期の設計が複雑になる場合があります。特に小規模なプロジェクトでは、オーバーヘッドとなることがあります。
  2. 学習コスト
    • 依存性逆転の原則や抽象化の概念に慣れていない開発者にとっては、理解と実装に一定の学習コストが必要です。
  3. 過剰な抽象化のリスク
    • 必要以上に抽象化を進めると、逆にコードが複雑になり、保守が難しくなる可能性があります。
  4. パフォーマンスへの影響
    • 抽象層を介した呼び出しが増えることで、わずかながらパフォーマンスに影響を与える場合があります。ただし、現代のほとんどのアプリケーションではこの影響は無視できる程度です。

データアクセス層を上位層とする設計

依存性逆転の原則を適用すると、データアクセス層を他の上位層から独立させることが可能になります。具体的には以下のように設計します:

  1. インターフェースの定義:ドメイン層(ビジネスロジック層)でデータアクセスのためのインターフェースを定義します。
  2. 実装の提供:データアクセス層では、このインターフェースを実装します。
  3. 依存関係の注入:ドメイン層にインターフェースを注入し、具体的な実装に依存しない形を維持します。

この設計により、データベースの変更や異なるデータソースへの対応が容易になります。

具体例

クラス図

(DIP適用前)

(DIP適用後)

解説

  • DIP適用前はドメイン層がデータアクセス層に依存しており、具象クラスが具象クラスに依存しています。データアクセス層の変更がドメイン層のコアな実装にまで波及してしまいます。(業務システムであれば可能な限り触れたくない部分かと思います)
  • DIP適用後はデータアクセス層にがドメイン層に依存しており、具象クラスは抽象クラスに依存しています。データアクセス層の変更はドメイン層に波及しません。

プログラム

以下に、C#を使用してDIP適用後のレイヤードアーキテクチャによるクラス図の実装例を示します。

ドメイン層(ビジネスロジック)

IUserRepository.cs
namespace Domain.Interfaces{
    // インターフェースの定義
    public interface IUserRepository
    {
        void Add(User user);
        IEnumerable<User> GetAll();
    }
}
User.cs
namespace Domain.Entities{
    // エンティティの定義
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}
UserService.cs
namespace Domain.Services{
    // サービスクラス
    public class UserService
    {
        private readonly IUserRepository _userRepository;

        // コンストラクタインジェクション
        public UserService(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public void AddUser(User user)
        {
            _userRepository.Add(user);
        }

        public IEnumerable GetUsers()
        {
            return _userRepository.GetAll();
        }
    }
}

データアクセス層

UserRepository.cs
namespace DataAccess.Repositories{
    public class UserRepository : IUserRepository
    {
        public void Add(User user)
        {
            // 外部サービス(データベース等)に対してUserデータを追加する処理
        }

        public IEnumerable<User> GetAll()
        {
            // 外部サービス(データベース等)からUserデータを取得する処理
            return _users;
        }
    }
}

アプリケーションのエントリポイント(依存関係の注入)

Program.cs
class Program
{
    static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // 依存関係の注入
        builder.Services.AddScoped<IUserRepository, UserRepository>();
    }
}

解説

  • インターフェースの定義:IUserRepositoryインターフェースをドメイン層で定義し、データアクセスの抽象を提供します。
  • サービスクラス:UserServiceクラスは、IUserRepositoryに依存しており、具体的な実装には依存しません。外部サービスがOracleなのかPostgreSQLなのか等はドメイン層が気にする必要がありません。
  • 実装クラス:UserRepositoryクラスは、IUserRepositoryを実装し、具体的なデータ管理ロジックを提供します。
  • 依存関係の注入:Programクラス(エントリーポイント)では、UserRepositoryのインスタンスを作成し、UserServiceにコンストラクタインジェクションをしています。これにより、UserServiceはUserRepositoryの具体的な実装に依存せず、柔軟な変更が可能です。

まとめ

依存性逆転の原則をレイヤードアーキテクチャに適用することで、システムの柔軟性、再利用性、テスト容易性が向上します。一方で、初期設計の複雑さや開発コストの増加といったデメリットも存在します。プロジェクトの規模や要件に応じて、DIPの適用の有無を検討することが重要です。

参考文献


この記事がレイヤードアーキテクチャの基本を理解する助けになれば幸いです。ご質問やご指摘事項がございましたら、お気軽にお寄せください。

Discussion

junerjuner

依存性逆転の法則(DIP)

dependency inversion principle なら 法則ではなく原則ではないでしょうか?

https://ja.wikipedia.org/wiki/依存性逆転の原則

tsutani2828tsutani2828

dependency inversion principle なら 法則ではなく原則ではないでしょうか?

ご指摘ありがとうございます。
おっしゃる通りで、誤っておりました。
記事の方は修正いたしました。