SOLID原則とは
SOLID原則は、オブジェクト指向のソフトウェア設計で使用される、重要な5つの原則をまとめたものです。
書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」では、SOLID原則は「設計の原則」という見出しで紹介されています。
本記事では、SOLID のうち「インターフェース分離の原則(ISP: Interface Segregation Principle)」に焦点を当て、iOSアプリ開発のクラス設計にどのように適用できるかを実例とともにご紹介できればと思います。
インターフェース分離の原則とは
インターフェース分離の原則は、SOLIDの「I」に該当する原則です。
この原則の意味を端的にいうと、
「自身が依存している型の、必要ではない機能に依存してはいけない」というものだと理解しています。
「必要ではない機能に依存している状況」とは、具体的にどういう場合でしょうか。
以下で具体例とともに考えていきます。
iOSアプリ開発におけるインターフェース分離
本記事では、「メモとタスクを管理するアプリ」を開発するケースで考えてみます。
このアプリのビジネスロジックを実装する場合、
簡単な構成だと以下のようなレイヤー図になるかと思います。

レイヤードアーキテクチャなどでよく見られる構成ですね。
この構成について、以下で簡単に説明します。
- 「メモ」と「タスク」は
Database
に保存されている-
Database
は、RealmSwift
やCoreData
などのフレームワークに依存する
-
-
UseCase
はRepository
を介してデータベースからデータを取得して使用する-
Repository
は、Database
から取得したデータ型をEntity
に変換する
-
ドメインロジック (UseCase
) が RealmSwift
などのフレームワークに依存しないように、Repository
という変換層を経由してデータを取得するような構成です。
実際にコードで書いてみると、まず Repository
の実装は以下の通りです。
// 📦 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 する
}
}
続いて、UseCase
で Repository
からデータを取得する実装は以下の通りです。
(MemoUseCase
の例だけですが、TaskUseCase
も同じようなコードになります)
// 📦 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
のメソッドを呼び出せば「タスク」も取得することができるため、
以下のようなコードも書けてしまいます。
/// メモに関するビジネスロジックを担うクラス
public class MemoUseCase {
public func firstTask() -> TaskEntity? {
// ❌ 「タスク」に関する処理も実装できてしまう
repository.fetchAllTask()
.first
}
}
このような構造の問題点は以下の通りです。
-
UseCase
が担当するビジネスロジックとは本来関係のないRepository
のメソッドに意図せず依存してしまう- 不要な依存メソッドが変更された場合でも、ビジネスロジックに影響が及ぶ恐れがある
- 別の開発担当者に、
UseCase
のクラス設計の意図から逸れた機能を拡張される恐れがある
下図の「❌」は、UseCase
が意図せず依存してしまっている Repository
のメソッドです。

今回は小規模な例なのでそこまで問題にならないかもしれませんが、
このプロジェクトが成長して巨大なプロジェクトになると、
Repository
が変更された時に影響を受けるクラスの数が膨大になってしまいます。
このような構造を禁止するための原則が「インターフェース分離の原則」です。
つまり、上記の例は「インターフェース分離の原則」に違反しているとみなせます。
Repository のインターフェースを分離
「インターフェース分離の原則」を適用して、Repository
のインターフェースを分割します。
まず、UseCase
が本当に必要な Repository
のメソッドだけをまとめたプロトコルを以下のように定義します。
// 📦 Interface モジュール
/// MemoUseCase に必要なメソッドだけをまとめたプロトコル
public protocol MemoRepository {
func fetchAllMemo() -> [MemoEntity]
}
/// TaskUseCase に必要なメソッドだけをまとめたプロトコル
public protocol TaskRepository {
func fetchAllTask() -> [TaskEntity]
}
そして、Repository
を以下の通り定義します。
// 📦 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
でも使用されていますね。
// https://developer.apple.com/documentation/swift/codable
public typealias Codable = Decodable & Encodable
最後に、UseCase
は以下の通り必要な Repository
の機能にのみ依存します。
// 📦 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
}
}
最終的な依存関係グラフ図は以下のようになりました。

まとめ
今回は、Repository
の実装クラス DatabaseRepository
自体は大きく作りました。
しかし、アプリが成長するにつれて Repository
の実装クラス自体を分割した方が良いという判断になるかもしれません。
そのような場合でも、UseCase
が依存する Repository
のプロトコルが分割されていれば、UseCase
の実装自体は変更せずに対応できます。
プロジェクトの経過時間や成熟度に合わせて柔軟に実装を変更することができるようになることも、インターフェース分離の原則のメリットだと感じました。
参考文献