SOLID — 由Robert C. Martin(Uncle Bob)在21世纪初提出的五项面向对象编程原则。根据 DigitalOcean, 2024,SOLID 代表单一职责、开闭原则、里氏替换、接口隔离和依赖反转。这些原则构成了Clean Architecture的基础,并应用于Android(MVP、MVVM、Clean Architecture)和iOS(VIPER、TCA)开发中。
要点
SOLID — 一个助记缩略词,表示五项面向对象设计原则。该术语由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消除紧耦合。它们共同构成了Clean Architecture的基础,用于采用MVP、MVVM和MVI架构的Android项目。
单一职责原则(SRP) — 公式:“一个类应该只有一个改变的理由。”这意味着每个模块或类恰好负责一个功能或一个领域实体。如果一个类同时管理用户和发送电子邮件 — 它有两个改变的理由,这就违反了SRP。
根据 Robert C. Martin, 2002,SRP 是最重要也是最常被违反的原则。在移动开发中,SRP经常在Activity/Fragment中被违反,将UI逻辑、导航、网络工作和业务逻辑混合在一起。解决方案 — 将每一层分离到单独的类中:ViewModel负责UI逻辑,Repository负责数据,NavController负责导航。
考虑 UserManager 类,它加载个人资料、保存设置和发送电子邮件。这是三个不同的职责,每个都应分离到单独的类:UserProfileRepository(加载)、UserSettingsStorage(保存)和EmailService(发送)。客户端代码(ViewModel)通过依赖注入使用所有三个类,每个类都可以轻松隔离测试并独立修改而不影响其他类。
// ❌ 违反SRP:Activity知道网络、数据库和UI
class ProfileActivity : AppCompatActivity() {
fun loadProfile() {
api.getUser() // 网络调用
db.saveUser() // 数据库操作
updateUI() // UI更新
}
}
// ✅ 遵守SRP:各层已分离
class ProfileViewModel : ViewModel() {
private val repo = UserRepository()
fun loadProfile() { repo.getUser() }
}
违反SRP的迹象:类包含超过200行,有来自不同领域的方法,经常因不同原因而改变。对于Android开发,规则很简单:Activity只负责屏幕的生命周期,ViewModel负责UI状态,Repository负责数据源。
SRP 原则不仅适用于类,也适用于服务级别的架构。每个微服务负责一个领域实体:UserService — 仅用户,PaymentService — 仅支付,NotificationService — 仅通知。这允许服务独立扩展、部署和测试。在移动应用中,微服务级别的SRP体现在API客户端按领域划分上。
开闭原则(OCP) — 类应对扩展开放(可以添加新行为),对修改关闭(现有代码不变)。通过多态、抽象类和接口实现。与其在现有方法中添加if-else,不如创建接口的新实现。
根据 Clean Coder Blog, 2014,OCP 与策略模式结合时最为有效。例如,如果应用程序支持不同的支付方式(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
}
里氏替换原则(LSP) — 如果S是T的子类型,那么T的对象可以替换为S的对象,而不改变程序的属性。正式来说:使用基类的函数应能与其任何子类正确工作。如果子类在基类不抛出异常的地方抛出异常 — LSP被违反。
根据 Robert C. Martin, 2002,LSP 是最难理解的SOLID原则。经典违反示例 — Square(正方形)类继承Rectangle(矩形)。如果Square的setWidth同时设置了宽度和高度,期望Rectangle行为的客户端代码将得到意外结果。在移动开发中,LSP常在ViewModel继承时被违反,当子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) }
}
接口隔离原则(ISP) — 客户端不应依赖它们不使用的接口。不要创建一个“臃肿”的接口,而是创建多个小而专的接口。如果一个类实现了一个接口,但部分方法抛出UnsupportedOperationException或为空 — 这是违反ISP的明显迹象。
根据 DigitalOcean, 2024,ISP 在移动开发中设计ViewModel和Repository时尤为重要。不要创建一个包含所有CRUD方法的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) }
依赖反转原则(DIP) — 高层模块不应依赖低层模块。两者都应依赖抽象(接口)。抽象不应依赖细节 — 细节应依赖抽象。这与“依赖注入”(DI)不同,尽管DI是实现DIP的常见方式。
根据 Robert C. Martin, 2019,DIP 是Clean Architecture的基础。ViewModel(高层)不应直接创建RetrofitApi(细节)的实例。相反,ViewModel依赖UserRepository接口,具体的UserRepositoryImpl实现(使用Retrofit)通过构造函数传入。在Android中,DIP通过Hilt/Dagger或Koin实现:所有依赖项通过DI容器提供。
// ✅ DIP:模块依赖抽象而非细节
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项目中,Clean Architecture将代码分为三层:domain(业务逻辑 — 独立于框架)、data(仓库、API、DB)和presentation(UI、ViewModel)。Domain层使用SOLID原则:用例(SRP)、仓库接口(DIP)、实体类(OCP + LSP)。
根据 Android Developers Guide, 2025,SRP 在Android中体现在ViewModel、Repository和Mapper的分离上。OCP — 通过DataSource接口添加新数据源时。LSP — 统一处理来自不同仓库的Result。ISP — 采用CQRS方法(分离读/写仓库)。DIP — 通过Hilt/Koin进行依赖注入。
| 原则 | 没有它的问题 | 移动项目中的解决方案 |
|---|---|---|
| SRP | Activity超过1000行 | ViewModel + UseCase + Repository |
| OCP | 按支付类型switch-case | 策略模式:PaymentGateway接口 |
| LSP | 替换BaseViewModel时出现错误 | 检查子类契约 |
| ISP | UnsupportedOperationException | Reader / Writer分离 |
| DIP | ViewModel手动创建Retrofit | Hilt / Koin DI容器 |
SOLID错误 通常与过度复杂化代码有关。第一个 — 不考虑上下文地机械遵循原则。将一个UserService类拆分为10个接口和15个类以实现“纯粹”的ISP是过度设计。SOLID是工具,不是目标。第二个错误 — 混淆SRP和“一个方法 = 一个职责”。一个类可以有多个方法,如果它们都属于同一职责领域。
根据 Simple Thread, 2024,第三个错误 — 在Android的ViewModel继承中忽略LSP。如果基础ViewModel期望LiveData,而子类使用StateFlow — 订阅LiveData的客户端代码将收不到更新。第四个 — 为了测试而违反DIP:RepositoryImpl直接创建OkHttpClient实例,使单元测试无法进行。
黄金法则:当SOLID解决实际问题时(频繁变更、测试困难、重复),才应用它。对于简单的CRUD页面,严格遵循所有五项原则是过度的。对于业务逻辑、财务计算和API交互,SOLID是必须的。
Clean Architecture(Robert C. Martin, 2012)— 在应用程序层级别直接应用SOLID。SRP定义用例边界(每个用例 — 一个类)。OCP通过仓库接口实现(Data层可以更改而不影响Domain)。ISP提供用例的输入/输出边界分离。DIP — 依赖方向指向Domain层内部。LSP确保任何仓库实现可替换而不会破坏用例。
常见问题
SOLID — 五个编写代码的规则,使代码易于修改、测试和理解。每个字母是一个原则:不要写大类(SRP),不要修改现有代码 — 添加新的(OCP),不要破坏继承者的行为(LSP)等等。
SRP(单一职责) 被认为最重要,因为违反它会导致上帝类 — 难以测试和修改的巨大类。然而,没有DIP(依赖反转),代码保持紧耦合,这也很关键。
不是必须的,但对于长生命周期的商业项目非常推荐。对于简单的应用(一个页面,没有业务逻辑),SOLID可能过度。对于50+页面和3+开发人员的项目,SOLID是必要的最低要求。
后果:类变得“臃肿”(1000+行),一处的更改破坏三处其他,无法编写单元测试,添加新功能需要数周而非数天。随着时间的推移,代码变成“Big Ball of Mud” — 混乱而脆弱。
遵守的迹象:每个类少于200行,功能更改不影响5+个文件,测试无需mock 10个依赖项即可编写,新开发人员一天内理解结构。SonarQube和detekt等工具有助于检测SRP和DIP的违反。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。