iOSアプリ開発でSOLID原則のインターフェース分離を意識してクラス設計する【Swift】

## SOLID原則とは

SOLID原則は、オブジェクト指向のソフトウェア設計で使用される、重要な5つの原則をまとめたものです。

書籍「**Clean Architecture 達人に学ぶソフトウェアの構造と設計**」では、SOLID原則は「**設計の原則**」という見出しで紹介されています。

本記事では、SOLID のうち「**インターフェース分離の原則（ISP: Interface Segregation Principle）**」に焦点を当て、iOSアプリ開発のクラス設計にどのように適用できるかを実例とともにご紹介できればと思います。

## インターフェース分離の原則とは

インターフェース分離の原則は、SOLIDの「I」に該当する原則です。

この原則の意味を端的にいうと、
「**自身が依存している型の、必要ではない機能に依存してはいけない**」というものだと理解しています。

「必要ではない機能に依存している状況」とは、具体的にどういう場合でしょうか。

以下で具体例とともに考えていきます。

## iOSアプリ開発におけるインターフェース分離

本記事では、「**メモとタスクを管理するアプリ**」を開発するケースで考えてみます。

このアプリのビジネスロジックを実装する場合、
簡単な構成だと以下のようなレイヤー図になるかと思います。

<br>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179818/a27b7301-be90-3346-46b2-e9565fa4ebc7.png" width=500>

レイヤードアーキテクチャなどでよく見られる構成ですね。
この構成について、以下で簡単に説明します。

* 「メモ」と「タスク」は `Database` に保存されている
  * `Database` は、`RealmSwift` や `CoreData` などのフレームワークに依存する
* `UseCase` は `Repository` を介してデータベースからデータを取得して使用する
  * `Repository` は、`Database` から取得したデータ型を `Entity` に変換する

ドメインロジック (`UseCase`) が `RealmSwift` などのフレームワークに依存しないように、`Repository` という変換層を経由してデータを取得するような構成です。

実際にコードで書いてみると、まず `Repository` の実装は以下の通りです。

<br>

```swift

// 📦 Interface モジュール
//  (protocol 定義だけをまとめたモジュールです)

/// Repository が準拠すべきプロトコル
public protocol Repository {
    /// Databaseからすべての「メモ」を取得する
    func fetchAllMemo() -> [MemoEntity]
    /// Databaseからすべての「タスク」を取得する
    func fetchAllTask() -> [TaskEntity]
}

// 📦 Domain モジュール

import Interface

/// Database から取得したデータを Entity に変換して返却するクラス
public class DatabaseRepository: Repository {

    public func fetchAllMemo() -> [MemoEntity] {
        let objects = database.fetchAllMemo()
        // Entity へ変換して return する
    }

    public func fetchAllTask() -> [TaskEntity] {
        let objects = database.fetchAllTask()
        // Entity へ変換して return する
    }
}
```

<br>

続いて、`UseCase` で `Repository` からデータを取得する実装は以下の通りです。
(`MemoUseCase` の例だけですが、`TaskUseCase` も同じようなコードになります)

<br>


```swift
// 📦 Domain モジュール

import Interface

/// メモに関するビジネスロジックを担うクラス
public class MemoUseCase {

    // 本クラスは Repository に依存する
    private let repository: Repository

    // Repository のインスタンスはイニシャライザで DI される
    public init(repository: Repository) {
        self.repository = repository
    }

    /// 最新のメモを取得
    public func fetchMostRecentlyCreatedMemo() -> MemoEntity? {
        // Repository からすべてのメモを取得し、任意のメモだけを返却する
        repository.fetchAllMemo()
            .sorted {...}
            .first
    }
}
```

### UseCaseが巨大なリポジトリに依存することの問題

ここで、ある問題が発生します。

本来、`MemoUseCase` は「メモ」に関するビジネスロジックだけを担当する想定で作成されたクラスです。
ところが、`Repository` のメソッドを呼び出せば「タスク」も取得することができるため、
以下のようなコードも書けてしまいます。


```swift
/// メモに関するビジネスロジックを担うクラス
public class MemoUseCase {

    public func firstTask() -> TaskEntity? {
        // ❌ 「タスク」に関する処理も実装できてしまう
        repository.fetchAllTask()
            .first
    }
}
```

このような構造の問題点は以下の通りです。

* `UseCase` が担当するビジネスロジックとは本来関係のない `Repository` のメソッドに意図せず依存してしまう
  * 不要な依存メソッドが変更された場合でも、ビジネスロジックに影響が及ぶ恐れがある
  * 別の開発担当者に、`UseCase` のクラス設計の意図から逸れた機能を拡張される恐れがある

下図の「❌」は、`UseCase` が意図せず依存してしまっている `Repository` のメソッドです。

<br>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179818/46da7beb-0c9b-7e00-679b-378a07215b50.png" width=600>


<br>
<br>

今回は小規模な例なのでそこまで問題にならないかもしれませんが、
このプロジェクトが成長して巨大なプロジェクトになると、
`Repository` が変更された時に影響を受けるクラスの数が膨大になってしまいます。

このような構造を禁止するための原則が「インターフェース分離の原則」です。

つまり、上記の例は「インターフェース分離の原則」に違反しているとみなせます。

### Repository のインターフェースを分離

「インターフェース分離の原則」を適用して、`Repository` のインターフェースを分割します。

まず、`UseCase` が本当に必要な `Repository` のメソッドだけをまとめたプロトコルを以下のように定義します。

```swift

// 📦 Interface モジュール

/// MemoUseCase に必要なメソッドだけをまとめたプロトコル
public protocol MemoRepository {
    func fetchAllMemo() -> [MemoEntity]
}

/// TaskUseCase に必要なメソッドだけをまとめたプロトコル
public protocol TaskRepository {
    func fetchAllTask() -> [TaskEntity]
}
```

そして、`Repository` を以下の通り定義します。


```swift
// 📦 Interface モジュール

/// Repository は、MemoRepository と TaskRepository に準拠した型
public typealias Repository = MemoRepository & TaskRepository

// 📦 Domain モジュール

public class DatabaseRepository: Repository {
    // 先ほどと同じように Repository を実装

    public func fetchAllMemo() -> [MemoEntity] {
        // ...
    }

    public func fetchAllTask() -> [TaskEntity] {
        // ...
    }
}
```

`Repository` は、`MemoRepository` と `TaskRepository` を組み合わせた型です。

`typealias` で複数のプロトコルをまとめるアプローチは、標準型の `Codable` でも使用されていますね。

```swift
// https://developer.apple.com/documentation/swift/codable
public typealias Codable = Decodable & Encodable
```

最後に、`UseCase` は以下の通り必要な `Repository` の機能にのみ依存します。

```swift
// 📦 Domain モジュール

import Interface

public class MemoUseCase {

    // 🟢 Repository の必要な機能にのみ依存
    private let repository: MemoRepository

    public init(repository: MemoRepository) {
        self.repository = repository
    }

    public func fetchMostRecentlyCreatedMemo() -> MemoEntity? {
        repository.fetchAllMemo()
            .sorted {...}
            .first
    }
}
```

最終的な依存関係グラフ図は以下のようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179818/0870b36b-891e-091a-d7ef-9c117f53a69b.png" width=600>


# まとめ

今回は、`Repository` の実装クラス `DatabaseRepository` 自体は大きく作りました。

しかし、アプリが成長するにつれて `Repository` の実装クラス自体を分割した方が良いという判断になるかもしれません。

そのような場合でも、`UseCase` が依存する `Repository` のプロトコルが分割されていれば、`UseCase` の実装自体は変更せずに対応できます。

プロジェクトの経過時間や成熟度に合わせて柔軟に実装を変更することができるようになることも、インターフェース分離の原則のメリットだと感じました。

# 参考文献

https://www.kadokawa.co.jp/product/301806000678/