设计原则 ⭐⭐


SOLID 原则 ⭐⭐

SOLID 是面向对象设计(OOD)中最经典、最核心的五大设计原则的首字母缩写,由 Robert C. Martin("Uncle Bob")在 2000 年代初系统化提出。这五条原则并非独立存在的"规则",而是一套相互配合的设计哲学体系——它们共同指向一个目标:让软件系统在面对需求变化时,能够以最小的代价进行扩展和修改,同时保持高内聚、低耦合的健康结构。

在 Android 开发中,SOLID 原则的重要性尤为突出。Android 应用天然具有生命周期复杂、组件众多、UI 与业务逻辑交织的特点——一个 Activity 可能同时承担视图渲染、数据加载、事件处理、权限管理等多重职责。如果不遵循良好的设计原则,代码会迅速膨胀为难以测试、难以维护的"God Object"。而在 Android Framework 层(AOSP 源码),Google 的工程师们也大量运用了 SOLID 原则来保证系统的可扩展性——从 View 体系的继承层次,到 Binder 机制的接口抽象,处处可见这些原则的身影。

接下来,我们逐一深入每个原则,结合 Android 应用层与 Framework 层的真实案例进行详细解析。


S — 单一职责原则 (Single Responsibility Principle) ⭐

核心定义:一个类只负责一件事

单一职责原则(SRP)的经典定义是:

A class should have one, and only one, reason to change. (一个类应该有且只有一个引起它变化的原因。)

这里的关键词不是"一件事",而是 "一个变化的原因"(one reason to change)。所谓"职责",本质上是指变化的轴线——如果一个类承担了两个不同的职责,当其中一个职责的需求发生变化时,你不得不修改这个类,而这次修改可能会意外影响另一个职责的正常工作。这就是 SRP 要规避的核心风险。

举个最直观的例子:一个 Android Activity 既负责渲染 UI,又负责从网络拉取数据,还负责缓存数据到数据库。当后端 API 格式变更时,你需要修改这个 Activity;当 UI 设计稿改版时,你也要修改同一个 Activity;当数据库 schema 升级时,你还是要改这个 Activity。三个完全不同的变化原因,都指向同一个类——这就是典型的 SRP 违反。

降低复杂度

遵循 SRP 最直接的收益就是降低单个类的复杂度。一个类只承担一个职责,意味着:

  • 代码行数更少:单个文件通常控制在 200-400 行以内,可读性大幅提升。
  • 认知负担更低:开发者阅读一个类时,只需理解一个维度的逻辑。
  • 测试更简单:单一职责的类通常只需要少量 mock 依赖,单元测试编写成本低。
  • 变更影响面小:修改一个职责不会波及其他不相关的功能。
  • 复用性提高:职责单一的类更容易在不同场景中被复用。

反面案例:God Activity

我们先来看一个在实际 Android 开发中极为常见的 SRP 违反案例——所谓的 "God Activity":

Kotlin
// ❌ 反面案例:一个 Activity 承担了至少 4 种职责
// 这是 Android 开发中最常见的"大泥球"模式
class UserProfileActivity : AppCompatActivity() {
 
    // ---- 职责1: UI 状态管理 ----
    private lateinit var tvUserName: TextView       // 用户名文本
    private lateinit var tvEmail: TextView           // 邮箱文本
    private lateinit var ivAvatar: ImageView         // 头像图片
    private lateinit var progressBar: ProgressBar    // 加载动画
 
    // ---- 职责2: 网络请求(数据获取)----
    private val httpClient = OkHttpClient()          // 直接持有网络客户端
 
    // ---- 职责3: 本地缓存(数据持久化)----
    private lateinit var database: SQLiteDatabase    // 直接持有数据库引用
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_profile)
 
        // 初始化视图(职责1)
        tvUserName = findViewById(R.id.tv_user_name)
        tvEmail = findViewById(R.id.tv_email)
        ivAvatar = findViewById(R.id.iv_avatar)
        progressBar = findViewById(R.id.progress_bar)
 
        // 加载用户数据:同时涉及网络请求 + 缓存 + UI 更新
        loadUserProfile()
    }
 
    // 这个方法混合了网络请求、JSON解析、数据库操作、UI更新 —— 4种职责纠缠在一起
    private fun loadUserProfile() {
        progressBar.visibility = View.VISIBLE       // UI操作:显示加载中
 
        // 职责2: 构建网络请求
        val request = Request.Builder()
            .url("https://api.example.com/user/profile")
            .build()
 
        // 职责2: 发起异步网络调用
        httpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                // 职责3: 网络失败时尝试从本地数据库读取缓存
                val cursor = database.rawQuery(
                    "SELECT * FROM user_profile LIMIT 1", null
                )
                if (cursor.moveToFirst()) {
                    val name = cursor.getString(cursor.getColumnIndex("name"))
                    val email = cursor.getString(cursor.getColumnIndex("email"))
                    // 职责1 + 职责4(线程切换): 切回主线程更新 UI
                    runOnUiThread {
                        tvUserName.text = name
                        tvEmail.text = email
                        progressBar.visibility = View.GONE
                    }
                }
                cursor.close()
            }
 
            override fun onResponse(call: Call, response: Response) {
                // 职责2: 手动解析 JSON(职责4: 数据转换)
                val json = JSONObject(response.body?.string() ?: "")
                val name = json.getString("name")
                val email = json.getString("email")
                val avatarUrl = json.getString("avatar_url")
 
                // 职责3: 写入本地缓存
                database.execSQL(
                    "INSERT OR REPLACE INTO user_profile(name, email, avatar) VALUES(?, ?, ?)",
                    arrayOf(name, email, avatarUrl)
                )
 
                // 职责1: 主线程更新 UI
                runOnUiThread {
                    tvUserName.text = name
                    tvEmail.text = email
                    progressBar.visibility = View.GONE
                    // 职责1: 图片加载(又混入了图片加载库的调用)
                    Glide.with(this@UserProfileActivity)
                        .load(avatarUrl)
                        .into(ivAvatar)
                }
            }
        })
    }
 
    // 职责5: 数据校验逻辑也放在 Activity 里
    private fun isValidEmail(email: String): Boolean {
        return Patterns.EMAIL_ADDRESS.matcher(email).matches()
    }
}

上面这段代码有什么问题?我们用一张图来直观展示职责的混乱:

正面案例:职责分离的 MVVM 架构

现在我们用 SRP 来重构。核心思路是:识别出每个不同的"变化原因",将其提取到独立的类中。在 Android 现代架构中,这自然地对应到了 MVVM + Repository 模式:

Kotlin
// ✅ 正面案例 — 第1层:数据模型(职责:定义数据结构)
// 变化原因:后端 API 返回字段变化时才需修改
data class UserProfile(
    val name: String,       // 用户名
    val email: String,      // 邮箱地址
    val avatarUrl: String   // 头像链接
)
Kotlin
// ✅ 正面案例 — 第2层:网络数据源(职责:纯粹负责网络请求)
// 变化原因:API 接口地址/协议变化时才需修改
class UserRemoteDataSource(
    private val apiService: UserApiService  // Retrofit 接口,由外部注入
) {
    // 仅负责从远端获取用户数据,不关心缓存、不关心 UI
    suspend fun fetchUserProfile(): UserProfile {
        // Retrofit 自动处理 JSON -> UserProfile 的转换
        return apiService.getUserProfile()
    }
}
Kotlin
// ✅ 正面案例 — 第3层:本地数据源(职责:纯粹负责本地持久化)
// 变化原因:数据库 schema 或缓存策略变化时才需修改
class UserLocalDataSource(
    private val userDao: UserDao    // Room DAO,由外部注入
) {
    // 仅负责从本地数据库读取用户数据
    suspend fun getCachedProfile(): UserProfile? {
        return userDao.getUser()?.toUserProfile()   // Entity -> Domain Model
    }
 
    // 仅负责将用户数据写入本地数据库
    suspend fun cacheProfile(profile: UserProfile) {
        userDao.insertUser(profile.toUserEntity())   // Domain Model -> Entity
    }
}
Kotlin
// ✅ 正面案例 — 第4层:Repository(职责:协调远程与本地数据源的策略)
// 变化原因:数据获取策略(先缓存还是先网络)变化时才需修改
class UserRepository(
    private val remoteDataSource: UserRemoteDataSource,  // 远程数据源
    private val localDataSource: UserLocalDataSource     // 本地数据源
) {
    // 策略:优先网络 -> 成功后缓存 -> 网络失败时回退缓存
    suspend fun getUserProfile(): Result<UserProfile> {
        return try {
            // 尝试从网络获取最新数据
            val profile = remoteDataSource.fetchUserProfile()
            // 获取成功后写入本地缓存(不阻塞返回)
            localDataSource.cacheProfile(profile)
            // 返回成功结果
            Result.success(profile)
        } catch (e: Exception) {
            // 网络失败,尝试从本地缓存读取
            val cached = localDataSource.getCachedProfile()
            if (cached != null) {
                Result.success(cached)  // 缓存命中
            } else {
                Result.failure(e)       // 缓存也没有,返回失败
            }
        }
    }
}
Kotlin
// ✅ 正面案例 — 第5层:ViewModel(职责:管理 UI 状态 + 连接业务逻辑)
// 变化原因:UI 需要展示的状态字段变化时才需修改
class UserProfileViewModel(
    private val repository: UserRepository  // Repository 由外部注入
) : ViewModel() {
 
    // 用 sealed class 明确定义 UI 的所有可能状态
    sealed class UiState {
        object Loading : UiState()                          // 加载中
        data class Success(val profile: UserProfile) : UiState()  // 加载成功
        data class Error(val message: String) : UiState()   // 加载失败
    }
 
    // 使用 StateFlow 暴露 UI 状态(替代 LiveData,更适合 Kotlin 协程)
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
 
    // 加载用户资料 —— ViewModel 不知道数据来自网络还是缓存
    fun loadProfile() {
        viewModelScope.launch {                             // 在 ViewModel 作用域启动协程
            _uiState.value = UiState.Loading                // 发射加载中状态
            val result = repository.getUserProfile()        // 调用 Repository
            _uiState.value = result.fold(                   // 根据结果映射为 UI 状态
                onSuccess = { UiState.Success(it) },        // 成功
                onFailure = { UiState.Error(it.message ?: "Unknown error") }  // 失败
            )
        }
    }
}
Kotlin
// ✅ 正面案例 — 第6层:Activity(职责:纯粹负责 UI 渲染 + 用户交互)
// 变化原因:UI布局/交互方式变化时才需修改
class UserProfileActivity : AppCompatActivity() {
 
    // ViewModel 通过 Activity 扩展函数懒加载
    private val viewModel: UserProfileViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_profile)
 
        // 观察 UI 状态变化并渲染 —— Activity 只做"展示"
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->        // 收集 StateFlow
                    when (state) {
                        is UiState.Loading -> showLoading()  // 显示加载动画
                        is UiState.Success -> showProfile(state.profile)  // 渲染数据
                        is UiState.Error -> showError(state.message)      // 显示错误
                    }
                }
            }
        }
 
        // 触发加载
        viewModel.loadProfile()
    }
 
    // 以下都是纯 UI 操作方法,不包含任何业务逻辑
    private fun showLoading() {
        findViewById<ProgressBar>(R.id.progress_bar).visibility = View.VISIBLE
    }
 
    private fun showProfile(profile: UserProfile) {
        findViewById<ProgressBar>(R.id.progress_bar).visibility = View.GONE
        findViewById<TextView>(R.id.tv_user_name).text = profile.name
        findViewById<TextView>(R.id.tv_email).text = profile.email
        Glide.with(this).load(profile.avatarUrl)
            .into(findViewById(R.id.iv_avatar))
    }
 
    private fun showError(message: String) {
        findViewById<ProgressBar>(R.id.progress_bar).visibility = View.GONE
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
}

重构后的类图关系如下:

Android Framework 中的 SRP 体现

在 AOSP 源码中,SRP 的应用非常典型。以 View 体系为例:

  • View 只负责绘制(onDraw)、测量(onMeasure)、布局(onLayout)这些视图自身的核心职责。
  • ViewGroup 在 View 基础上增加"子 View 管理"的职责,但它不负责具体的布局算法。
  • LinearLayout、RelativeLayout、ConstraintLayout 各自只负责自己特定的布局策略。
  • TouchDelegate 专门负责扩展触摸区域,从 View 的事件处理中分离出来。
  • ViewTreeObserver 专门负责视图树的全局事件监听(如 OnGlobalLayoutListener)。

再看 RecyclerView 的设计更是 SRP 的教科书:

类名单一职责
RecyclerView滚动容器 + 协调者
LayoutManager子 View 的测量与布局策略
Adapter数据到 ViewHolder 的绑定
ViewHolder单个 item 的视图缓存
ItemDecorationitem 的装饰(分割线、间距等)
ItemAnimatoritem 增删改的动画
RecycledViewPoolViewHolder 的回收与复用管理

每个类都有且只有一个"变化的原因"。当你想换布局方式(从列表变网格),只需替换 LayoutManager;想换动画效果,只需替换 ItemAnimator——其他组件完全不受影响。


O — 开闭原则 (Open/Closed Principle) ⭐⭐

核心定义

开闭原则(OCP)是 SOLID 中最重要的一条原则,由 Bertrand Meyer 于 1988 年首次提出:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. (软件实体应该对扩展开放,对修改关闭。)

翻译成实践语言就是:当需求变化时,你应该通过"新增代码"来满足需求,而不是"修改已有代码"。 已有的、经过测试的、稳定运行的代码不应该被轻易改动——每次改动都有引入 bug 的风险。

这听起来似乎不可能——需求变了,怎么可能不改代码?关键在于提前设计好抽象层。通过抽象(接口、抽象类)定义稳定的契约,具体实现则通过子类或策略对象来"扩展"。当需求变化时,你新增一个实现类,而不需要修改抽象层或调用方的代码。

对扩展开放

"对扩展开放"意味着系统的行为可以被扩展——你可以增加新的功能、新的类型、新的策略。在 Kotlin/Java 中,主要通过以下机制实现:

  • 继承(inheritance):新增子类覆写父类方法。
  • 接口实现(interface implementation):新增接口的实现类。
  • 高阶函数 / Lambda(Kotlin 特有):通过函数参数注入新行为。
  • 装饰器 / 策略模式:通过组合注入新的行为对象。

对修改关闭

"对修改关闭"意味着已有的源代码不应该被修改。核心是:抽象层一旦确定就保持稳定。调用方依赖抽象,不依赖具体实现,因此新增实现不需要改调用方。

反面案例:if-else 硬编码

Kotlin
// ❌ 反面案例:每次新增一种消息类型,都必须修改这个函数
// 这违反了 OCP —— 对修改"敞开大门"
class NotificationHandler {
 
    // 处理推送通知 —— 用 if-else / when 硬编码所有类型
    fun handleNotification(type: String, data: Bundle) {
        when (type) {
            "chat" -> {
                // 聊天消息的处理逻辑:解析发送者、消息内容...
                val sender = data.getString("sender")
                val message = data.getString("message")
                showChatNotification(sender, message)
            }
            "order" -> {
                // 订单消息的处理逻辑:解析订单号、状态...
                val orderId = data.getString("order_id")
                val status = data.getString("status")
                showOrderNotification(orderId, status)
            }
            "promotion" -> {
                // 促销消息的处理逻辑
                val title = data.getString("title")
                showPromotionNotification(title)
            }
            // ❌ 每新增一种通知类型(如 "system", "payment", "logistics"...)
            //    都必须在这里添加新的分支,修改已有代码
            //    如果这个类已经上线并稳定运行,每次修改都有引入 bug 的风险
        }
    }
 
    private fun showChatNotification(sender: String?, message: String?) { /* ... */ }
    private fun showOrderNotification(orderId: String?, status: String?) { /* ... */ }
    private fun showPromotionNotification(title: String?) { /* ... */ }
}

正面案例:策略模式 + 注册机制

Kotlin
// ✅ 正面案例 — Step 1: 定义通知处理器的抽象接口
// 这是"稳定的抽象层" —— 一旦定义好就不需要修改
interface NotificationProcessor {
    // 该处理器支持的通知类型
    val supportedType: String
 
    // 处理通知的具体逻辑
    fun process(data: Bundle)
}
Kotlin
// ✅ 正面案例 — Step 2: 各类型通知处理器分别实现(对扩展开放)
// 聊天通知处理器
class ChatNotificationProcessor : NotificationProcessor {
    override val supportedType: String = "chat"    // 声明自己处理 "chat" 类型
 
    override fun process(data: Bundle) {
        val sender = data.getString("sender")       // 解析发送者
        val message = data.getString("message")     // 解析消息内容
        // 构建并显示聊天通知...
        NotificationCompat.Builder(/* ... */)
            .setContentTitle(sender)
            .setContentText(message)
            .build()
    }
}
 
// 订单通知处理器
class OrderNotificationProcessor : NotificationProcessor {
    override val supportedType: String = "order"   // 声明自己处理 "order" 类型
 
    override fun process(data: Bundle) {
        val orderId = data.getString("order_id")    // 解析订单号
        val status = data.getString("status")       // 解析订单状态
        // 构建并显示订单通知...
    }
}
 
// ✅ 未来新增 "logistics" 类型时,只需新增一个类:
class LogisticsNotificationProcessor : NotificationProcessor {
    override val supportedType: String = "logistics"
 
    override fun process(data: Bundle) {
        // 物流通知的处理逻辑...
    }
}
Kotlin
// ✅ 正面案例 — Step 3: 通知分发器(对修改关闭)
// 这个类一旦写好,不管新增多少种通知类型,都不需要修改它
class NotificationDispatcher {
 
    // 注册表:type -> processor 的映射
    private val processorMap = mutableMapOf<String, NotificationProcessor>()
 
    // 注册处理器 —— 通过"注册"而非"硬编码"来添加新类型
    fun register(processor: NotificationProcessor) {
        processorMap[processor.supportedType] = processor
    }
 
    // 分发通知 —— 永远不需要修改这个方法
    fun dispatch(type: String, data: Bundle) {
        val processor = processorMap[type]           // 从注册表查找
            ?: throw IllegalArgumentException("Unknown notification type: $type")
        processor.process(data)                       // 委托给具体处理器
    }
}
Kotlin
// ✅ 正面案例 — Step 4: 组装(通常在 Application 或 DI 模块中)
class MyApplication : Application() {
    lateinit var notificationDispatcher: NotificationDispatcher
 
    override fun onCreate() {
        super.onCreate()
        // 创建分发器
        notificationDispatcher = NotificationDispatcher()
 
        // 注册所有处理器 —— 新增类型只需在这里多加一行注册
        notificationDispatcher.register(ChatNotificationProcessor())
        notificationDispatcher.register(OrderNotificationProcessor())
        notificationDispatcher.register(LogisticsNotificationProcessor())
        // 未来新增: notificationDispatcher.register(PaymentNotificationProcessor())
    }
}

整体流程与扩展机制可以用以下时序图表示:

Android Framework 中的 OCP 体现

Android Framework 中 OCP 的经典实践随处可见:

  • View.OnClickListener:View 类定义了 setOnClickListener(listener) 接口,对"点击后做什么"完全开放扩展——你可以传入任何 OnClickListener 实现。View 内部的点击分发代码(performClick())则是"对修改关闭"的,不管你传入什么 listener,它都不需要改。

  • RecyclerView.Adapter:RecyclerView 定义了 Adapter 抽象,具体的数据绑定逻辑通过子类扩展。RecyclerView 内部的回收、复用、滚动机制不需要因为你换了 Adapter 而修改。

  • BroadcastReceiver 机制:Android 的广播系统天然符合 OCP。系统发送广播(Closed),任何 App 可以注册 Receiver 来响应广播(Open for extension)。

  • ContentProvider:Framework 定义了统一的 CRUD 抽象接口,任何 App 可以实现自己的 ContentProvider 来暴露数据,而访问方(ContentResolver)不需要修改。


L — 里氏替换原则 (Liskov Substitution Principle)

核心定义:子类可替换父类

里氏替换原则(LSP)由 Barbara Liskov 于 1987 年提出,是对"继承"这一 OOP 核心机制的行为约束:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program. (如果 S 是 T 的子类型,那么程序中使用 T 的地方都可以用 S 替换,而不改变程序的正确性。)

简单说:凡是父类能出现的地方,子类都能无缝替换,且程序行为不会出错。 这不仅仅是语法层面的"编译通过",而是语义层面的行为一致。

不改变程序正确性

LSP 违反的典型标志包括:

  1. 子类覆写父类方法后抛出了父类不会抛出的异常。
  2. 子类覆写父类方法后改变了方法的语义契约(如前置条件更严格,后置条件更宽松)。
  3. 子类的行为与父类的"合理预期"不一致——最经典的就是"正方形继承矩形"问题。

经典反面案例:正方形 vs 矩形

Kotlin
// ❌ 反面案例:经典的 LSP 违反 —— 正方形继承矩形
// 从数学角度看,正方形 IS-A 矩形,但从行为角度看,这个继承关系是有问题的
 
open class Rectangle(                   // 矩形:宽和高可以独立变化
    open var width: Int,                // 宽度
    open var height: Int                // 高度
) {
    // 计算面积
    open fun area(): Int = width * height
}
 
class Square(side: Int) : Rectangle(side, side) {  // 正方形继承矩形
    // 正方形的约束:宽 == 高,所以设置宽度时必须同步高度
    override var width: Int
        get() = super.width
        set(value) {
            super.width = value         // 设置宽度
            super.height = value        // ❌ 同步修改高度(改变了父类的行为契约!)
        }
 
    // 同理,设置高度时也必须同步宽度
    override var height: Int
        get() = super.height
        set(value) {
            super.height = value        // 设置高度
            super.width = value         // ❌ 同步修改宽度
        }
}
Kotlin
// 调用方代码 —— 按照 Rectangle 的行为契约编写
fun testArea(rect: Rectangle) {
    rect.width = 5                      // 只修改宽度
    rect.height = 4                     // 只修改高度
    // 按照矩形的语义,面积应该是 5 * 4 = 20
    assert(rect.area() == 20)           // ✅ Rectangle → 20,通过
                                        // ❌ Square → 4 * 4 = 16,断言失败!
}
 
fun main() {
    testArea(Rectangle(0, 0))           // ✅ 正确
    testArea(Square(0))                 // ❌ 失败!LSP 被违反
}

问题在于:Square 在语法上是 Rectangle 的子类,但在行为上破坏了 Rectangle 的契约——"设置宽度不会影响高度"。当调用方按照 Rectangle 的预期来操作 Square 时,程序行为出错了。

正面案例:用接口抽象替代继承

Kotlin
// ✅ 正面案例:用接口定义"形状"的抽象行为
// 不使用继承,避免行为契约被子类破坏
 
interface Shape {
    fun area(): Int                     // 所有形状都能计算面积
}
 
// 矩形:独立的宽和高
class Rectangle(
    val width: Int,                     // 宽度(val,不可变,避免状态变异问题)
    val height: Int                     // 高度
) : Shape {
    override fun area(): Int = width * height
}
 
// 正方形:只有边长
class Square(
    val side: Int                       // 边长
) : Shape {
    override fun area(): Int = side * side
}
 
// 调用方依赖抽象接口,不做任何"宽高可独立修改"的假设
fun printArea(shape: Shape) {
    println("面积 = ${shape.area()}")    // ✅ Rectangle 和 Square 都能正确工作
}

Android 中的 LSP 实践

在 Android 开发中,LSP 最常见的应用场景是自定义 View。当你继承 View 或 ViewGroup 时,必须遵守父类的行为契约:

Kotlin
// ✅ 正确的自定义 View —— 遵守 View 的测量契约
class CircleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // ✅ 关键:必须调用 setMeasuredDimension(),这是 View 的行为契约
        // 如果不调用,父类会抛出 IllegalStateException
        val size = minOf(
            MeasureSpec.getSize(widthMeasureSpec),
            MeasureSpec.getSize(heightMeasureSpec)
        )
        setMeasuredDimension(size, size)  // ✅ 遵守契约:必须设置测量结果
    }
 
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)             // ✅ 调用父类方法,保持契约链
        // 绘制圆形...
        val radius = width / 2f
        canvas.drawCircle(radius, radius, radius, Paint().apply {
            color = Color.BLUE
            isAntiAlias = true
        })
    }
}

违反 LSP 的典型错误:

Kotlin
// ❌ 违反 LSP 的自定义 View
class BrokenView(context: Context) : View(context) {
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // ❌ 忘记调用 setMeasuredDimension() —— 违反了 View.onMeasure 的行为契约
        // 虽然编译通过,但运行时 ViewGroup 调用 child.getMeasuredWidth() 会得到错误结果
    }
 
    override fun setVisibility(visibility: Int) {
        // ❌ 覆写 setVisibility 但不调用 super —— 破坏了父类的可见性管理契约
        // 其他代码调用 view.visibility = View.GONE 时预期 View 会消失,但实际不会
        // 违反了"子类可替换父类"的原则
        if (visibility == GONE) return    // 忽略 GONE 请求
        super.setVisibility(visibility)
    }
}

在 Framework 层,Android 的 InputStream 体系也是 LSP 的典范——FileInputStream、ByteArrayInputStream、BufferedInputStream 都可以无缝替换使用 InputStream 的地方,行为完全符合预期。


I — 接口隔离原则 (Interface Segregation Principle)

核心定义:接口小而专

接口隔离原则(ISP)指出:

No client should be forced to depend on methods it does not use. (客户端不应该被迫依赖它不需要的方法。)

这条原则针对的是**"胖接口"(Fat Interface)** 问题——当一个接口包含了太多方法时,实现这个接口的类被迫实现所有方法,即使其中很多方法对它毫无意义。

不强迫依赖不需要的方法

ISP 的解决方案是:将一个大接口拆分为多个小的、专注的接口。每个实现类只需要实现自己真正需要的接口,而不是被迫实现一整个"万能接口"。

反面案例:胖接口

Kotlin
// ❌ 反面案例:一个"万能"的用户操作接口
// 强迫所有实现类都实现所有方法
interface UserAction {
    fun login(username: String, password: String)    // 登录
    fun logout()                                      // 登出
    fun updateProfile(name: String, avatar: String)   // 更新资料
    fun deleteAccount()                               // 注销账号
    fun exportData(): ByteArray                       // 导出数据
    fun sendMessage(to: String, content: String)      // 发送消息
    fun blockUser(userId: String)                     // 拉黑用户
}
 
// 一个"访客"模块只需要登录功能,但被迫实现所有方法
class GuestModule : UserAction {
    override fun login(username: String, password: String) {
        // ✅ 这是访客唯一需要的功能
    }
 
    // ❌ 以下所有方法对访客毫无意义,但必须实现
    override fun logout() { /* 空实现 */ }
    override fun updateProfile(name: String, avatar: String) {
        throw UnsupportedOperationException("访客不能修改资料")  // ❌ 被迫抛异常
    }
    override fun deleteAccount() {
        throw UnsupportedOperationException("访客不能注销")
    }
    override fun exportData(): ByteArray = byteArrayOf()
    override fun sendMessage(to: String, content: String) {
        throw UnsupportedOperationException("访客不能发消息")
    }
    override fun blockUser(userId: String) {
        throw UnsupportedOperationException("访客不能拉黑")
    }
}

这种代码有几个严重问题:

  • 空实现 / 异常抛出:大量 UnsupportedOperationException 是代码的"坏味道"。
  • 接口污染:调用方拿到 UserAction 引用后,不知道哪些方法能调、哪些会爆炸。
  • 违反 LSP:GuestModule 无法真正替换 UserAction 的使用场景。

正面案例:接口拆分

Kotlin
// ✅ 正面案例:将一个胖接口拆分为多个小接口
 
// 认证相关操作
interface Authenticatable {
    fun login(username: String, password: String)    // 登录
    fun logout()                                      // 登出
}
 
// 资料管理操作
interface ProfileManageable {
    fun updateProfile(name: String, avatar: String)   // 更新资料
    fun deleteAccount()                               // 注销账号
    fun exportData(): ByteArray                       // 导出数据
}
 
// 社交操作
interface Socializable {
    fun sendMessage(to: String, content: String)      // 发送消息
    fun blockUser(userId: String)                     // 拉黑用户
}
 
// 访客模块:只实现需要的认证接口
class GuestModule : Authenticatable {
    override fun login(username: String, password: String) {
        // ✅ 只实现自己需要的方法,干净利落
    }
    override fun logout() {
        // ✅ 清理访客会话
    }
}
 
// 正式用户模块:实现所有相关接口
class RegisteredUserModule : Authenticatable, ProfileManageable, Socializable {
    override fun login(username: String, password: String) { /* ... */ }
    override fun logout() { /* ... */ }
    override fun updateProfile(name: String, avatar: String) { /* ... */ }
    override fun deleteAccount() { /* ... */ }
    override fun exportData(): ByteArray = byteArrayOf(/* ... */)
    override fun sendMessage(to: String, content: String) { /* ... */ }
    override fun blockUser(userId: String) { /* ... */ }
}

Android 中的 ISP 实践

Android Framework 中 ISP 的实践非常典型:

1. View 的事件监听器体系

Android 没有设计一个 OnEverythingListener,而是将不同事件拆分为独立的小接口:

Kotlin
// Android SDK 中的接口设计 —— 每个接口只关注一种事件
view.setOnClickListener { /* 点击 */ }           // View.OnClickListener —— 1 个方法
view.setOnLongClickListener { /* 长按 */ }       // View.OnLongClickListener —— 1 个方法
view.setOnTouchListener { _, _ -> false }         // View.OnTouchListener —— 1 个方法
view.setOnDragListener { _, _ -> false }          // View.OnDragListener —— 1 个方法
view.setOnKeyListener { _, _, _ -> false }        // View.OnKeyListener —— 1 个方法

如果这些全合并成一个 OnViewEventListener,那只关心点击事件的类就被迫实现拖拽、按键等所有方法。

2. TextWatcher 的设计反思

有趣的是,TextWatcher 接口恰好是 ISP 的一个轻微违反案例:

Java
// Android SDK 中的 TextWatcher —— 包含 3 个方法
public interface TextWatcher extends NoCopySpan {
    void beforeTextChanged(CharSequence s, int start, int count, int after);
    void onTextChanged(CharSequence s, int start, int before, int count);
    void afterTextChanged(Editable s);
}

实际开发中,大多数场景只需要 afterTextChanged,但被迫实现另外两个方法。这就是为什么 Android KTX 提供了扩展函数 doAfterTextChanged 来简化使用——本质上就是用 Kotlin 的高阶函数弥补了原始接口设计的 ISP 不足。

3. Kotlin 的 SAM (Single Abstract Method) 与 ISP

Kotlin 语言层面天然鼓励 ISP——fun interface(SAM 接口)就是最小化接口的极致体现:

Kotlin
// Kotlin 中声明 SAM 接口
fun interface OnItemSelected {
    fun onSelected(position: Int)       // 只有一个抽象方法
}
 
// 使用时可以直接用 lambda 替代
adapter.setOnItemSelectedListener { position ->
    // 处理选中事件
}

D — 依赖倒置原则 (Dependency Inversion Principle) ⭐

核心定义:依赖抽象而非具体

依赖倒置原则(DIP)包含两层含义:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions. (高层模块不应该依赖低层模块,两者都应该依赖抽象。)
  2. Abstractions should not depend on details. Details should depend on abstractions. (抽象不应该依赖细节,细节应该依赖抽象。)

这里的"倒置"指的是依赖关系的方向反转。在传统的分层架构中,高层模块(如业务逻辑)直接依赖低层模块(如数据库访问)。DIP 要求在两者之间插入一个抽象层,使得依赖箭头"倒过来"——低层模块去实现高层定义的抽象接口,而不是高层直接依赖低层的具体实现。

高层不依赖低层

注意看上图中依赖箭头的方向变化。在"倒置后"的架构中:

  • ViewModel → 依赖 → Repository 接口(抽象)
  • RoomRepository → 实现 → Repository 接口(抽象)

低层模块的依赖箭头"向上"指向了抽象层——这就是所谓的"倒置"。

反面案例:直接依赖具体实现

Kotlin
// ❌ 反面案例:ViewModel 直接依赖具体的数据库实现类
class UserViewModel : ViewModel() {
 
    // 直接 new 出具体的数据库 Helper —— 高层直接依赖低层
    private val dbHelper = UserDatabaseHelper()     // ❌ 硬编码依赖
 
    fun getUser(id: String): LiveData<User> {
        val user = dbHelper.queryUserById(id)       // 直接调用具体方法
        return MutableLiveData(user)
    }
}
 
// 问题:
// 1. 如果要从 SQLite 换成 Room —— 必须修改 ViewModel
// 2. 如果要加网络缓存层 —— 必须修改 ViewModel
// 3. 单元测试时无法 mock 数据源 —— 因为是 new 出来的,无法替换
// 4. ViewModel(高层业务逻辑)和 SQLite(低层存储细节)紧密耦合

正面案例:依赖抽象 + 构造函数注入

Kotlin
// ✅ 正面案例 — Step 1: 定义抽象接口(高层与低层之间的契约)
// 这个接口由"高层"定义,表达的是高层需要什么能力
interface UserRepository {
    suspend fun getUserById(id: String): User?       // 获取用户
    suspend fun saveUser(user: User)                 // 保存用户
    suspend fun deleteUser(id: String)               // 删除用户
}
Kotlin
// ✅ 正面案例 — Step 2: 低层模块实现抽象接口
// Room 实现
class RoomUserRepository(
    private val userDao: UserDao                     // Room DAO
) : UserRepository {
    override suspend fun getUserById(id: String): User? {
        return userDao.findById(id)?.toDomainModel() // Entity -> Domain Model
    }
    override suspend fun saveUser(user: User) {
        userDao.insert(user.toEntity())              // Domain Model -> Entity
    }
    override suspend fun deleteUser(id: String) {
        userDao.deleteById(id)
    }
}
 
// 网络实现(同一个接口,不同的底层技术)
class RemoteUserRepository(
    private val apiService: UserApiService           // Retrofit 接口
) : UserRepository {
    override suspend fun getUserById(id: String): User? {
        return apiService.fetchUser(id)              // 从网络获取
    }
    override suspend fun saveUser(user: User) {
        apiService.updateUser(user)                  // 上传到服务器
    }
    override suspend fun deleteUser(id: String) {
        apiService.deleteUser(id)                    // 远程删除
    }
}
Kotlin
// ✅ 正面案例 — Step 3: 高层模块依赖抽象(通过构造函数注入)
class UserViewModel(
    private val repository: UserRepository           // ✅ 依赖抽象接口,不依赖具体实现
) : ViewModel() {
 
    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user.asStateFlow()
 
    fun loadUser(id: String) {
        viewModelScope.launch {
            _user.value = repository.getUserById(id) // 调用抽象方法
            // ViewModel 完全不知道数据来自 Room 还是网络
            // 未来切换数据源时,这里的代码零修改
        }
    }
}
Kotlin
// ✅ 正面案例 — Step 4: 在 DI 层(Hilt/Koin)进行装配
// 使用 Hilt 的示例
@Module
@InstallIn(SingletonComponent::class)               // 全局单例作用域
abstract class RepositoryModule {
 
    @Binds                                            // 告诉 Hilt:当需要 UserRepository 时
    @Singleton                                        // 单例
    abstract fun bindUserRepository(
        impl: RoomUserRepository                      // 注入 RoomUserRepository 实现
    ): UserRepository                                 // 返回抽象接口类型
}
 
// 切换实现只需修改 DI 配置,ViewModel 和所有调用方代码零改动:
// impl: RoomUserRepository → impl: RemoteUserRepository

Android Framework 中的 DIP 体现

在 Android Framework 中,DIP 最经典的体现是 Binder IPC 机制:

AIDL(Android Interface Definition Language)就是"抽象层"——Client 和 Service 都依赖这个抽象接口定义,而不直接依赖彼此。Client 进程通过 Proxy 调用接口方法,Service 进程通过 Stub 实现接口方法。两者完全解耦,可以在不同进程、甚至不同设备上运行。

此外,Android 的 Context 也是 DIP 的体现——Context 是一个抽象类,Activity、Service、Application 都是它的实现。当你调用 context.getSystemService() 时,你依赖的是 Context 抽象,而不关心它背后是 Activity 还是 Application。


📝 练习题

题目 1: 在 Android MVVM 架构中,以下哪种做法 违反了 依赖倒置原则(DIP)?

A. ViewModel 通过构造函数接收一个 Repository 接口类型的参数

B. ViewModel 内部直接 new RetrofitUserService() 来发起网络请求

C. 使用 Hilt 的 @Binds 将 RoomRepository 绑定到 Repository 接口

D. Repository 接口定义在 domain 层,具体实现定义在 data 层

【答案】 B

【解析】 DIP 的核心是"高层模块不应该依赖低层模块,两者都应该依赖抽象"。选项 B 中,ViewModel(高层业务逻辑)直接 new 了 RetrofitUserService(低层网络实现),这就是高层直接依赖低层的典型违反。这样做导致 ViewModel 与 Retrofit 紧耦合,无法单独对 ViewModel 进行单元测试(因为无法 mock 网络层),也无法在不修改 ViewModel 的情况下替换网络库。选项 A 使用构造函数注入抽象类型,选项 C 使用 DI 框架绑定实现到抽象,选项 D 将接口放在高层(domain)、实现放在低层(data),三者都是 DIP 的正确实践。


题目 2: 以下关于 SOLID 原则在 Android RecyclerView 设计中的应用,说法 错误 的是?

A. RecyclerView 将布局策略委托给 LayoutManager,体现了单一职责原则(SRP)

B. 新增一种布局方式只需实现新的 LayoutManager 子类,无需修改 RecyclerView 源码,体现了开闭原则(OCP)

C. ItemDecoration 和 ItemAnimator 是两个独立的接口,体现了接口隔离原则(ISP)

D. RecyclerView.Adapter 是一个抽象类而非接口,因此它违反了依赖倒置原则(DIP)

【答案】 D

【解析】 DIP 的关键是"依赖抽象而非具体",这里的"抽象"包括 接口和抽象类 两种形式。RecyclerView.Adapter 虽然是抽象类而非接口,但它仍然是一种"抽象"——RecyclerView 内部持有的是 Adapter 抽象类型的引用,不知道也不关心具体是哪个子类。因此 D 选项"违反了 DIP"的说法是错误的。选项 A、B、C 的分析都是正确的:LayoutManager 的分离体现了 SRP 和 OCP;ItemDecoration 与 ItemAnimator 的独立设计避免了将装饰逻辑和动画逻辑捆绑在一个接口中,体现了 ISP。


其他原则

在 SOLID 五大原则之外,面向对象设计领域还有两条极其重要的补充原则——迪米特法则(Law of Demeter, LoD) 和 合成复用原则(Composite Reuse Principle, CRP)。它们与 SOLID 并非对立关系,而是从不同维度对"高内聚、低耦合"这一终极目标的进一步诠释。如果说 SOLID 侧重于类与接口的"微观结构"设计,那么这两条原则更多着眼于对象之间的"宏观交互"与"复用策略"。在 Android 开发中,无论是应用层的 MVVM 架构,还是 Framework 层的 Binder、WindowManager 等系统服务,都大量体现了这两条原则的思想。


迪米特法则(最少知识原则)

核心定义

迪米特法则(Law of Demeter),又称 最少知识原则(Principle of Least Knowledge),由 Ian Holland 于 1987 年在美国东北大学(Northeastern University)的 Demeter 项目中首次提出。其核心表述非常简洁:

一个对象应该对其他对象保持最少的了解(Each unit should have only limited knowledge about other units)。

更具体地说,一个对象 O 的某个方法 M,只应该调用以下范围内的方法:

  1. O 自身的方法(this.xxx())
  2. M 的参数对象的方法
  3. M 内部创建的局部对象的方法
  4. O 的直接成员变量(组件对象) 的方法

而不应该调用通过上述调用链间接获得的"陌生对象"的方法。这就是著名的 "只与直接朋友通信(Only talk to your immediate friends)" 规则。所谓"朋友",就是上面列出的四类对象。

为什么"链式穿透"是危险的

考虑这样一个反例——在 Android 中,一个 Activity 想获取当前用户的城市名称:

Kotlin
// ❌ 违反迪米特法则:链式穿透访问
// Activity 直接"穿透"了 UserManager → User → Address → city
// Activity 不仅知道 UserManager,还知道 User 的内部结构,甚至 Address 的字段
val cityName = userManager.getCurrentUser().getAddress().getCity()

这行代码看似简洁,实际暗藏巨大隐患:

  • 耦合链条:Activity 对 UserManager、User、Address 三个类都产生了依赖。任何一个中间类的结构变动,都可能导致 Activity 编译失败。
  • 空安全灾难:Kotlin 中会变成 userManager.getCurrentUser()?.getAddress()?.getCity(),链条越长,null 检查越恐怖。
  • 违反封装:Activity 根本不需要知道"用户有一个地址对象、地址对象有一个城市字段"这些内部细节。

我们用一张类图来直观对比"违反"与"遵守"迪米特法则的两种设计:

在右侧"遵守"的方案中,Activity 只与它的直接朋友 UserManager 交互,而 UserManager 封装了"获取城市"的细节,对外仅暴露一个 getUserCity() 方法。这就是迪米特法则的精髓。

Android 中的正确实践

下面给出完整的 Kotlin 重构示例:

Kotlin
// === 数据层:Address 类 ===
data class Address(
    val city: String,       // 城市名
    val district: String    // 区县名
)
 
// === 数据层:User 类 ===
// User 封装了从自身获取城市的能力,外部无需知道 Address 的存在
data class User(
    val name: String,       // 用户名
    val address: Address    // 地址(内部细节)
) {
    // ✅ User 自己提供 "获取城市" 的方法
    // 外部无需知道 address 字段的结构
    fun getCity(): String = address.city
}
 
// === 业务层:UserManager ===
// UserManager 对外只暴露"最终结果",隐藏 User 的获取细节
class UserManager(
    private val userRepository: UserRepository  // 数据仓库(直接朋友)
) {
    // ✅ 封装了获取当前用户城市的完整链路
    // Activity 只需调用这一个方法即可
    fun getCurrentUserCity(): String {
        val user = userRepository.getCurrentUser()  // 获取用户对象
        return user.getCity()                        // 委托 User 获取城市
    }
}
 
// === 表现层:Activity ===
class ProfileActivity : AppCompatActivity() {
 
    // UserManager 是 Activity 的"直接朋友"
    private lateinit var userManager: UserManager
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        // ✅ 只与直接朋友通信,一步到位
        // Activity 完全不知道 User、Address 的存在
        val city = userManager.getCurrentUserCity()
        binding.tvCity.text = city  // 直接展示
    }
}

关键点:每一层只与自己的"直接朋友"对话。Activity → UserManager → User → Address,每一步只跨一层,绝不越级。

迪米特法则在 Android Framework 中的体现

Android Framework 本身就是迪米特法则的忠实践行者。以 Context 体系为例:

当你的应用需要获取系统服务(如 ConnectivityManager、WindowManager)时,你从来不需要知道这些服务的内部 Binder 代理是如何创建的。你只需要:

Kotlin
// ✅ 通过 Context 这个"直接朋友"获取系统服务
// 你不需要了解 ServiceManager、IBinder、AIDL Proxy 等底层细节
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
    as ConnectivityManager

在 Framework 内部,这个调用实际上经过了一条很长的链路:

但对于应用开发者来说,你的"世界"只有 Context 这一个朋友。SystemServiceRegistry、ServiceManager、BinderProxy——这些全部被封装在 Framework 内部,你无需感知,也不应感知。这正是迪米特法则在系统级架构中的经典应用:每一层只暴露最少的必要接口给上层调用者。

迪米特法则的适度把握

迪米特法则虽好,但过度应用也会带来问题。如果严格遵守"不与陌生人说话",可能会在中间层引入大量委托方法(Wrapper / Delegate Methods),导致:

  • 中间类膨胀:UserManager 为了满足上层各种需求,不得不为 User 的每一个属性都写一个代理方法。
  • 过度封装:简单的数据传递被包了三四层,阅读成本反而上升。

因此,在实践中需要权衡:

场景建议
跨模块调用(Activity → Repository)严格遵守,通过 ViewModel/UseCase 封装
同模块内部(ViewModel → Entity)适度放松,允许直接访问 data class 属性
Kotlin data class 的属性链user.address.city 在 Kotlin 中因空安全 ?. 机制可接受
Framework 层对外 API严格遵守,系统服务对 App 层只暴露接口

合成复用原则(组合优于继承)

核心定义

合成复用原则(Composite Reuse Principle, CRP),也被称为 "组合/聚合复用原则" 或更广为人知的 "Composition over Inheritance",其核心表述为:

优先使用对象组合(has-a)来实现代码复用,而非通过类继承(is-a)。

这并不是说"永远不要用继承",而是说:当你想复用某个类的功能时,第一反应应该是"我能不能把它组合进来",而不是"我能不能继承它"。 继承应该只在真正的 "is-a" 语义关系下使用。

继承复用的致命问题

让我们用一个 Android 场景来说明继承的陷阱。假设我们有一个带日志功能的网络请求类:

Kotlin
// === 基础网络请求类 ===
open class BaseHttpClient {
    // 发送 GET 请求
    open fun get(url: String): String {
        println("HTTP GET: $url")  // 日志输出
        return "response"           // 模拟返回
    }
 
    // 发送 POST 请求
    open fun post(url: String, body: String): String {
        println("HTTP POST: $url") // 日志输出
        return "response"           // 模拟返回
    }
}
 
// ❌ 通过继承来添加"缓存"功能
// CachedHttpClient "is-a" BaseHttpClient?语义上不太对
// 缓存和 HTTP 请求是两个独立的关注点
class CachedHttpClient : BaseHttpClient() {
 
    private val cache = mutableMapOf<String, String>()  // 简单内存缓存
 
    // 覆写 get,加入缓存逻辑
    override fun get(url: String): String {
        // 先查缓存
        cache[url]?.let { return it }
        // 缓存未命中,调用父类实际请求
        val response = super.get(url)
        cache[url] = response  // 写入缓存
        return response
    }
}
 
// ❌ 再通过继承添加"认证"功能
// AuthCachedHttpClient 同时需要缓存和认证
// 必须继承 CachedHttpClient,形成脆弱的层级链
class AuthCachedHttpClient : CachedHttpClient() {
 
    private var token: String = ""  // 认证令牌
 
    // 覆写 get,加入认证 Header
    override fun get(url: String): String {
        println("Adding auth token: $token")  // 注入认证信息
        return super.get(url)                  // 委托给带缓存的父类
    }
}

这种设计的问题是灾难性的:

左侧继承方案的根本问题:

  • 组合爆炸:N 个功能会产生 2^N 个子类(缓存、认证、日志三个功能的排列组合就可能需要 7 个子类)。
  • 脆弱基类问题(Fragile Base Class Problem):父类的任何修改都可能破坏所有子类的行为,这在 Java/Kotlin 中尤为突出,因为方法调用是动态分派的。
  • 违反单一职责:AuthCachedHttpClient 同时承担了认证、缓存、HTTP 请求三种职责。
  • 无法在运行时动态切换:继承关系在编译时就固定了,运行时无法灵活地"关闭缓存但保留认证"。

用组合重构:拦截器模式

这正是 OkHttp 的核心设计思想——用拦截器(Interceptor)链实现组合:

Kotlin
// === 拦截器接口:定义统一的"处理切面" ===
// 每个拦截器只负责一个关注点
interface Interceptor {
    // 拦截处理,chain 允许将请求传递给下一个拦截器
    fun intercept(chain: Chain): Response
 
    // 链接口,负责串联多个拦截器
    interface Chain {
        fun request(): Request           // 获取当前请求
        fun proceed(request: Request): Response  // 传递给下一个拦截器
    }
}
 
// === 缓存拦截器:只负责缓存 ===
class CacheInterceptor(
    private val cache: Cache  // 通过构造器注入缓存实现(又是组合!)
) : Interceptor {
 
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()                   // 获取请求
        val cachedResponse = cache.get(request)         // 查询缓存
 
        if (cachedResponse != null) {
            return cachedResponse                       // 命中缓存,直接返回
        }
 
        val response = chain.proceed(request)           // 未命中,传递给下一个拦截器
        cache.put(request, response)                    // 将结果写入缓存
        return response                                 // 返回响应
    }
}
 
// === 认证拦截器:只负责认证 ===
class AuthInterceptor(
    private val tokenProvider: () -> String  // 令牌提供者(函数式组合)
) : Interceptor {
 
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()           // 获取原始请求
 
        // 在请求头中添加认证令牌
        val authenticatedRequest = originalRequest.newBuilder()
            .addHeader("Authorization", "Bearer ${tokenProvider()}")  // 注入 token
            .build()
 
        return chain.proceed(authenticatedRequest)      // 传递修改后的请求
    }
}
 
// === 日志拦截器:只负责日志 ===
class LogInterceptor : Interceptor {
 
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        println("➡️ Sending: ${request.url}")           // 打印请求日志
 
        val startTime = System.nanoTime()               // 记录开始时间
        val response = chain.proceed(request)           // 传递给下一个拦截器
        val duration = (System.nanoTime() - startTime) / 1e6  // 计算耗时
 
        println("⬅️ Received: ${response.code} in ${duration}ms")  // 打印响应日志
        return response
    }
}

客户端可以自由组合任意拦截器,运行时动态增减功能:

Kotlin
// === 使用组合构建 HttpClient ===
val client = OkHttpClient.Builder()
    .addInterceptor(LogInterceptor())                       // ✅ 添加日志
    .addInterceptor(AuthInterceptor { "my-secret-token" })  // ✅ 添加认证
    .addInterceptor(CacheInterceptor(diskCache))            // ✅ 添加缓存
    .build()
 
// 想要一个不带缓存的客户端?直接去掉那一行即可!
val noCacheClient = OkHttpClient.Builder()
    .addInterceptor(LogInterceptor())                       // ✅ 只要日志
    .addInterceptor(AuthInterceptor { "my-secret-token" })  // ✅ 和认证
    .build()

这就是组合的威力:每个功能独立为一个小模块,可以像乐高积木一样任意拼装,无需创建新的子类。

合成复用原则在 Android Framework 中的经典体现

Android Framework 中最典型的"组合优于继承"案例莫过于 RecyclerView 的架构设计:

RecyclerView 本身不负责布局、不负责动画、不负责分割线。它通过持有(has-a)各种组件对象来获得这些能力:

组件职责替换方式
LayoutManager决定子 View 如何排列setLayoutManager()
Adapter提供数据和创建 ViewHoldersetAdapter()
ItemDecoration绘制分割线、间距等装饰addItemDecoration()
ItemAnimator处理增删改动画setItemAnimator()

试想如果用继承来实现——LinearRecyclerView、GridRecyclerView、StaggeredRecyclerView、LinearAnimatedRecyclerView、GridAnimatedRecyclerView……这完全是组合爆炸的灾难。

再看一个 Framework Java 层的经典案例——Window 与 WindowManager:

Java
// === Android Framework: Window.java(简化)===
// Window 类通过组合持有 Callback,而非要求 Activity 继承 Window
public abstract class Window {
 
    // ✅ 组合:Window 持有一个 Callback 接口引用
    // Activity 实现此接口,Window 通过组合调用 Activity 的回调
    private Callback mCallback;  // "has-a" 关系
 
    // Callback 接口定义了 Window 与上层的通信协议
    public interface Callback {
        boolean dispatchKeyEvent(KeyEvent event);     // 按键事件分发
        boolean dispatchTouchEvent(MotionEvent event); // 触摸事件分发
        void onWindowFocusChanged(boolean hasFocus);   // 焦点变化通知
    }
 
    // 设置回调——运行时可替换!
    public void setCallback(Callback callback) {
        mCallback = callback;  // 动态绑定
    }
}
 
// Activity 实现 Window.Callback,而非继承 Window
// 这正是"组合优于继承"的体现
// public class Activity extends ContextThemeWrapper
//     implements Window.Callback, ... { }

Activity 并没有继承 Window,而是实现了 Window.Callback 接口,与 Window 形成组合关系。这使得 Window 可以独立演化,Activity 也不会被 Window 的实现细节所绑架。

何时该用继承

虽然原则叫"组合优于继承",但继承在以下场景中仍然是正确的选择:

Kotlin
// ✅ 正确使用继承:真正的 "is-a" 关系
// AppCompatActivity 确实"是一个" Activity
class MainActivity : AppCompatActivity() { /* ... */ }
 
// ✅ 正确使用继承:框架要求的模板方法模式
// 自定义 View 确实"是一个" View
class CircleImageView : ImageView(context, attrs) {
    override fun onDraw(canvas: Canvas) {  // 模板方法
        // 自定义绘制逻辑
    }
}
 
// ✅ 正确使用继承:密封类层级(Kotlin Sealed Class)
// 代表有限的、封闭的类型集合
sealed class UiState {               // 密封类定义状态集
    object Loading : UiState()        // 加载中——"是一种" UiState
    data class Success(val data: Any) : UiState()  // 成功
    data class Error(val msg: String) : UiState()  // 失败
}

组合 vs 继承 决策树

核心判断逻辑:只有同时满足 "是 is-a 关系" + "不需运行时替换" + "父类稳定" 这三个条件时,才考虑继承。否则,默认选择组合。

Kotlin 中组合的语法优势

Kotlin 语言本身就提供了丰富的语法糖来鼓励组合:

Kotlin
// === 1. 委托模式:by 关键字 ===
// Kotlin 的 "by" 可以零成本实现组合式委托
// 不需要手动编写所有委托方法
interface Logger {
    fun log(message: String)  // 日志接口
}
 
// 具体日志实现
class ConsoleLogger : Logger {
    override fun log(message: String) {
        println("[LOG] $message")  // 控制台输出
    }
}
 
// ✅ UserService 通过 "by" 委托给 consoleLogger
// 编译器自动生成所有接口方法的委托代码
// 这既是组合(has-a ConsoleLogger),又拥有继承的便捷语法
class UserService(
    consoleLogger: ConsoleLogger  // 通过构造器注入
) : Logger by consoleLogger {
    // UserService 自动拥有 log() 方法
    // 但无需手写 override fun log(msg) = consoleLogger.log(msg)
 
    fun createUser(name: String) {
        log("Creating user: $name")  // 直接调用委托方法
    }
}
 
// === 2. 扩展函数:不修改原类即可"添加"功能 ===
// 这也是组合思想的体现——不通过继承来扩展类
fun ImageView.loadUrl(url: String) {
    Glide.with(this.context)    // 获取 Glide 实例
        .load(url)              // 加载图片 URL
        .into(this)             // 设置到当前 ImageView
}
// 使用:imageView.loadUrl("https://example.com/photo.jpg")
 
// === 3. 高阶函数:行为参数化 ===
// 将"行为"作为参数传入,而非通过继承覆写
inline fun <T> measureTimeAndExecute(
    tag: String,           // 标签名
    block: () -> T         // 要执行的代码块(行为参数化)
): T {
    val start = System.currentTimeMillis()  // 记录开始时间
    val result = block()                     // 执行代码块
    val elapsed = System.currentTimeMillis() - start  // 计算耗时
    Log.d(tag, "Executed in ${elapsed}ms")   // 输出日志
    return result                             // 返回结果
}
 
// 使用
val users = measureTimeAndExecute("DB") {
    database.userDao().getAllUsers()  // 实际的数据库操作
}

Kotlin 的 by 委托机制特别值得关注——它在语言层面解决了"组合时需要手写大量委托方法"的痛点。编译器自动为你生成所有接口方法的转发代码,使得组合在使用体验上几乎等同于继承,却保留了组合的全部灵活性。


本章小结

原则核心思想一句话口诀Android 典型案例
S 单一职责一个类只做一件事"一个类,一个变化原因"Activity / ViewModel / Repository 分层
O 开闭原则对扩展开放,对修改关闭"加功能不改老代码"RecyclerView.Adapter、OkHttp Interceptor
L 里氏替换子类可无缝替换父类"父类能用的地方,子类也能用"AppCompatActivity 替换 Activity
I 接口隔离接口小而专"不强迫实现用不到的方法"View.OnClickListener 单方法接口
D 依赖倒置依赖抽象不依赖具体"面向接口编程"Retrofit 接口定义、Room DAO
LoD 迪米特法则只与直接朋友通信"不和陌生人说话"Context.getSystemService() 封装
CRP 合成复用组合优于继承"has-a 优先于 is-a"RecyclerView 组件化、OkHttp 拦截器链

这七条原则并非孤立存在,它们共同指向一个目标:

在实际 Android 开发中,不需要也不可能每行代码都完美遵循所有原则。重要的是理解每条原则背后的"为什么",在面对设计决策时能够有意识地权衡取舍。过度设计(Over-engineering)和设计不足(Under-engineering)都是问题——原则是指南针,不是枷锁。


📝 练习题 1

以下代码违反了迪米特法则,请问最佳重构方案是什么?

Kotlin
class OrderActivity : AppCompatActivity() {
    fun showPrice() {
        val price = orderManager.getOrder().getItem().getPrice()
        binding.tvPrice.text = price.toString()
    }
}

A. 将 getOrder()、getItem()、getPrice() 合并为 OrderManager 的一个内部方法

B. 让 OrderActivity 直接持有 Item 对象的引用

C. 在 OrderManager 中提供 getOrderItemPrice() 方法,Activity 只调用这一个方法

D. 把 Order、Item、Price 合并成一个类以减少调用链

【答案】 C

【解析】 迪米特法则要求"只与直接朋友通信"。OrderActivity 的直接朋友是 OrderManager,但代码中 Activity 还间接触及了 Order、Item 这些"陌生对象"。选项 C 是标准的重构手法:在 OrderManager 中封装一个 getOrderItemPrice() 方法,内部处理链式调用,对外只暴露最终结果。Activity 只需调用 orderManager.getOrderItemPrice(),完全不感知 Order 和 Item 的存在。选项 A 表述含糊,没有说明封装到哪个层级;选项 B 是反向违反——让 Activity 直接持有更深层的对象反而增加了耦合;选项 D 是过度简化,破坏了领域模型的合理结构。


📝 练习题 2

关于"合成复用原则",以下哪个说法是错误的?

A. Kotlin 的 by 委托关键字是语言层面对合成复用原则的支持

B. RecyclerView 通过组合 LayoutManager、Adapter 等组件来实现功能,是合成复用的典范

C. 合成复用原则意味着在任何情况下都不应使用继承

D. OkHttp 的 Interceptor 链式设计体现了"组合优于继承"的思想

【答案】 C

【解析】 合成复用原则的核心是"优先使用组合",而非"禁止使用继承"。在真正的 is-a 语义关系下(如 AppCompatActivity 继承 Activity、自定义 View 继承 View),继承仍然是正确的选择。判断标准是:是否满足 is-a 语义 + 是否不需要运行时动态替换 + 父类是否稳定。选项 A 正确,by 是 Kotlin 提供的类委托语法糖;选项 B 正确,RecyclerView 的组件化架构是合成复用的教科书案例;选项 D 正确,OkHttp 将日志、缓存、认证等功能拆分为独立拦截器,通过组合灵活拼装。


本章小结

经过对 SOLID 原则以及迪米特法则、合成复用原则的逐一深入剖析,我们已经建立起了面向对象设计中最核心的"指导思想体系"。本章小结将从 全局视角 重新审视这些原则之间的内在联系,归纳它们在 Android 实际开发中的落地策略,并提供一张可随时查阅的速查表,帮助你在日常编码和架构设计中迅速做出正确的决策。


七大原则全景回顾

设计原则并非彼此孤立的教条,而是一套 相互支撑、环环相扣 的有机体系。一个违反单一职责原则的类,往往也同时违反了开闭原则和接口隔离原则;而依赖倒置原则的正确实践,天然地促进了里氏替换原则的达成。下面用一张 Mermaid 图来可视化它们之间的协同关系:

从图中可以清楚看到:所有原则最终都指向同一个终极目标——构建高内聚、低耦合、易扩展、可测试的软件系统。 每条原则从不同维度切入,共同形成了一张"防护网",防止代码随着需求增长而腐化。


原则速查表:一句话 + Android 映射

以下表格将每条原则浓缩为 一句核心心法,并给出其在 Android 开发中最典型的应用场景,方便日后快速查阅:

原则一句话心法Android 典型映射违反时的典型症状
S — 单一职责一个类只有一个变化的理由Activity 只负责 UI 调度,业务逻辑抽到 ViewModel/UseCase"God Activity":一个 Activity 超过 2000 行
O — 开闭原则新增功能靠扩展,已有代码不动RecyclerView.Adapter + 多 ViewType;自定义 Interceptor 扩展 OkHttp每次加需求都要修改核心 switch-case
L — 里氏替换子类在任何父类出现的地方都能安全替换自定义 View 重写 onDraw() 不破坏 View 合约子类重写方法后抛出 UnsupportedOperationException
I — 接口隔离客户端不应被迫依赖用不到的方法用 DefaultLifecycleObserver 替代需要全部实现的旧版 LifecycleObserver接口有 10 个方法,实现类 8 个留空
D — 依赖倒置高层模块和低层模块都依赖抽象Repository 模式:ViewModel → Repository(接口) ← RemoteDataSource直接 new Retrofit() 写在 ViewModel 里无法单元测试
迪米特法则只和"直接朋友"说话通过 Mediator/EventBus 通信,而非链式 getA().getB().getC()一个改动引发连锁编译错误
合成复用优先用"有一个"替代"是一个"TextWatcher 组合注入而非继承 EditText;Kotlin 委托 by继承层次超过 3 层,父类修改导致子类全部崩溃

原则之间的协同与张力

在真实项目中,这些原则并不总是完美和谐的,它们之间存在一定的 设计张力 (Design Tension),需要工程师根据场景做出权衡。

协同关系(Synergy)

  1. SRP + ISP → 细粒度抽象:单一职责驱动我们把大类拆小,接口隔离驱动我们把大接口拆小,两者方向一致,共同促进模块化。
  2. DIP + LSP → 安全的多态替换:依赖倒置让高层依赖抽象接口,里氏替换保证任何实现该接口的子类都能安全注入,两者形成完美闭环。这在 Android 中的 Repository 模式里体现得淋漓尽致——ViewModel 依赖 Repository 接口,FakeRepository 和 RealRepository 都可以无缝替换。
  3. OCP + CRP → 通过组合实现扩展:开闭原则要求不修改已有代码,合成复用原则提供了达成途径——通过组合新组件来扩展行为,而不是通过继承修改已有类。

张力关系(Tension)

  1. SRP vs. 过度拆分:过于极端地追求单一职责,可能导致类的数量爆炸,增加理解成本。权衡策略:以"变化的理由"为判断依据,而非机械地"一个方法一个类"。
  2. OCP vs. YAGNI:开闭原则鼓励提前设计扩展点,但 YAGNI (You Ain't Gonna Need It) 原则告诫我们不要为不确定的未来过度设计。权衡策略:对于明确会变化的维度(如网络库切换、数据源切换)预留抽象;对于不确定的维度,先写简单实现,等到需要扩展时再重构。
  3. ISP vs. 接口碎片化:过于细粒度的接口拆分可能导致接口数量过多,增加组装复杂度。权衡策略:以"角色"为单位划分接口,一个角色一个接口,而不是一个方法一个接口。

Android 分层架构中的原则落地全景

让我们将所有原则映射到 Android 推荐的分层架构中,看看每一层最需要关注哪些原则:

各层要点解读:

  • UI Layer:Activity/Fragment/Composable 最容易膨胀为 God Class,因此 SRP(单一职责) 是第一要务——只负责 UI 渲染和用户事件分发,绝不处理业务逻辑。同时遵守迪米特法则,UI 层不应该知道 DataSource 的存在,只和 ViewModel 对话。

  • Domain Layer:UseCase 是业务逻辑的原子单元,每个 UseCase 做一件事(SRP)。当新增业务规则时,新建 UseCase 而非修改已有的(OCP)。UseCase 暴露的接口应当精简(ISP)。

  • Data Layer:Repository 接口定义在 Domain 层,实现放在 Data 层——这是 DIP 的经典落地。不同的 DataSource(Remote/Local/Cache)实现同一个 Repository 接口,必须保证行为一致性(LSP)。新增数据源时只需新增实现类(OCP)。

  • DI Container:Hilt/Dagger 是 DIP 的基础设施,负责将抽象与具体实现绑定。它天然鼓励合成复用——通过构造函数注入组合各个模块,而非通过继承获取依赖。


代码坏味道(Code Smells)与原则对照

在 Code Review 时,以下常见的"坏味道"可以直接映射到被违反的原则,帮助你快速定位问题根因:

Kotlin
// ❌ 坏味道 1:God Class(违反 SRP)
// 一个 Activity 同时处理 UI、网络请求、数据库操作、业务逻辑
class OrderActivity : AppCompatActivity() {
    // 500 行 UI 代码...
    // 300 行网络请求代码...
    // 200 行数据库代码...
    // 400 行业务逻辑代码...
}
 
// ❌ 坏味道 2:Switch/When 类型判断泛滥(违反 OCP)
// 每新增一种消息类型,都必须修改这个函数
fun handleMessage(type: Int) {
    when (type) {
        1 -> handleText()
        2 -> handleImage()
        3 -> handleVideo()
        // 新增类型 4, 5, 6... 无限 case
    }
}
 
// ❌ 坏味道 3:火车残骸(Train Wreck,违反迪米特法则)
// 调用链暴露了内部结构,任何中间层的改动都会波及这里
val cityName = order.getCustomer().getAddress().getCity().getName()
 
// ❌ 坏味道 4:继承仅为复用代码(违反合成复用 + LSP)
// BaseActivity 塞满各种工具方法,子类"是一个"关系根本不成立
open class BaseActivity : AppCompatActivity() {
    fun showToast(msg: String) { /* ... */ }       // 工具方法
    fun checkNetwork(): Boolean { /* ... */ }       // 工具方法
    fun logEvent(event: String) { /* ... */ }       // 工具方法
    // 30 个与 Activity 生命周期无关的工具方法...
}
Kotlin
// ✅ 修正方向汇总
 
// SRP:拆分到 ViewModel + UseCase + Repository
// OCP:用多态(策略模式/工厂模式)替代 when
// 迪米特:提供高层方法 order.getCityName(),封装内部链路
// 合成复用:把工具方法提取为独立工具类,通过组合注入使用
 
// 例:用组合替代 BaseActivity 里的工具方法
class ToastHelper @Inject constructor(           // 独立的 Toast 工具
    private val context: Context                  // 只依赖必要的 Context
) {
    fun show(msg: String) {                       // 单一职责:只管 Toast
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
    }
}
 
class NetworkChecker @Inject constructor(        // 独立的网络检测工具
    private val connectivityManager: ConnectivityManager
) {
    fun isAvailable(): Boolean {                  // 单一职责:只管网络状态
        return connectivityManager.activeNetwork != null
    }
}
 
// Activity 通过组合使用,而非继承获取
class OrderActivity : AppCompatActivity() {
    @Inject lateinit var toastHelper: ToastHelper          // 组合注入
    @Inject lateinit var networkChecker: NetworkChecker     // 组合注入
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (!networkChecker.isAvailable()) {               // 清晰使用
            toastHelper.show("No network")                 // 职责明确
        }
    }
}

原则应用的"度":三级心智模型

初学者常犯两个极端错误:要么完全不顾原则(导致代码腐化),要么过度应用原则(导致过度设计)。以下是一个实用的三级心智模型:

  • Level 1(基本功):每位 Android 开发者都应该从第一天就践行。不写 God Activity、用接口解耦、优先组合——这三条就能避免 80% 的代码腐化。

  • Level 2(进阶):当你开始设计供团队使用的 SDK、库或公共模块时,OCP(扩展点设计)、ISP(精简 API 表面积)和迪米特法则(控制模块间耦合)变得至关重要。

  • Level 3(架构级):在大型项目的架构设计中,需要同时考虑所有原则的协同效应,并且能够在原则之间发生冲突时做出合理的权衡(Trade-off)。这是从"工程师"到"架构师"的跨越。


Android Framework 中的原则体现

回顾本章内容,Android Framework 本身就是设计原则的一部教科书:

Framework 组件体现的原则具体分析
View / ViewGroup 体系OCP + LSP通过继承扩展自定义 View,onDraw()、onMeasure() 等模板方法保证了子类可安全替换
RecyclerView + Adapter + LayoutManagerSRP + OCP + DIP数据绑定、布局策略、滑动回收分别由不同组件负责;新增布局只需扩展 LayoutManager
ContentProviderDIP + 迪米特应用间数据共享通过统一的 URI 抽象,调用方不知道底层是 SQLite 还是文件
BroadcastReceiverOCP + 迪米特发送方不知道接收方是谁,新增接收方只需注册,无需修改发送方
Lifecycle / LiveDataISP + SRPDefaultLifecycleObserver 提供细粒度的生命周期回调接口,LiveData 单一职责——只做可观察的数据持有
OkHttp Interceptor 链OCP + SRP每个 Interceptor 只做一件事(日志、重试、缓存),新增功能只需添加新的 Interceptor

核心要义:三句话总结全章

  1. 原则是"为什么",模式是"怎么做"——设计原则提供了判断代码好坏的标尺,而设计模式(我们后续章节的内容)是落地这些原则的具体方案。

  2. 所有原则的终极目标只有一个:管理变化——软件开发中唯一不变的就是变化,每条原则都在回答同一个问题:"当需求变化时,如何让修改的范围最小、影响最小、成本最低?"

  3. 原则不是法律,而是 Trade-off 的指南针——在工程实践中,100% 遵守所有原则既不现实也不必要。关键是在违反某条原则时,你 清楚地知道 自己在做什么 trade-off,以及为什么在当前场景下这个 trade-off 是合理的。


📝 练习题

题目 1: 以下代码违反了哪些设计原则?(多选)

Kotlin
class UserManager {
    fun login(username: String, password: String) { /* 网络请求登录 */ }
    fun saveUserToDb(user: User) { /* 操作数据库 */ }
    fun formatUserName(name: String): String { /* 格式化展示 */ }
    fun sendAnalyticsEvent(event: String) { /* 发送埋点 */ }
}

A. 单一职责原则(SRP)

B. 开闭原则(OCP)

C. 接口隔离原则(ISP)

D. 迪米特法则(LoD)

【答案】 A

【解析】 UserManager 承担了至少四种不同的职责:认证(login)、持久化(saveUserToDb)、UI 展示格式化(formatUserName)、数据分析(sendAnalyticsEvent)。这意味着它有四个不同的变化理由(认证逻辑变化、数据库结构变化、UI 格式变化、埋点 SDK 变化),明确违反了 单一职责原则。正确做法是拆分为 AuthRepository、UserDao、UserFormatter、AnalyticsTracker 四个独立类。虽然代码中未涉及继承或接口定义,因此 OCP、ISP、LoD 在当前代码片段中无法直接判定违反。


题目 2: 在 Android 项目中,ViewModel 直接创建了 Retrofit.create(ApiService::class.java) 来获取网络数据。这主要违反了以下哪条原则?

A. 里氏替换原则(LSP)

B. 依赖倒置原则(DIP)

C. 合成复用原则(CRP)

D. 开闭原则(OCP)

【答案】 B

【解析】 ViewModel 属于高层模块(业务逻辑层),Retrofit/ApiService 属于低层模块(网络实现细节层)。ViewModel 直接 new 出 Retrofit 实例,意味着高层模块直接依赖了低层模块的具体实现,而非依赖抽象接口——这是典型的 依赖倒置原则(DIP) 违反。正确做法是:ViewModel 依赖 Repository 接口,Repository 的具体实现(内部使用 Retrofit)通过 DI 框架(如 Hilt)注入。这样 ViewModel 既不知道网络库是什么,也可以在单元测试中轻松替换为 FakeRepository。虽然也间接涉及 OCP(切换网络库需要改 ViewModel),但 根因(root cause) 是 DIP 的违反。