设计模式与 Android 源码应用
设计模式几乎每场中级面试都问, 且面试官爱问 “在 Android 源码 / 常用库里的实际应用”: 光背定义不够, 要能举出框架里的真实例子. 本篇按这个思路组织.
学习目标与使用边界
完成后应能从变化点选择模式, 写出最小 Kotlin/Java 模板, 并说明生命周期, 并发和失败转移. 本章以模式机制和 Android 对照为主; 支付领域完整验签, 幂等与对账链路见支付订单与状态机, 不能用模式名替代业务可靠性设计.
一, SOLID 五项原则与补充原则
| 原则 | 核心含义 | Android 反例 → 正例 | 面试落点 |
|---|---|---|---|
| 单一职责 (SRP) | 一个类只承担一个变化原因 | Activity 同时写 UI, 网络, 缓存, 埋点 → 拆成 ViewModel, Repository, Tracker | 不是 “类越小越好”, 而是变更原因隔离 |
| 开闭原则 (OCP) | 对扩展开放, 对修改关闭 | 支付页新增渠道就改 when(type) → 抽 PaymentStrategy, 新增渠道只加实现 | 用多态 / 注册表替代反复改老代码 |
| 里氏替换 (LSP) | 子类可替换父类而不破坏调用方预期 | 自定义 View 重写 onMeasure 却不尊重 MeasureSpec → 保持父类契约 | 继承要遵守父类语义, 否则组合优先 |
| 接口隔离 (ISP) | 接口最小化, 不强迫实现无用方法 | UserModuleService 同时暴露登录, 头像, 支付状态 → 拆 AuthService/ProfileService | 组件化接口下沉时尤其重要 |
| 依赖倒置 (DIP) | 高层依赖抽象, 不依赖具体实现 | ViewModel 直接 new RetrofitApi → 依赖 UserRepository 接口, 由 Hilt / 工厂注入实现 | DI 的理论基础, 也是可测试性的基础 |
| 迪米特法则 | 最少知识, 只和直接朋友通信 | 页面跨模块拿 OrderManager.userManager.tokenStore.token → 通过 AuthService.getToken() | 减少调用链泄漏, 降低跨模块耦合 |
Android 重构口诀: Activity 变薄 (SRP), 新增业务走扩展点 (OCP), 继承守契约 (LSP), 跨模块接口要小 (ISP), 高层依赖接口 (DIP), 不要跨层级 “摸内部对象”(迪米特).
SOLID 只有五项原则. 迪米特法则 (Law of Demeter) 和 “组合优于继承” 是常用的补充原则, 不是 SOLID 的第六项.
设计模式题的统一答法
不要先背模式名, 先描述变化点和约束:
- 哪一部分会变化, 谁应该拥有这份变化.
- 调用方依赖什么稳定契约, 新实现如何接入.
- 生命周期, 并发, 错误恢复和测试替换点在哪里.
- 引入模式增加了多少类型和间接层, 当前规模是否值得.
这套顺序能把 “我知道这是观察者模式” 变成 “我为什么在这里选择观察者, 以及它的代价是什么”.
二, 创建型模式
单例 (最高频)
确保一个类只有一个实例. Kotlin 用 object 天然单例. Java 重点是双重检查锁 (DCL):
class Singleton private constructor() {
companion object {
@Volatile private var instance: Singleton? = null
fun get(): Singleton =
instance ?: synchronized(this) {
instance ?: Singleton().also { instance = it }
}
}
}
- volatile 的作用: 防止指令重排导致拿到未初始化完成的对象.
- Android 应用: 进程内共享的
OkHttpClient, 数据库实例和 Repository.Application由框架在进程内创建和管理, 常表现为单实例, 但不是业务代码通过私有构造器实现的 GoF 单例;getSystemService()返回对象是否缓存也取决于具体服务实现.
Java 静态内部类模板: 类初始化由 JVM 保证线程安全, 且内部类仅在第一次 getInstance() 时初始化, 兼具懒加载与无需显式锁.
public final class SettingsStore {
private SettingsStore() {}
private static class Holder {
static final SettingsStore INSTANCE = new SettingsStore();
}
public static SettingsStore getInstance() { return Holder.INSTANCE; }
}
边界: 这只保证单个 ClassLoader 内的单例, Android 多进程会各有实例; 它也不自动保证所持有的 Context, 缓存和 I/O 的线程安全. 测试可并发获取多个引用并断言相同, 再单独验证初始化副作用只执行一次.
工厂 / 抽象工厂
封装对象创建, 调用方只描述 “我要什么”, 不关心具体构造细节.
- 角色: 调用方 (Client)→ 工厂 (Factory)→ 具体产品 (Product).
- Android 应用:
BitmapFactory.decodeStream()根据输入流 / Options 创建Bitmap;LayoutInflater.from(context)根据Context找到合适 inflater;Executors根据方法创建不同线程池. - 为什么匹配: 调用方不直接
new Bitmap(...), 而把复杂创建逻辑, 兼容分支, 缓存 / 复用细节交给工厂.
简单工厂, 工厂方法, 抽象工厂怎么区分
| 形式 | 变化点 | Android / 业务例子 | 主要代价 |
|---|---|---|---|
| 简单工厂 | 一个入口按参数选择产品 | PaymentFactory.create(channel) | 新增产品通常要修改工厂分支 |
| 工厂方法 | 把创建延迟给子类 / 实现 | 不同环境创建不同 DataSource | 类型数量增加, 需要维护创建者层次 |
| 抽象工厂 | 一次创建一组相互匹配的产品 | 同一渠道的 ApiClient + Serializer + Signer | 适合产品族, 不适合只有一个可变对象 |
面试时不要把所有 create() 都叫抽象工厂. 只有 “创建一组有关联的产品” 时才需要抽象工厂.
简单工厂模板: 一个集中入口按稳定枚举选择产品, 新增渠道必须修改分支, 因此适合渠道数量少且受控的场景.
enum class ParserType { JSON, CSV }
interface Parser { fun parse(input: String): List<String> }
object ParserFactory {
fun create(type: ParserType): Parser = when (type) {
ParserType.JSON -> JsonParser()
ParserType.CSV -> CsvParser()
}
}
class JsonParser : Parser { override fun parse(input: String) = listOf(input) }
class CsvParser : Parser { override fun parse(input: String) = input.split(',') }
这是可运行的 Kotlin/JVM 最小示例, JsonParser 仅为了展示构造分派, 并未真正解析 JSON. 边界: 外部插件可扩展渠道时应改注册表 / 工厂方法, 避免每次改中心 when; 未知输入必须在解析为 ParserType 时显式报错.
建造者 (Builder)
链式构造复杂对象, 适合可选参数多, 构建过程需要校验的场景.
- 角色:
Builder暂存配置,build()/show()生成最终对象. - Android 应用:
AlertDialog.Builder.setTitle().setPositiveButton().show(),OkHttpClient.Builder.addInterceptor().build(),Retrofit.Builder.baseUrl().addConverterFactory().build(),Notification.Builder, Glide 请求构建. - 为什么匹配: 避免超长构造函数, 把 “配置过程” 和 “不可变/可执行对象” 分离; 面试讲 OkHttp/Retrofit 时可顺带讲 Builder.
原型 (Prototype)
通过拷贝已有对象创建新对象, 适合 “保留大部分配置, 只改少数字段”.Android 应用: Intent.clone()/复制 Bundle 后修改 extras. 它匹配原型模式, 因为新对象来自已有对象快照, 而不是从零组装.
深浅拷贝边界: Kotlin data class.copy() 是浅拷贝, 引用字段仍共享; 只有嵌套可变对象也被复制才是所需意义上的深拷贝.
data class RetryPolicy(val delaysMs: MutableList<Long>)
data class RequestConfig(val url: String, val retry: RetryPolicy)
val original = RequestConfig("/pay", RetryPolicy(mutableListOf(100, 500)))
val shallow = original.copy()
shallow.retry.delaysMs += 1_000 // original 也会看到 1_000
val deep = original.copy(retry = original.retry.copy(delaysMs = original.retry.delaysMs.toMutableList()))
练习证据: 修改 deep.retry.delaysMs 后 original 不变; 修改 shallow 后原对象变化. 不可变集合能减少此类风险, 但不等同于跨进程 / 磁盘快照.
三, 结构型模式
代理 (Proxy)
为真实对象提供一个代理入口, 在调用前后做控制, 延迟, 跨进程或增强.
- 角色: 接口 Subject, 真实对象 RealSubject, 代理 Proxy.
- Retrofit:
retrofit.create(Api::class.java)用Proxy.newProxyInstance生成接口实现; 调用api.getUser()时进入InvocationHandler, 解析注解并创建 HTTP 请求. - Binder AIDL: 客户端拿到的是
Stub.Proxy, 方法调用先写入Parcel, 再通过 Binder 驱动发给服务端Stub.onTransact(). - 为什么匹配: 调用方以为自己在调普通接口, 实际被代理拦截并转成网络 / 跨进程调用.
动态代理定义与最小模板: 运行时代理由 Proxy 为接口生成实现, 所有方法调用进入 InvocationHandler; 它要求目标类型是接口, 类代理需要其他机制.
interface Greeting { String hello(String name); }
Greeting logged = (Greeting) java.lang.reflect.Proxy.newProxyInstance(
Greeting.class.getClassLoader(),
new Class<?>[] { Greeting.class },
(proxy, method, args) -> {
if (method.getName().equals("hello")) return "hello, " + args[0];
throw new UnsupportedOperationException(method.toString());
}
);
// logged.hello("Ada") -> "hello, Ada"
此 Java 片段可放入普通 JVM main 运行; 生产 handler 还要正确处理 equals/hashCode/toString, 异常包装, 线程与反射开销. Retrofit 的代理还会解析注解, 缓存 service method, 并非这个最小 handler 的全部逻辑.
适配器 (Adapter)
把一个接口转换成调用方需要的另一个接口.
- 角色: 目标接口 (Target), 被适配者 (Adaptee), 适配器 (Adapter).
- RecyclerView.Adapter: 数据源可能是
List<Item>, 但RecyclerView需要getItemCount/onCreateViewHolder/onBindViewHolder这组接口; Adapter 把数据转换成可复用的ViewHolder绑定流程. - 为什么匹配:
RecyclerView不直接认识业务数据, 只认识 Adapter 契约; 业务侧实现契约即可接入框架.
装饰器 (Decorator)
在不修改原对象的情况下动态叠加能力.
- 角色: 组件接口 Component, 被装饰对象 ConcreteComponent, 装饰器 Decorator.
- Android 应用:
ContextWrapper持有一个 baseContext并转发大多数调用;ContextThemeWrapper在 base 能力上增加 theme 解析能力. - 可验证点: AOSP
ContextWrapper.java持有mBase字段,getSystemService()/startActivity()等方法直接转发给mBase对应方法, base 在构造时经attachBaseContext()注入;ContextThemeWrapper重写onApplyThemeResource()把 theme 解析叠加到 base 能力上. - 为什么匹配: 外部仍按
Context使用, 但功能被包装增强; 这与继承扩展不同, 装饰可以按需组合.
外观 (Facade)
提供统一高层接口, 隐藏子系统复杂度.Android 应用: Context.getSystemService() 把 ActivityManager, ClipboardManager, LayoutInflater 等系统能力收口到统一入口; 调用方不需要知道服务发现和 Binder 细节. 它匹配外观模式, 因为 Context 是简化入口, 不是具体业务实现本身.
组合 (Composite)
把单个对象和对象树用同一套接口处理.Android 应用: ViewGroup 同时作为 View 使用, 又持有和遍历子 View; 调用方可以对整棵 View 树执行测量, 布局和绘制, 不需要区分叶子节点和容器.
- 适用边界: 层级天然存在, 且父节点和子节点需要统一操作时适合. 如果只是多个对象的顺序调用, 不要为了形式引入树结构.
- 面试追问: ViewGroup 的
measure/layout/draw为什么是组合模式? 需要补 “父节点递归驱动子节点, 但子节点仍遵守 View 契约”. - 可验证点:
ViewGroup通过getChildAt()/getChildCount()管理子节点,dispatchTouchEvent()中遍历 children 逐个调用dispatchTransformedTouchEvent()完成事件分发; 绘制同理由dispatchDraw()递归驱动整棵 View 树.
桥接 (Bridge)
把抽象和实现拆成两个可以独立变化的层次.Android 工程应用: 业务定义统一 ImageLoader 抽象, 再接入 Glide, Coil 或测试实现; 图片加载能力和具体库可以分别演进, 调用页面不依赖第三方 API. 框架级例子是 Window(抽象窗口行为) 与其平台实现 PhoneWindow: Activity 只依赖 Window 抽象, 具体窗口实现可替换 (以当前 AOSP 源码核验).
桥接和适配器的区别是: 适配器解决已有接口不兼容, 桥接从设计之初就把两个变化维度拆开, 避免继承组合爆炸.
享元 (Flyweight)
共享可复用的内部状态, 降低大量相似对象的内存成本.Android 应用: Drawable.ConstantState 可让多个 Drawable 实例共享相同资源对应的常量状态, 各实例再保留边界, 状态等外部数据; 调用 mutate() 时要理解共享状态会被分离. StateListDrawable 同样经 ConstantState 共享同一份 selector 资源的子 drawable 列表, 多个实例共用这份不变数据, 各自只保留当前选中状态; 布局中多处引用同一 drawable 时它们共享同一份 ConstantState.
注意享元必须区分可共享的内部状态和每次调用的外部状态. RecyclerView ViewHolder 和 BitmapPool 更准确地说是对象池 / 复用机制, 不应仅因 “复用” 就机械归为享元. 把带有页面生命周期的对象做全局共享, 还会造成泄漏和状态污染.
四, 行为型模式
观察者 (Observer, 高频)
一对多依赖, 被观察者状态变化时通知观察者.
- 角色: Subject 保存观察者列表, Observer 接收回调.
- Android 应用:
LiveData.observe(owner) {}在生命周期活跃时通知观察者;OnClickListener是 View 事件的观察回调;Adapter.notifyDataSetChanged()通知 RecyclerView 数据变化. - 为什么匹配: 数据/事件源不直接依赖具体页面逻辑, 只通知已注册观察者; 注意 Flow 更偏响应式流, 面试可类比观察者但要说明它还有冷/热流, 背压, 协程上下文语义.
- 踩坑: 注册了忘注销会造成泄漏:
onDestroy中必须对称unsubscribe/removeObserver, 把监听器绑定到宿主生命周期可减少遗漏; 通知顺序依赖注册顺序, 订阅者相互影响时异常顺序难排查.
经典 Subject / Observer 最小完整模板: Subject 只依赖 Observer 接口; 订阅, 取消订阅与通知均由它负责. 实际 Android 场景还必须把生命周期和线程切换纳入实现.
fun interface Observer<T> {
fun onChanged(value: T)
}
class Subject<T> {
private val observers = mutableSetOf<Observer<T>>()
fun subscribe(observer: Observer<T>) { observers += observer }
fun unsubscribe(observer: Observer<T>) { observers -= observer }
fun publish(value: T) { observers.toList().forEach { it.onChanged(value) } }
}
val subject = Subject<String>()
val observer = Observer<String> { value -> println(value) }
subject.subscribe(observer)
subject.publish("updated")
subject.unsubscribe(observer)
+----------+ subscribe / unsubscribe +------------+
| Observer | <---------------------------- | Subject |
+----------+ +------------+
^ |
+-------------- onChanged(value) -----------+
责任链 (Chain of Responsibility, 高频)
请求沿链传递, 每个节点决定处理, 增强或继续传递.
- OkHttp:
RealInterceptorChain.proceed(request)把请求交给下一个拦截器; 应用拦截器, 重试重定向, 桥接, 缓存, 连接, CallServer 等节点逐层处理. - View 事件分发:
Activity.dispatchTouchEvent → ViewGroup.dispatchTouchEvent/onInterceptTouchEvent → View.dispatchTouchEvent/onTouchEvent, 每层决定拦截, 消费或继续下发 / 回传. - 为什么匹配: 发送方不需要知道最终谁处理, 链上每个节点只关心自己职责; 这也是和主流第三方库中 OkHttp 内容的跨章重复点, 这里讲模式原因即可.
- 踩坑: 链上顺序直接决定结果: OkHttp 中缓存 / 重试拦截器位置不同, 缓存命中与重试行为就不同; 每个节点要显式返回 “已处理” 或 “继续传递”, 不能把
null同时表达失败和未处理.
策略 (Strategy)
封装可互换算法, 调用方依赖统一接口.Android 应用: 属性动画依赖 TimeInterpolator.getInterpolation(input), 线性, 加速, 回弹等插值器都可替换. 它匹配策略模式, 因为动画框架不改主流程, 只替换 “时间进度如何映射为动画进度” 的算法.
模板方法 (Template Method)
父类 / 框架定义流程骨架, 子类填充步骤.Android 应用: ActivityThread/框架按生命周期调度, 业务 Activity 重写 onCreate/onStart/onResume; 自定义 View 重写 onMeasure/onDraw. 它匹配模板方法, 因为整体流程由框架控制, 应用只实现钩子步骤. AsyncTask 也体现该模式但已废弃, 面试只作历史例子.
命令 (Command)
把请求封装成对象, 便于排队, 延迟, 取消或跨线程传递.Android 应用: Handler.post(Runnable) 把一段动作封装进 MessageQueue; Message.what/obj/callback 描述要执行的命令. 它匹配命令模式, 因为发送方只投递命令对象, 执行时机由 Looper 决定.
可验证点: Message.callback 字段即命令对象, Handler.dispatchMessage() 优先执行 msg.callback, 否则回调 handleMessage(msg); Looper.loop() 从 MessageQueue.next() 取出消息后交回 msg.target.dispatchMessage() 执行.
状态 (State, 高频)
把对象在不同状态下的行为封装起来, 避免一个巨大的 when(state) 分支不断膨胀.Android / 业务应用: 支付订单可有 Created, Paying, Success, Failed, Cancelling, Cancelled; 每个状态定义允许的事件和转移结果, UI 只观察状态快照.
sealed interface OrderEvent {
data object Pay : OrderEvent
data object Paid : OrderEvent
data class Error(val reason: String) : OrderEvent
data object Retry : OrderEvent
data object Cancel : OrderEvent
data object Cancelled : OrderEvent
}
sealed interface OrderState {
data object Created : OrderState
data object Paying : OrderState
data class Failed(val reason: String) : OrderState
data object Success : OrderState
data object Cancelling : OrderState
data object Cancelled : OrderState
}
fun reduce(state: OrderState, event: OrderEvent): OrderState = when (state) {
OrderState.Created -> when (event) {
OrderEvent.Pay -> OrderState.Paying
OrderEvent.Cancel -> OrderState.Cancelled
else -> state
}
OrderState.Paying -> when (event) {
OrderEvent.Paid -> OrderState.Success
is OrderEvent.Error -> OrderState.Failed(event.reason)
OrderEvent.Cancel -> OrderState.Cancelling
else -> state
}
is OrderState.Failed -> if (event is OrderEvent.Retry) OrderState.Paying else state
OrderState.Cancelling -> if (event is OrderEvent.Cancelled) OrderState.Cancelled else state
OrderState.Success, OrderState.Cancelled -> state
}
State vs Strategy: Strategy 是调用方主动选择一个可替换算法; State 是对象状态变化后决定下一步行为, 通常由状态机驱动转移. 支付, 下载, 连接, 登录等有明确生命周期和非法转移时优先考虑 State.
支付状态图 (简化示例):
Created --Pay--> Paying --Paid--> Success
| | \--Error--> Failed --Retry--> Paying
\--Cancel------> Cancelled
Paying --Cancel--> Cancelling --Cancelled--> Cancelled
图与 reduce 使用同一组状态和事件: Created/Paying/Failed/Success/Cancelling/Cancelled 以及 Pay/Paid/Error/Retry/Cancel/Cancelled. 其中 Paid 必须绑定服务端交易号 / 验签结果, 不能仅凭客户端回调进入 Success; 网络超时通常应进入 “待确认” 或触发查询, 而不是直接 Failed. 每一次持久化转移应带幂等 key. 此处的 OrderState/reduce 为演示状态模式的简化教学模型 (状态类 / 转移表 / 非法转移兜底), 生产级完整订单状态机 (含 PENDING_CONFIRMATION, REFUNDING, RISK_REVIEW 等异常态) 见支付订单与状态机.
踩坑: else -> state 静默忽略非法转移会让状态机 “没有兜底”, 非法事件被吞掉后线上难排查; 应在遇到未定义转移时记录日志 / 埋点, 或显式落入 Illegal 状态, 而不是静默返回原状态.
中介者 (Mediator)
用一个中介对象协调多个对象, 避免对象之间形成网状依赖.Android 应用: FragmentManager 协调 Fragment 生命周期和事务, CoordinatorLayout 协调 AppBarLayout, 滚动子 View 等参与者; 组件化中也可以用一个明确的导航 / 服务入口协调跨模块调用.
中介者不是把所有业务都塞进一个 “万能 Manager”. 中介者自身必须有清晰边界, 否则只是把网状耦合集中成一个巨型类.
其他
- 迭代器: 集合的
Iterator. - 备忘录:
onSaveInstanceState.
五, 高频模式选择表
| 面试场景 | 优先考虑 | 不要混淆 |
|---|---|---|
| 多种支付/排序/校验算法可替换 | Strategy | 有状态转移时用 State |
| 订单/下载/连接生命周期 | State | 只替换算法时不要做 State |
| 旧接口接入新框架 | Adapter | 运行时增强用 Decorator, 调用拦截用 Proxy |
| 给对象叠加日志, 缓存, 重试 | Decorator | 跨进程 / 动态代理更接近 Proxy |
| 隐藏子系统复杂调用 | Facade | 不是把所有逻辑都放进一个 Manager |
| 请求逐层处理 | Chain of Responsibility | 每层都必须能决定终止或继续 |
| 页面/模块之间互相协调 | Mediator | 需要长期状态时仍要配合 Repository/StateFlow |
| 组装一个多参数复杂对象 | Builder | Factory 关注创建哪一种产品, Builder 关注如何分步配置一个产品 |
| 一对多同步回调 | Observer | Flow 还定义冷 / 热流, 取消, 操作符和协程上下文语义 |
高频手写模板
Builder 模板: Builder 暂存可选配置, build() 校验后产出不可变对象; 避免超长构造函数和半初始化对象.
class HttpClient private constructor(val baseUrl: String, val timeoutMs: Long) {
class Builder {
private var baseUrl: String? = null
private var timeoutMs: Long = 10_000
fun baseUrl(v: String) = apply { baseUrl = v }
fun timeout(v: Long) = apply { timeoutMs = v }
fun build(): HttpClient = HttpClient(
requireNotNull(baseUrl) { "baseUrl is required" },
timeoutMs,
)
}
}
Strategy 模板见下方 PaymentStrategy: 它把渠道选择与支付算法解耦, 是策略模式的可运行骨架, 不再重复写第二个接口.
支付策略的关键不是写一个接口, 而是让渠道选择和支付算法都可替换, 同时把未知渠道变成显式错误:
enum class PayChannel { ALIPAY, WECHAT }
sealed interface PaymentResult {
data class Submitted(val transactionId: String) : PaymentResult
data class Failed(val reason: String) : PaymentResult
}
fun interface PaymentStrategy {
suspend fun pay(orderId: String): PaymentResult
}
class PaymentProcessor(
private val strategies: Map<PayChannel, PaymentStrategy>,
) {
suspend fun pay(channel: PayChannel, orderId: String): PaymentResult {
val strategy = requireNotNull(strategies[channel]) {
"Unsupported channel: $channel"
}
return strategy.pay(orderId)
}
}
责任链模板要明确 “已处理” 和 “继续传递”, 不能让 null 同时表达失败和未处理:
data class Request(val token: String?)
sealed interface HandleResult {
data class Accepted(val token: String) : HandleResult
data class Rejected(val reason: String) : HandleResult
data object Continue : HandleResult
}
abstract class RequestHandler(
private val next: RequestHandler? = null,
) {
fun handle(request: Request): HandleResult = when (val result = doHandle(request)) {
HandleResult.Continue -> next?.handle(request) ?: HandleResult.Continue
else -> result
}
protected abstract fun doHandle(request: Request): HandleResult
}
事件订阅需要把生命周期, 缓冲和一次性语义写进实现. SharedFlow 可以承载发布订阅, 但它比经典 Observer 多了协程取消和流操作语义:
sealed interface AppEvent {
data class Message(val text: String) : AppEvent
}
class EventHub {
private val mutableEvents = MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 32,
)
val events: SharedFlow<AppEvent> = mutableEvents.asSharedFlow()
fun publish(event: AppEvent): Boolean = mutableEvents.tryEmit(event)
}
fun LifecycleOwner.observeEvents(
eventHub: EventHub,
onEvent: suspend (AppEvent) -> Unit,
) = lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
eventHub.events.collect(onEvent)
}
}
tryEmit() 返回 false 时要有丢弃, 持久化或降级策略. 订单结果等关键业务事实不应只放在进程内事件流中, 应由 Repository 持久化状态, UI 再观察状态源.
设计模式的工程检查项
- 生命周期: 模式对象的作用域是否比它持有的 Context, View 或回调更长.
- 并发: 单例, 缓存, 观察者列表和状态转移是否有明确线程模型.
- 错误恢复: 责任链, 策略和状态机是否定义失败, 重试, 取消和超时.
- 可测试性: 依赖是否能替换为 fake, 是否需要测试观察通知顺序和非法状态转移.
- 复杂度: 引入模式后类数量, 调用链和调试成本是否真的下降.
主题练习与预期证据
- 并发调用静态内部类
getInstance(), 预期全部引用相等; 另说明为何 Android remote process 不共享该对象. - 给简单工厂输入未知字符串, 预期在输入转换层返回可观察错误, 而不是落入默认 Parser.
- 画出一次支付超时后的 “待确认 / 查询” 转移, 预期不允许客户端仅因超时将已扣款订单置为 Failed.
六, 记忆法: 按 “框架反推模式”
面试被问设计模式, 从你熟的框架反推最稳:
- OkHttp 拦截器 → 责任链
- Retrofit create → 代理 (动态代理)
- 各种 Builder → 建造者
- LiveData/Flow → 观察者
- RecyclerView.Adapter → 适配器
- 事件分发 → 责任链
RequestBuilder链式配置图片请求 → 建造者- View/ViewGroup → 组合
- 支付 / 下载状态机 → 状态
- FragmentManager/CoordinatorLayout → 中介者
高频面试题
Q1: 手写一个线程安全的单例.
用 DCL (双重检查锁)+ volatile, 或静态内部类 (依赖 JVM 类加载的线程安全初始化 + 懒加载), Kotlin 直接 object. volatile 防止指令重排拿到半初始化对象.
Q2: DCL 里 volatile 为什么必须加?
instance = Singleton() 不是原子操作, 分为分配内存, 初始化, 赋值引用三步. 指令重排后其他线程可能拿到已赋值但未初始化完成的对象. volatile 禁止重排.
Q3: OkHttp 用了什么设计模式? 责任链 (拦截器链, 核心), 建造者 (OkHttpClient.Builder). 不要生搬 “工厂 / 外观”: OkHttpClient 是 Builder 的产物, 不是 GoF 外观. 重点讲责任链: 请求依次经过各拦截器, 每个调 chain.proceed 传递.
Q4: Retrofit 怎么用设计模式把接口变成实现? 动态代理 (代理模式). create () 用 Proxy.newProxyInstance 生成接口代理, 方法调用被 InvocationHandler 拦截, 解析注解构造请求.
Q5: 观察者模式在 Android 哪里用到? LiveData/Flow 的数据观察, RxJava, 事件监听器, BroadcastReceiver. 一对多, 被观察者变化时通知所有观察者.
Q6: 事件分发是什么设计模式? 责任链. 事件沿 View 树 (Activity→ViewGroup→View) 传递, 每层决定拦截处理还是向下 / 向上传递.
Q7: MVC/MVP/MVVM 算设计模式吗? 算架构模式 (架构层面的模式), 不是 GoF 23 种设计模式. MVVM 内部用到观察者 (数据绑定).区分 “设计模式”(类级别) 和 “架构模式”(应用级别).
Q8: Strategy 和 State 的区别? Strategy 由调用方选择一个可互换算法, 算法之间通常相互独立; State 由当前状态决定行为和下一状态, 重点是合法转移和生命周期. 支付, 下载, 连接状态机优先讲 State.
Q9: Adapter, Decorator, Proxy 怎么区分? Adapter 改接口, 让不兼容的对象接入; Decorator 保持同一接口并叠加能力; Proxy 通常代表另一个对象, 在调用前后做控制, 延迟, 权限, 跨进程或远程调用.
Q10: 什么时候不应该使用设计模式? 只有一个实现且没有稳定变化点时, 直接函数或简单类更清晰. 模式应减少变化成本和耦合, 不能为了展示知识引入无意义的工厂, Manager 或层级.