SOLID — 2000年代初頭にRobert C. Martin(Uncle Bob)によって提唱された、オブジェクト指向プログラミングの5つの原則です。DigitalOcean(2024年)によると、SOLIDはSingle Responsibility、Open-Closed、Liskov Substitution、Interface Segregation、Dependency Inversionの頭文字を取ったものです。これらの原則はクリーンアーキテクチャの基礎を形成し、Android開発(MVP、MVVM、Clean Architecture)やiOS(VIPER、TCA)で適用されています。
重要ポイント
SOLID — オブジェクト指向設計の5つの原則を表す記憶術的な頭字語です。この用語はRobert C. Martinが記事「Design Principles and Design Patterns」(2000年)で紹介し、後に書籍「Agile Software Development: Principles, Patterns, and Practices」(2002年)で広められました。SOLIDはフレームワークやライブラリではなく、コードの結合度を低くし、テストしやすく、変更しやすくするためのプラクティスの集合です。
Clean Coder Blog(2014年)によると、各SOLID原則は特定の設計問題を解決します。SRPは神クラスと戦い、OCPは連鎖的な変更を防ぎ、LSPは不正確な継承から保護し、ISPは大きなインターフェースを避け、DIPは強い結合を減らします。これらは一緒になってクリーンアーキテクチャの基盤を形成し、AndroidプロジェクトでMVP、MVVM、MVIとともに使用されています。
Single Responsibility Principle(SRP) — 単一責任の原則。定式化:「クラスに変更理由が1つだけあるべき」というものです。つまり、各モジュールやクラスは、正確に1つの機能または1つのドメインエンティティに対して責任を持ちます。クラスがユーザー管理とメール送信の両方を扱う場合、変更理由が2つあるため、SRPに違反します。
Robert C. Martin(2002年)によると、SRPは最も重要でありながら最も違反されている原則です。モバイル開発では、Activity/FragmentでUIロジック、ナビゲーション、ネットワーキング、ビジネスロジックを混在させることでSRPがしばしば違反されます。解決策は各レイヤーを別々のクラスに抽出することです。UIロジックはViewModel、データはRepository、ナビゲーションはNavControllerが担当します。
プロファイルの読み込み、設定の保存、メール送信を行うUserManagerクラスを考えてみましょう。これらは3つの異なる責任であり、それぞれ別々のクラスに抽出する必要があります。UserProfileRepository(読み込み)、UserSettingsStorage(保存)、EmailService(送信)です。クライアントコード(ViewModel)は依存性注入を通じて3つすべてを使用し、各クラスは独立して簡単にテストでき、他に影響を与えることなく変更できます。
// ❌ SRP違反:Activityがネットワーク、DB、UIを把握
class ProfileActivity : AppCompatActivity() {
fun loadProfile() {
api.getUser() // ネットワーク呼び出し
db.saveUser() // DB操作
updateUI() // UI更新
}
}
// ✅ SRP遵守:レイヤーが分離されている
class ProfileViewModel : ViewModel() {
private val repo = UserRepository()
fun loadProfile() { repo.getUser() }
}
SRP違反の兆候:200行を超えるクラス、異なるドメインのメソッド、異なる理由での頻繁な変更。Android開発のルールはシンプルです。Activityは画面のライフサイクルだけを処理し、ViewModelはUI状態を処理し、Repositoryはデータソースを処理します。
SRP原則はクラスだけでなく、サービスレベルのアーキテクチャにも適用されます。各マイクロサービスは1つのドメインエンティティを処理します。UserService — ユーザーのみ、PaymentService — 支払いのみ、NotificationService — 通知のみ。これにより、サービスの独立したスケーリング、デプロイ、テストが可能になります。モバイルアプリケーションでは、マイクロサービスレベルでのSRPは、APIクライアントをドメインごとに分離することで現れます。
Open-Closed Principle(OCP) — クラスは拡張に対して開かれ(新しい振る舞いを追加できる)、修正に対して閉じられる(既存のコードは変更されない)べきです。これはポリモーフィズム、抽象クラス、インターフェースを通じて達成されます。既存のメソッドにif-elseを追加する代わりに、新しいインターフェースの実装が作成されます。
Clean Coder Blog(2014年)によると、OCPはStrategyパターンと組み合わせることで最も効果的です。例えば、アプリが異なる支払い方法(Google Pay、Apple Pay、PayPal)をサポートする場合、支払いプロセッサにswitch-caseを追加する必要はありません。各支払い方法は共通のPaymentGatewayインターフェースを実装し、新しい支払いシステムは既存のコードを変更せずに新しいクラスとして追加されます。
// ✅ OCP:拡張に開かれ、修正に閉じられている
interface PaymentGateway {
fun processPayment(amount: Double): Boolean
}
class GooglePayGateway : PaymentGateway {
override fun processPayment(amount: Double) = true
}
// 新しい決済システム — 既存コードの変更なし
class ApplePayGateway : PaymentGateway {
override fun processPayment(amount: Double) = true
}
Liskov Substitution Principle(LSP) — Barbara Liskovの置換原則。SがTのサブタイプである場合、T型のオブジェクトはプログラムのプロパティを変更することなくS型のオブジェクトで置き換えられます。形式的には:基底クラスを使用する関数は、その任意のサブクラスでも正しく動作する必要があります。基底クラスがスローしない例外をサブクラスがスローする場合、LSPに違反しています。
Robert C. Martin(2002年)によると、LSPはSOLID原則の中で最も理解が難しいものです。古典的な違反例は、Rectangleを継承するSquareクラスです。SquareのsetWidthが幅と高さの両方を設定する場合、Rectangleの振る舞いを期待するクライアントコードは予期しない結果を得ます。モバイル開発では、ViewModelを継承する際にLSPがしばしば違反されます — 子ViewModelが必須の依存関係を追加する場合です。
// ❌ LSP違反:SquareがRectangleの振る舞いを壊す
open class Rectangle(open var width: Int, open var height: Int)
class Square(side: Int) : Rectangle(side, side) {
override var width
get() = super.width
set(value) { super.setBoth(value, value) }
}
Interface Segregation Principle(ISP) — クライアントは使用しないインターフェースに依存すべきではありません。1つの「太った」インターフェースの代わりに、複数の狭く特化したインターフェースを作成します。クラスがインターフェースを実装しているが、一部のメソッドがUnsupportedOperationExceptionをスローしたり空のままだったりする場合、それはISP違反の明確な兆候です。
DigitalOcean(2024年)によると、ISPはViewModelとRepositoryを設計する際にモバイル開発で特に重要です。すべてのCRUDメソッドを持つ1つのUserRepositoryインターフェースの代わりに、QueryUserRepository(読み取り専用)とCommandUserRepository(書き込み)を作成する方が良いでしょう。そうすれば、読み取り専用のクライアント(UI要素)はQueryインターフェースのみに依存し、書き込みメソッドについて何も知る必要がありません。
// ❌ 大きなインターフェース — クライアントが不要なメソッドを強制的に実装
interface UserOperations {
fun getUser(id: String): User
fun saveUser(user: User)
fun deleteUser(id: String)
fun exportUsers(): File
}
// ✅ ISP:分離されたインターフェース
interface UserReader { fun getUser(id: String): User }
interface UserWriter { fun saveUser(user: User) }
interface UserDeleter { fun deleteUser(id: String) }
Dependency Inversion Principle(DIP) — 高レベルモジュールは低レベルモジュールに依存すべきではありません。両方とも抽象化(インターフェース)に依存すべきです。抽象化は詳細に依存すべきではなく、詳細が抽象化に依存すべきです。これは「依存性注入」(DI)ではありませんが、DIはDIPを実装する一般的な方法です。
Robert C. Martin(2019年)によると、DIPはクリーンアーキテクチャの基盤です。ViewModel(高レベル)は直接RetrofitApi(詳細)のインスタンスを作成すべきではありません。代わりに、ViewModelはUserRepositoryインターフェースに依存し、Retrofitを使用した具体的なUserRepositoryImplがコンストラクタを通じて渡されます。Androidでは、DIPはHilt/DaggerやKoinを通じて実装されます。すべての依存関係はDIコンテナを介して提供されます。
// ✅ DIP:Moduleは抽象化に依存し、詳細に依存しない
class UserRepositoryImpl(
private val api: UserApi, // インターフェースに依存
private val db: UserDao // インターフェースに依存
) : UserRepository {
override suspend fun getUser(id: String): User {
return api.fetchUser(id)
}
}
// Hilt DI:詳細はDIモジュールを通じて配線される
@Module
object NetworkModule {
@Provides
fun provideUserApi(retrofit: Retrofit): UserApi =
retrofit.create(UserApi::class.java)
}
モバイル開発におけるSOLIDは、アプリケーションアーキテクチャから個々のクラスまで、すべてのレベルで適用されます。Androidプロジェクトでは、クリーンアーキテクチャがコードを3つのレイヤーに分割します。domain(ビジネスロジック — フレームワークに依存しない)、data(リポジトリ、API、DB)、presentation(UI、ViewModel)です。domainレイヤーはSOLID原則を使用します。ユースケース(SRP)、リポジトリインターフェース(DIP)、エンティティクラス(OCP + LSP)です。
Android Developers Guide(2025年)によると、AndroidでのSRPはViewModel、Repository、Mapperの分離に現れます。OCP — DataSourceインターフェースを通じた新しいデータソースの追加時。LSP — 異なるリポジトリ間での統一的なResult処理。ISP — CQRSアプローチ(Read/Writeリポジトリの分離)。DIP — 依存性注入のためのHilt/Koin。
| 原則 | なしでの問題 | モバイルプロジェクトでの解決策 |
|---|---|---|
| SRP | 1000+行のActivity | ViewModel + UseCase + Repository |
| OCP | 支払いタイプによるswitch-case | Strategy:PaymentGatewayインターフェース |
| LSP | BaseViewModel置き換え時のバグ | サブクラスの契約を確認 |
| ISP | UnsupportedOperationException | Reader / Writerの分離 |
| DIP | ViewModelが手動でRetrofitを作成 | Hilt / Koin DIコンテナ |
SOLIDの間違いは、ほとんどの場合、コードの過度な複雑化に関連しています。1つ目は — コンテキストを考慮せずに原則を文字通りに従うこと。たった1つの「クリーンな」ISPのために、1つのUserServiceクラスを10のインターフェースと15のクラスに分割するのは過剰設計です。SOLIDはツールであって、目的ではありません。2つ目の間違いは — SRPを「1メソッド=1責任」と混同すること。すべてが同じ責任領域に属している限り、クラスは複数のメソッドを持つことができます。
Simple Thread(2024年)によると、3つ目の間違い — AndroidでViewModelを継承する際にLSPを無視すること。基底ViewModelがLiveDataを期待しているのに子がStateFlowを使用する場合、LiveDataに購読しているクライアントコードは更新を受け取りません。4つ目 — テストのためにDIPに違反すること。RepositoryImplが直接OkHttpClientのインスタンスを作成し、単体テストを不可能にします。
黄金律:実際の問題(頻繁な変更、テストの難しさ、重複)を解決する場合にのみSOLIDを適用してください。単純なCRUD画面では、5つの原則すべてに厳密に従うことは過剰です。ビジネスロジック、金融計算、API連携にはSOLIDが不可欠です。
クリーンアーキテクチャ(Robert C. Martin、2012年) — アプリケーションレイヤーレベルでのSOLIDの直接的な適用です。SRPはユースケースの境界を定義します(各ユースケース — 1つのクラス)。OCPはリポジトリインターフェースを通じて実装されます(Data LayerはDomainを変更せずに変更可能)。ISPはユースケースを入力/出力境界に分離します。DIP — 依存関係の方向をDomainレイヤーの内側に向けます。LSPは、任意のリポジトリ実装がユースケースを壊すことなく置き換え可能であることを保証します。
よくある質問
SOLID — コードを変更・テスト・理解しやすくするための5つのルールです。各文字が1つの原則を表します。大きなクラスを書かない(SRP)、既存のコードを変更せず新しいものを追加する(OCP)、サブクラスの振る舞いを壊さない(LSP)などです。
SRP(Single Responsibility)が最も重要と考えられています。その違反は神クラス — テストや変更が難しい巨大なクラス — につながるからです。しかし、DIP(Dependency Inversion)なしではコードは強い結合のままとなり、それもまた重要です。
必須ではありませんが、ライフサイクルの長い商用プロジェクトには強く推奨されます。シンプルなアプリ(1画面、ビジネスロジックなし)にはSOLIDは過剰かもしれません。50画面以上、3人以上の開発者がいるプロジェクトでは、SOLIDは最低限必要なものです。
結果:クラスが「太って」(1000行以上)、1か所の変更が他の3か所を壊し、単体テストが書けなくなり、新機能の追加に日数の代わりに数週間かかります。時間とともに、コードは「Big Ball of Mud」— 絡み合ってもろい状態 — になります。
遵守の兆候:各クラスが200行未満、機能の変更が5ファイル以上に影響しない、10の依存関係をモックせずにテストが書ける、新しい開発者が1日で構造を理解できる。SonarQubeやdetektなどのツールはSRPやDIPの違反を特定するのに役立ちます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。