Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

设计模式与 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 的第六项.

设计模式题的统一答法

不要先背模式名, 先描述变化点和约束:

  1. 哪一部分会变化, 谁应该拥有这份变化.
  2. 调用方依赖什么稳定契约, 新实现如何接入.
  3. 生命周期, 并发, 错误恢复和测试替换点在哪里.
  4. 引入模式增加了多少类型和间接层, 当前规模是否值得.

这套顺序能把 “我知道这是观察者模式” 变成 “我为什么在这里选择观察者, 以及它的代价是什么”.

二, 创建型模式

单例 (最高频)

确保一个类只有一个实例. 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 持有一个 base Context 并转发大多数调用; 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
组装一个多参数复杂对象BuilderFactory 关注创建哪一种产品, Builder 关注如何分步配置一个产品
一对多同步回调ObserverFlow 还定义冷 / 热流, 取消, 操作符和协程上下文语义

高频手写模板

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, 是否需要测试观察通知顺序和非法状态转移.
  • 复杂度: 引入模式后类数量, 调用链和调试成本是否真的下降.

主题练习与预期证据

  1. 并发调用静态内部类 getInstance(), 预期全部引用相等; 另说明为何 Android remote process 不共享该对象.
  2. 给简单工厂输入未知字符串, 预期在输入转换层返回可观察错误, 而不是落入默认 Parser.
  3. 画出一次支付超时后的 “待确认 / 查询” 转移, 预期不允许客户端仅因超时将已扣款订单置为 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 或层级.