客户端架构:为什么、什么时候、怎么做
https://blog.csdn.net/mix39/article/details/161257993客户端设计(上):MVC/MVP/MVVM 与高内聚低耦合
https://blog.csdn.net/mix39/article/details/161257807客户端设计(中):OOP、SOLID 与设计模式
https://blog.csdn.net/mix39/article/details/148036409客户端设计(下):场景流派与实战设计方式
https://blog.csdn.net/mix39/article/details/161322269?spm=1001.2014.3001.5502
前言
架构模式(MV*)决定了代码的"分工体制",高内聚低耦合决定了代码的"质量下限"。但真正落地的武器是什么?是 OOP、SOLID、设计模式—— 一条线从基础到套路:先学面向对象,再学设计原则,最后用设计模式落地。
8.3 面向对象编程(OOP)
面向对象是设计模式和设计原则的基础。封装、继承、多态三大特性让代码有了边界、复用和扩展能力。但 OOP 不是银弹——继承的耦合、抽象的边界、类膨胀的风险,都是实战中必须面对的问题。
8.3.1 封装、继承、多态
封装——把变化关起来
藏实现细节,只暴露行为。外面只知道"能干什么",不知道"怎么干的"。
封装前:未读数直接暴露 public int unreadCount;
任何地方都能 unreadCount = -1; // 谁知道传了什么鬼
封装后:private int unreadCount;
public void setUnreadCount(int count) {
if (count < 0) this.unreadCount = 0;
else this.unreadCount = count;
}
// 不合法的值进不来,变化控制在类内部
封装的本质不是藏字段,而是藏变化——字段类型可能改、校验规则可能加、赋值可能有副作用,这些变化被锁在类内部,外部无感。
继承——让子类复用父类的能力
is-a 关系:Dog is an Animal,ChatTab is a Tab。
abstract class TabPage {
abstract fun onCreateView() // 子类填
fun onLoadFinish() { ... } // 父类提供通用逻辑
}
class ChatTabPage : TabPage() {
override fun onCreateView() { ... } // 只写自己特殊的
}
继承的限制:只能继承一个父类,功能有限。Java 单继承决定了你没法同时复用多个父类的能力,这也是为什么组合比继承更灵活。
多态——同一接口,不同行为
父类引用指向子类对象,调用同一方法,执行不同逻辑。
// 多态:同一接口,运行时不同行为
TabPreload preloader = new ChatPreload();
preloader.preloadSync(); // 执行 ChatPreload 的逻辑
preloader = new HomePreload();
preloader.preloadSync(); // 执行 HomePreload 的逻辑
多态的三个前提:继承/实现、方法重写、父类引用指向子类对象。
三者的关系:封装隐藏实现,继承复用能力,多态扩展行为。抽象定义能力——是三者的灵魂。
抽象——提取共性,忽略差异
抽象类/接口只定义"有什么能力",不定义"怎么实现"。
interface TabPreload {
fun preloadSync() // 能力:同步预加载
fun preloadOnVisible() // 能力:可见时预加载
}
// AbstractTabPreload 提供默认实现
// 具体的 Tab 各自 override 自己的预加载逻辑
抽象的边界:
抽象最适合用在一个通用的、稳定的、跨业务的公共能力上——比如网络框架、缓存组件、日志体系。
抽象不太适合的场景:自定义 View 或 BaseActivity。实际项目中经常出现 BaseActivity 东塞一个 View 接口、西塞一个回调,实现的时候代码流程不直观不好看,子类为了满足抽象契约被迫写一堆空实现。对于 View 相关的能力,用组合或者扩展函数往往比抽象类更灵活。
8.3.2 优缺点
面向对象的优点
- 建模直观——现实世界就是对象,User、Message、Chat、Contact,脑袋里好映射
- 复用——继承和组合让公共逻辑写一次
- 扩展——多态让新类型丢进去不丢老代码
- 维护——封装让改动范围可控,改一个类的心里有底
不用 OOP 有什么不好
- C 风格的过程式:一堆函数操作一堆结构体,谁都能改谁都敢改,没有边界
- 全局变量满天飞,改一个变量不知道影响谁
- 加功能 = 复制粘贴 + 到处 if-else,三个月后人就不认识了
继承的缺点
- 耦合:子类依赖父类实现细节,父类改一个字段,子类全崩
- 脆弱基类:父类觉得自己的方法子类不需要,删了——结果子类偷偷在用
- 继承层次深:TabPage → ChatTabPage → ChatTabPageV2 → V3,四层下去你想改 V1 的逻辑,中间 V2 怎么办?继承层级尽量浅,深了就该考虑组合替代
- 容易被子类篡改:protected 方法被子类乱 override,父类的行为不可控,复杂情况下看不出程序走向造成失败
- 单继承限制:Java 只能继承一个类,继承了 A 就没法继承 B
- 编译时绑定:继承关系在编译期就定死了,运行时不能换爹
- 类膨胀:组合替代继承后,Feature 类会变多
怎么解决继承的缺点
| 方案 | 怎么做 |
| 组合替代继承 | TabPage 持有 Feature 列表,要什么能力加什么 Feature,不要就不加 |
| 接口替代抽象类 | 用接口定义能力,默认实现用 extension/default method,不硬绑定继承链 |
| 委托模式 | 不继承,持有对象并转发调用,想换实现随时换 |
| 模板方法谨慎用 | 父类只定流程骨架,hook 方法尽量少,子类只填关键步骤 |
| 泛型优化 | 有缺点的业务场景用泛型约束类型,避免继承链过长 |
| 设计模式优化 | 策略模式替代继承中的算法变化,状态模式替代继承中的状态变化 |
| 接口代替继承 | 部分场景直接用接口定义行为,不需要抽象类的默认实现 |
类膨胀怎么解决
组合替代继承后,Feature 类会变多——BadgeFeature、PreloadFeature、SkinFeature……
| 手段 | 说明 |
| Feature 注册表 | 用 Map<String, Feature> 管理,按需取,不需要的 Tab 不注册 |
| Feature 工厂 | 根据 Tab 类型从工厂取 Feature 组合,而不是每个 Tab 写一套 |
| 默认实现 | 大部分 Feature 行为一样,提供 DefaultBadgeFeature,只有特殊 Tab override |
| 消灭空实现 | 如果一个 Feature 80% 的子类都是空实现,说明这个抽象没意义,降级为可选接口 |
8.3.3 重写 vs 重载
| 重写 (Override) | 重载 (Overload) | |
| 是什么 | 子类重新实现父类的方法 | 同一个类里方法名相同、参数不同 |
| 签名 | 必须完全一致(方法名 + 参数 + 返回值) | 方法名相同,参数类型/个数/顺序不同 |
| 返回值 | 相同或是协变返回类型(子类) | 随意,跟重载无关 |
| 访问修饰符 | 不能比父类更严格 | 随意 |
| 异常 | 只能抛父类声明的异常或其子类 | 随意 |
| 绑定时机 | 运行时(多态) | 编译时(静态) |
| 注解 |
| 无特殊注解 |
// 重写:子类改了父类的行为
class TabBarController {
void onTabChanged(String tabId) { /* 通用逻辑 */ }
}
class CustomTabBarController extends TabBarController {
@Override
void onTabChanged(String tabId) { /* 特殊逻辑 */ }
}
// 重载:同一个类,同名不同参
class BadgeManager {
void showBadge(String tabId) { ... } // 只传 tabId
void showBadge(String tabId, BadgeInfo info) { ... } // 传 tabId + 数据
void showBadge(String tabId, int num, String text) { ... } // 传详细参数
}
坑:重载是编译时绑定的,别以为重载能实现多态——它不能。多态靠重写。
8.4 设计原则(SOLID + 实战扩展)
开发过程中应以实际业务需求为中心,大部分时候需要做到:类职责单一、结构清晰、高内聚低耦合、细的颗粒度、灵活、高的可拓展性、封装变化、能稳健的迭代功能、低成本维护、小的包体积。
使用面向对象语言的特点,让软件达到抽象和细的颗粒度的效果。
SOLID 六大原则速查:
| 原则 | 一句话 | 违反了会怎样 |
| SRP 单一职责 | 一个类只负责一种功能 | 改一个功能改三个类 |
| OCP 开闭 | 加功能不改老代码 | 加类型改工厂 |
| LSP 里氏替换 | 子类能替换父类,行为不变 | 程序跑着跑着炸了 |
| DIP 依赖倒置 | 细节依赖抽象,别反过来 | 换实现要改上层 |
| ISP 接口隔离 | 大接口拆小接口 | 实现类一堆空方法 |
| LOD 迪米特 | 只跟直接朋友说话 | 改个字段连锁反应 |
8.4.1 术语
| 术语 | 定义 |
| 抽象 | 接口或抽象类——定义"能做什么",不定义"怎么做" |
| 细节 | 实现类——实现接口或继承抽象类而产生的类 |
| 高层模块 | 调用端——发起调用的那一层 |
| 底层模块 | 具体实现类——被调用的那一层 |
理解这四个术语是读懂 SOLID 的前提——尤其 DIP 里说的"高层不依赖底层,都依赖抽象",搞清楚谁是高层谁是底层才不会晕。
8.4.2 单一职责原则(SRP)
定义:一个类,应该仅有一个引起它变化的原因。一个类应该是一组相关性很高的函数、数据封装。
实现方式:一个类只负责一种类型的功能。
为什么要用:
项目需要不断升级以满足用户需求,升级过程中需要保证系统的稳定性和灵活性。随着功能增多,类会越来越庞大,功能和代码都很复杂,修改的时候不好找改动点。如果耦合性太高,回归验证的时候测试范围会增大。因此需要考虑代码结构的可拓展性、灵活性、降低功能类耦合性。
违反 SRP 的信号:
| 信号 | 说明 |
| 一个类有多个原因变 | 既改 UI 逻辑又改数据逻辑,应该拆 |
| 一个类超过 1000 行 | 大概率职责不单一 |
| 改一个功能要改同一个类的多处 | 职责交织了 |
| 类名带 Manager/Util/Helper | 这类名字通常是职责不单一的信号 |
8.4.3 开闭原则(OCP)
定义:软件中的对象(类、模块、函数等)应该对于拓展是开放的,对于修改是封闭的。
实现方式:将公共方法抽象成一个接口功能类,子类再去多种多样的拓展实现。已存在的实现类对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。
为什么要用:
程序一旦开发完成,一个类的实现只应该因错误修改。新的或者改变的特性应该通过新建不同的类实现,新建的类可以通过继承的方式来重用原类的代码。
实操要点:
- 不是绝对不改老代码——bug 当然要改,OCP 是说加功能尽量不改老代码
- 抽象是关键——接口/抽象类稳定,实现类可替换,加功能加实现类
- 变化点预判——哪里可能变,就在哪里留扩展点;确定不变的别过度抽象
8.4.4 里氏替换原则(LSP)
定义:所有引用基类的地方必须能透明地使用其子类的对象。
实现方法:子类可以替换父类(抽象)。通过抽象建立规范,具体的实现在运行时替换掉抽象。
为什么要用:
优点:
- 代码重用,减少创建类的成本,每个子类都引用父类的方法和属性
- 子类与父类基本相似但与父类有所区别
- 提高代码的可拓展性
代价:
- 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法
- 可能造成子类代码冗余,降低灵活性
实例:TextView 和 ImageView 可以替换 View
// 窗口类
public class Window {
public void show(View child) {
child.draw();
}
}
// 建立视图抽象,测量视图的宽高为公用代码,绘制实现交给具体的子类
public abstract class View {
public abstract void draw();
public void measure(int width, int height) {
// 测量视图大小
}
}
// 文本控件类的具体实现
public class TextView extends View {
public void draw() {
// 绘制文本
}
}
// ImageView 的具体实现
public class ImageView extends View {
public void draw() {
// 绘制图片
}
}
// Window 不关心传入的是 TextView 还是 ImageView
// 只要知道它是 View 就能调用 draw()——这就是里氏替换
违反 LSP 的典型表现:
- 子类 override 了父类方法但返回了不同的行为(父类说返回正数,子类返回负数)
- 子类抛出了父类没声明的异常
- 子类的方法是空实现(我不需要这个功能但被迫继承)→ 应该用接口隔离
8.4.5 依赖倒置原则(DIP)
定义:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
实现方法:
- 细节应依赖于抽象。面向接口编程,面向抽象编程
- 高层模块不应该依赖低层模块,两者都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖抽象
为什么要用:
解耦、高可拓展性。如果类与类依赖于细节,之间就会存在直接耦合,当具体实现需要变化时,意味着要同时修改依赖者的代码,限制系统的可拓展性。
实例:Mother 读任何内容
// 依赖倒置原则:要依赖抽象,不要依赖于细节。针对接口编程,不针对实现编程
public interface IReader {
public abstract void getContent();
}
public class Book implements IReader {
@Override
public void getContent() {
System.out.println("Reading book");
}
}
public class NewsPaper implements IReader {
@Override
public void getContent() {
System.out.println("Reading newspaper");
}
}
public class Mother {
void read(IReader reader) {
reader.getContent();
}
}
public class Main {
public static void main(String[] args) {
Mother mother = new Mother();
mother.read(new Book());
mother.read(new NewsPaper());
// 未来加 Magazine?实现 IReader 即可,Mother 不用改
}
}
DIP 在客户端的体现:
- BadgeManager 依赖 IBadgeProvider(接口),不依赖 CdpBadgeProvider(实现)
- TabLauncher 依赖 ITabPreload(接口),不依赖具体的 PreloadImpl
- Activity 依赖 ViewModel(抽象),不依赖 Repository(实现)
8.4.6 接口隔离原则(ISP)
定义:客户端不应该依赖它不需要的接口,类之间的依赖关系应该建立在最小的接口上。
实现方法:将接口颗粒化。将非常庞大、臃肿的接口拆分成更小的更具体的接口。
为什么要用:
- 让客户端依赖的接口尽可能的小,使用方只需要他们使用的接口
- 系统解开耦合,从而更容易重构、更改和重新部署
违反 ISP 的典型表现:
- 实现类有大量空方法(被迫实现接口里不需要的方法)
- 一个接口的方法超过 7 个,可以考虑拆
- 调用方只用了接口 3 个方法中的 1 个,但被迫依赖整个接口
ISP vs SRP:
- SRP 是类的维度——一个类只负责一件事
- ISP 是接口的维度——一个接口只暴露调用方需要的方法
- 两者经常一起出现:大类拆小类(SRP),大接口拆小接口(ISP)
8.4.7 迪米特原则(LOD)
定义:一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现与调用者或者依赖者没有关系,调用者或者依赖者只需要知道它需要的方法即可。
实现方法:调用者和依赖者只需要知道要调用的方法,不需要知道具体的细节。
为什么要用:低耦合,让各个类职责更加清晰,调用方无需感知到他不需要关注的细节。
迪米特的通俗理解:
- 只跟直接朋友说话,不跟陌生人说话
- 一个方法只应该调用:自己的字段、自己的方法、方法参数的方法、自己创建的对象的方法
- 不要链式调用:
a.getB().getC().doSomething()——你不需要知道 B 和 C 的存在
违反 LOD 的典型具现:
- 链式调用过深:
user.getAccount().getBadge().getCount()——改了 Badge 的结构,User 那边也要改 - 依赖了不需要知道的细节:调用方拿到了被调方的内部对象
修正方式:
// 违反 LOD
int count = user.getAccount().getBadge().getCount();
// 符合 LOD —— User 提供方法,调用方不需要知道 Badge
int count = user.getUnreadCount();
8.4.8 实战扩展原则
| 原则 | 一句话 |
| 组合复用原则 | 优先组合,少用继承 |
| KISS | 能简单就别复杂,过度设计比没设计还坑 |
| YAGNI | 你不会需要它——没来需求别提前搞 |
| DRY | 别重复自己,但提前抽象比重复更危险 |
SOLID 不是教条——一个 200 行的类不用拆成 5 个,一个 if-else 不用硬改成策略模式。SOLID 是方向,不是标准答案。
8.5 设计模式
代码设计的原因:良好的设计可以在开发前期就规避很多坑。
设计模式的代价:大部分时候使用设计模式都会增加类的数量,消耗内存。单例模式是个例外——它减少内存消耗。所以不要为了用模式而用模式,只在必要场景使用。
设计模式分三大类:创建型(怎么建对象)、结构型(怎么组合对象)、行为型(怎么协调对象)。23 种模式不用全记,记自己用过的 + 客户端常见的就够。
8.5.1 创建型
创建型关注对象的创建方式——怎么建、谁来建、什么时候建。
单例模式(常用)
数据存在于整个应用生命周期、多个地方使用。
特点:某个类在应用里只存在一个对象,减少内存消耗。类里面的全局变量跟随应用生命周期,也经常被拿来当应用的临时缓存类。
缺点:
- 持有 UI 相关代码或者 Context 容易内存泄漏(严重)——传给单例的 Context 最好是 Application Context
- 经常搭配观察者模式和异步回调,需要注意没有反注册导致的内存泄漏和数据并发问题
使用必要性:必要场景时可以使用,推荐静态内部类和 DCL 方式实现。
六种实现方式:
1. 饿汉式
系统加载这个类的时候就初始化静态全局变量,外部调用时已经且只会存在一个实例。
// 饿汉式:系统加载这个类时就被创建
// 线程安全(JVM 在类加载时保证不会初始化多个 static 对象)
public class SingleHungry {
private static SingleHungry singleHungry = new SingleHungry();
private SingleHungry() {}
public static SingleHungry getInstance() {
return singleHungry;
}
}
优点:稳,线程安全。类的生命周期:加载→验证→准备→解析→初始化→使用→卸载,singleHungry 在初始化阶段完成。
缺点:类加载时就创建对象,万一没用到就是浪费资源。(大型项目不建议,除非确认一定会用到)
2. 懒汉式
需要使用的时候再创建,通过 synchronized 保证多线程情况下单例对象的唯一性。
// 懒汉式:需要使用时再创建,节约资源
// 加入 synchronized 变得线程安全
public class SingleLazy {
private static SingleLazy singleLazy = null;
private SingleLazy() {}
public synchronized static SingleLazy getInstance() {
if (singleLazy == null) {
singleLazy = new SingleLazy();
}
return singleLazy;
}
}
优点:随用随建,不浪费资源。
缺点:每次调用 getInstance 都会 synchronized,造成不必要的同步开销。第一次加载时才实例化,慢。
3. 双检索 DCL(推荐)
懒汉模式上再加一层判空,只有第一次才会 synchronized。
// DCL:只有第一次使用 synchronized,减少同步开销
// volatile 防止指令重排序
public class SingleTonDoubleCheck {
private volatile static SingleTonDoubleCheck singleTon = null;
private SingleTonDoubleCheck() {}
public static SingleTonDoubleCheck getInstance() {
if (singleTon == null) { // 第一次检查
synchronized (SingleTonDoubleCheck.class) {
if (singleTon == null) { // 第二次检查
singleTon = new SingleTonDoubleCheck();
}
}
}
return singleTon;
}
}
为什么要 volatile:singleTon = new SingleTonDoubleCheck() 不是原子操作,可能只分配地址但不初始化,导致其他线程拿到的 singleTon 不为 null 但还没初始化完。volatile 保证分配空间的顺序不变。
缺点:第一次加载还是有点慢;volatile 比较重。不过这种 case 概率极小,如果不是复杂场景或 JDK 6 以下,普通 DCL 就够用。
4. 静态内部类(推荐)
// 静态内部类:使用到内部类时才加载,不用不加载,节约空间
// 不用加锁就能达到线程同步(JVM 保证单线程访问)
public class SingleTonHolder {
private static class SingleTonInnerHolder {
private static SingleTonHolder instance = new SingleTonHolder();
}
private SingleTonHolder() {}
public static SingleTonHolder getInstance() {
return SingleTonInnerHolder.instance;
}
}
优点:不用加锁就线程安全,需要时才创建。
5. 枚举单例
public enum SingletonEnum {
INSTANCE;
public void doSomeThing() {
System.out.println("do something.");
}
}
Effective Java 推荐,防反射和序列化破坏。Android 中少用。
6. 容器方法实现单例
// 将多个单例统一管理,按 key 取
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
private SingletonManager() {}
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
适合系统级单例管理,比如 SystemServiceRegistry。
五种主流写法对比(容器方法属于系统级用法,日常开发较少使用):
| 写法 | 线程安全 | 懒加载 | 适用 |
| 饿汉 | ✅ | ❌ | 简单场景,初始化不耗时 |
| 懒汉+同步 | ✅ | ✅ | 性能差,不推荐 |
| DCL | ✅ | ✅ | 最常用,注意 volatile |
| 静态内部类 | ✅ | ✅ | 最优雅,推荐 |
| 枚举 | ✅ | ❌ | Effective Java 推荐,Android 少用 |
其他坑:
- 多进程?每个进程一份,不是真单例
- 序列化/反序列化会破坏单例 → readResolve()
- 反射可以破坏单例 → 枚举防住
Android 实例:Application → 全局生命周期单例、InputMethodManager.getInstance() → 系统服务单例、LayoutInflater
构造者模式(Builder)(常用)
AlertDialog、自定义View封装可变参数、Glide、OkHttp。
特点:封装构建复杂对象的过程,对外隐藏构建细节。链式调用,每个 setter 方法都返回自身。
使用场景:作为配置类的构建器将配置的构建和表示分离开。
标准实现(含 Director):
// Builder 抽象出具体步骤
public abstract class Builder {
public abstract void buildBoard(String board);
public abstract void buildDisplay(String display);
public abstract void buildOs();
public abstract Computer create();
}
// 具体 Builder
public class MacbookBuilder extends Builder {
private Computer mComputer = new Macbook();
@Override
public void buildBoard(String board) { mComputer.setBoard(board); }
@Override
public void buildDisplay(String display) { mComputer.setDisplay(display); }
@Override
public void buildOs() { mComputer.setOs(); }
@Override
public Computer create() { return mComputer; }
}
// Computer 抽象数据模型
public abstract class Computer {
protected String mBoard;
protected String mDisplay;
protected String mOs;
public void setBoard(String board) { this.mBoard = board; }
public void setDisplay(String display) { this.mDisplay = display; }
public abstract void setOs();
}
// Director 封装构建过程,对外隐藏构建细节
public class Director {
Builder mBuilder = null;
public Director(Builder builder) { mBuilder = builder; }
public void construct(String board, String display) {
mBuilder.buildBoard(board);
mBuilder.buildDisplay(display);
mBuilder.setOs();
}
}
// 使用
Builder builder = new MacbookBuilder();
Director director = new Director(builder);
director.construct("英特尔主板", "Retina显示器");
Computer computer = builder.create();
简化版(实战更常用):不通过 Director 代理,直接用 Builder 链式调用,每个 setter 返回 this。
// 简化版:省掉 Director,Builder 直接链式调用
Computer computer = new Computer.Builder()
.board("英特尔主板")
.display("Retina显示器")
.os("Mac OS X")
.build();
Android 实例:AlertDialog.Builder、Glide.with().load().into()、OkHttp Request.Builder().url().build()
什么时候用 Builder:
- 参数 > 4 个
- 很多参数可选
- 参数之间有约束关系(比如设了A必须设B)
- 构建过程需要多步
工厂方法
创建哪种对象由子类决定。BitmapFactory。
Android 实例:
- BitmapFactory.decodeResource() / decodeFile() / decodeStream()
根据来源创建 Bitmap,调用方不需要知道怎么解析
- LayoutInflater.from(context)
根据系统版本返回不同的 LayoutInflater 实现
- ViewModelProvider.Factory
根据 ViewModel 类型创建不同实例
工厂方法 vs 简单工厂 vs 抽象工厂:
| 简单工厂 | 工厂方法 | 抽象工厂 | |
| 谁决定创建什么 | 一个工厂类 switch | 子类决定 | 一族相关对象 |
| 开闭原则 | 不满足(加类型改工厂) | 满足(加子类) | 满足 |
| 复杂度 | 低 | 中 | 高 |
| 适用 | 类型少且稳定 | 类型会扩展 | 需要创建一族对象 |
原型模式(常用)
clone、Intent深拷贝。
特点:clone 方法。当 new 对象消耗资源太大的时候可以使用原型模式节省 new Object 开销,保护性拷贝防止其他多个对象修改到原始对象的原始值。
浅拷贝 vs 深拷贝:
- 浅拷贝:复制对象本身,但共享内部引用的对象。引用类型的成员变量只复制地址,原始对象和拷贝对象仍然指向同一个对象
- 深拷贝:不仅复制对象本身,还会递归复制对象内部引用的所有对象。修改拷贝对象不会影响原始对象
// implements Cloneable,override clone
public class WordDocument implements Cloneable {
private String mText;
private ArrayList<String> mImages = new ArrayList<String>();
@Override
protected WordDocument clone() {
WordDocument doc = null;
try {
doc = (WordDocument) super.clone();
doc.mText = this.mText; // String 类型浅拷贝即可
doc.mImages = (ArrayList<String>) this.mImages.clone(); // 引用类型需要深拷贝
return doc;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
注意:
- 通过 clone 拷贝对象时不会执行构造函数
- 如果 ArrayList 里存的是 Object 而不是 String,浅拷贝 mImages 列表后,修改 mImages.get(0).属性 仍然会影响原始对象——因为列表里的对象还是同一份引用
什么时候用原型:
- 创建对象成本高(数据库查询、网络请求的结果)
- 需要保存对象快照(撤销/恢复场景)
- 大量相似对象(改一点点就用)
Android 实例:Intent.clone()、Bundle.clone()
8.5.2 结构型
结构型关注对象的组合方式——怎么把不兼容的接口接起来、怎么控制访问、怎么简化调用。
适配器模式(常用)
RecyclerView。
核心:两个不兼容的接口要对接,中间加一层适配。
Android 实例:
- RecyclerView.Adapter → 数据模型适配成 ViewHolder
数据是 List<Message>,展示是 ItemView,Adapter 就是中间的适配层
- ArrayAdapter → 数组适配成 ListView Item
- FragmentPagerAdapter → Fragment 适配成 ViewPager 页
三种适配器:
| 类型 | 怎么做 | 适用 |
| 类适配器 | 继承被适配类 + 实现目标接口 | Java 不推荐(单继承) |
| 对象适配器 | 持有被适配对象 + 实现目标接口 | 最常用 ✅ |
| 接口适配器 | 抽象类空实现目标接口,子类按需 override | 接口方法太多时 |
适配器 vs 外观:适配器是让 A 能调 B;外观是让调用 A 更简单。
代理模式(常用)
AIDL。
核心:控制访问或延迟加载,不直接访问目标对象。
Android 实例:
- AIDL → 客户端持有的是 Proxy 对象,不是真正的 Service
Proxy 把方法调用序列化 → 跨进程传给 Service → 拿回结果反序列化
调用方完全不知道是本地调用还是远程调用
- ActivityManagerProxy → 系统 Service 的代理
- Binder → Android IPC 的代理模式实现
三种代理:
| 类型 | 怎么做 | 适用 |
| 静态代理 | 手写代理类 | 代理逻辑固定、代理对象少 |
| 动态代理 | 运行时生成代理类(Proxy.newProxyInstance)+ 反射机制 | 代理对象多、代理逻辑通用 |
| 虚代理 | 先返回占位对象,真正用到才加载 | 延迟初始化、大对象加载 |
AIDL 就是静态代理 + 虚代理的结合:Proxy 代理了远程调用(静态),Stub 提供了占位(虚)。
外观模式(常用)
音视频SDK、MediaPlayer、Context。
特点:将复杂子系统封装为一层接口,隐藏具体实现,做 SDK 的时候经常用到。
注意:设计接口的时候参数尽量低耦合性。
Android 实例:
- MediaPlayer → 内部涉及 AudioFlinger、AudioTrack、解码器、缓冲区管理
调用方只需要 setDataSource() → prepare() → start()
- Context → 内部涉及 PackageManager、ResourceManager、SharedPreferences、ActivityManager
调用方只需要 context.startActivity() / context.getString() / context.getSharedPreferences()
- 音视频SDK → 内部涉及编解码、渲染、音频焦点、网络拉流
调用方只需要 sdk.play(url) / sdk.stop()
外观 vs 适配器:
- 适配器:A和B不兼容,中间加一层让它们能对话
- 外观:A想用B,但B太复杂,中间加一层让A更好用
装饰器模式
动态加功能,不改原类。
Android 实例:
- InputStream → FileInputStream → BufferedInputStream → DataInputStream
一层包一层,每层加一个功能
- Context → ContextWrapper → Activity / Service / Application
ContextWrapper 装饰了 Context,加了一层委托
- RecyclerView.ItemDecoration → 给 Item 加装饰(分割线、间距)
装饰器 vs 代理:
- 代理:控制访问,调用方不知道真实对象存在
- 装饰器:加功能,调用方知道被装饰对象存在
8.5.3 行为型
行为型关注对象之间的通信——谁通知谁、谁处理谁、状态怎么变、流程怎么走。
观察者模式(常用)
等待数据、被动通知、一对多通知数据、长链接消息、广播、EventBus、MVVM、adapter.notifyDataSetChanged()。
这是客户端用得最多的模式,没有之一。
特点:解耦。一对多消息通知。
缺点:容易被某个观察者卡住通知发不出去。
推模式 vs 拉模式:
推模式——不管观察者是否需要,推送信息通常是主题对象的全部或部分数据通过 update 的参数传递给观察者:
// 推模式:被观察者把所有数据推给观察者
public interface Observer {
void update();
}
public class LaoWang implements Observer {
WeatherStation weatherStation;
public LaoWang(WeatherStation weatherStation) {
this.weatherStation = weatherStation;
}
@Override
public void update() {
if (weatherStation.getTemperature() < 0) {
System.out.println("老王穿上了羽绒服!");
}
}
}
public class XiaoLi implements Observer {
WeatherStation weatherStation;
public XiaoLi(WeatherStation weatherStation) {
this.weatherStation = weatherStation;
}
@Override
public void update() {
if (weatherStation.getDampaness() > 50) {
System.out.println("小李打开空调,开始除湿!");
}
}
}
// 被观察者
public class WeatherStation implements WeatherSubject {
List<Observer> observers = new ArrayList<>();
private int temperature = 0;
private int dampaness = 0;
@Override
public void registerObserver(Observer o) { observers.add(o); }
@Override
public void removeObserver(Observer o) { observers.remove(o); }
@Override
public void notifyObserver() {
for (Observer o : observers) { o.update(); } // 推:直接调 update
}
// setter 里调 notifyObserver
}
拉模式(更好)——被观察者在通知时只传递少量信息(比如自身引用),观察者按需主动拉取:
// 拉模式:观察者按需取数据
public interface Observer {
void update(WeatherStation weatherStation); // 传被观察者引用
}
public class LaoWang implements Observer {
@Override
public void update(WeatherStation weatherStation) {
if (weatherStation.getTemperature() < 0) {
System.out.println("老王穿上了羽绒服");
}
}
}
public class XiaoLi implements Observer {
@Override
public void update(WeatherStation weatherStation) {
if (weatherStation.getDampaness() > 50) {
System.out.println("小李打开空调,开始除湿!");
}
}
}
// 被观察者
@Override
public void notifyObserver() {
for (Observer o : observers) {
o.update(this); // 拉:传自身引用,观察者按需取
}
}
观察者的坑:
- 内存泄漏——忘记反注册 → 用 WeakReference 或 Lifecycle 感知
- 事件风暴——一个变化触发 N 个观察者,N 个又各触发 M 个
- 顺序依赖——观察者 A 和 B 的执行顺序不确定
- 容易被某个观察者卡住通知发不出去——某个观察者的 update 里有耗时操作或异常,后面的观察者收不到通知
单例 + 观察者 + 异步回调的并发陷阱:
单例里注册观察者、反注册观察者、消息通知,且消息通知时去刷 UI,需要关注:
- View 强引用问题——一般可用 WeakReference 解决
- 数据并发问题——修改数据后,多个观察者再获取数据时如果 case 比较复杂容易出现并发问题,读脏数据。需要处理好数据同步问题:加锁 / 深拷贝 / final 传参 / ConcurrentArrayList / ConcurrentHashMap / ConcurrentLinkedQueue
Android 实例:BroadcastReceiver、EventBus、LiveData/Flow、ContentObserver、RecyclerView.Adapter.notifyDataSetChanged()、长链接消息推送
责任链模式
类加载双亲委托机制、事件分发、OA系统流程单流转。
特点:请求者和处理者关系解耦。链式编程。
Android 实例:
- 事件分发 → ViewGroup.dispatchTouchEvent → onInterceptTouchEvent → child.dispatchTouchEvent
不处理就传给下一层,处理了就消费
- ClassLoader 双亲委托 → 先让父 ClassLoader 加载,父加载不了自己来
避免重复加载 + 安全(核心类不会被篡改)
- OkHttp Interceptor → Request 经历一系列拦截器:Retry → Bridge → Cache → Connect → CallServer
每个拦截器处理自己的事,处理完传给下一个
- 未读数展示链 → 产品未读 → 疲劳度检查 → 营销未读 → 最终展示
- 有序广播 → 按优先级传递,可拦截
责任链 vs 观察者:
- 责任链:一个请求,沿链传,被一个处理者消费
- 观察者:一个事件,广播,所有观察者都收到
备忘录模式
备份状态。
特点:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复。
Android 实例:
- Activity 的 onSaveInstanceState 和 onRestoreInstanceState——系统在 Activity 可能被销毁前保存状态,恢复时还原
- TextField 的 Undo/Redo——每次输入保存一个快照,撤销时恢复
策略模式
切换不同的算法、LayoutManager。
核心:同一行为不同实现,运行时切换。
Android 实例:
- RecyclerView.LayoutManager → LinearLayoutManager / GridLayoutManager / StaggeredGridLayoutManager
同一个 RecyclerView,换 LayoutManager 就换布局方式
- 动画插值器 Interpolator → AccelerateInterpolator / DecelerateInterpolator / LinearInterpolator
同一个动画,换个插值器换缓动效果
- 未读数优先级策略 → 产品优先 / 营销优先 / 同级竞争
同一个展示逻辑,换策略换展示结果
策略 vs 状态:
- 策略:外部选择用哪个算法,算法之间独立
- 状态:内部状态决定用什么行为,状态之间有转换
模板方法模式(常用)
封装流程。
实现方法:把某个固定的流程按顺序封装到一个 final 函数中,并且让子类能够定制这个流程中的某些或者所有步骤细节,主体流程顺序又不会被打乱。
Android 实例:
- Activity 生命周期 → onCreate → onStart → onResume → onPause → onStop → onDestroy
Framework 定了流程,子类 override 各步骤
- AsyncTask → onPreExecute → doInBackground → onPostExecute
流程固定,子类填每步的实现
- ViewPager + PagerAdapter → instantiateItem / destroyItem / isViewFromObject
翻页流程固定,子类填怎么创建和销毁页面
- BaseFragment → onCreateView / initView / loadData / onLazyLoad
自定义模板:子类只关心填数据,不关心流程
模板方法 vs 策略:
- 模板方法:流程固定,步骤可变(继承)
- 策略:算法整体可换(组合)
- 想改流程用策略,想改步骤用模板方法
解释器模式
定义语法规则 + 解析(写框架会用到)、PackageManager 解析 AndroidManifest.xml。
特点:常用于简单语言单个表达式封装、解释。会生成大量的类。
Android 实例:
- PackageManager 解析 AndroidManifest.xml
定义了 <activity> <service> <receiver> <provider> 等语法规则
PM 解析这些标签 → 构建出 Component 信息
- SharedPreferences 解析 XML
定义了 <map> <string> <int> <boolean> 等语法规则
SP 解析这些标签 → 构建出键值对
- 路由框架解析 URI
scheme://host/path?param=value
路由框架定义了解析规则 → 解析出目标页面和参数
- 动态化框架
定义 JSON → UI 的语法规则
解释器解析 JSON → 渲染成视图树
什么时候用解释器:
- 你在写框架,需要定义自己的 DSL/配置语法
- 你在解析固定格式的配置文件
- 你在做规则引擎,业务逻辑以规则的形式下发
什么时候不用:
- 规则很少且简单(直接 if-else)
- 规则频繁变化(解释器改起来麻烦)
- 性能敏感(解释器比硬编码慢)
状态模式
行为随状态变化。
Android 实例:
- 音频焦点状态 → 持有焦点 / 丢失焦点 / 暂时失去焦点
不同状态不同播放行为,状态之间有转换规则
- 下载状态 → 等待 / 下载中 / 暂停 / 完成 / 失败
不同状态不同 UI 和操作
- 播放器状态 → Idle / Preparing / Playing / Paused / Stopped / Error
状态决定行为,转换有约束
状态模式 vs 策略模式:
- 策略:外部选算法,算法之间独立无转换
- 状态:状态决定行为,状态之间有转换规则
命令模式
操作封装,可撤销可排队。
Android 实例:
- Intent → 把"启动一个Activity"封装成对象
可以延迟执行、可以跨进程传递、可以被拦截
- Handler.post(Runnable) → 把"执行一段逻辑"封装成消息
可以延迟执行、可以取消、可以按顺序处理
- Thread_POOL.execute(Runnable) → 把"执行一个任务"封装成对象
可以排队、可以拒绝、可以管理
8.5.4 设计模式有什么用?平时用吗?
你一定在用,只是不知道叫什么名字。
- 写了个 Callback 接口?——观察者模式
- 写了个 Singleton?——单例模式
- 用了 AlertDialog.Builder?——构造者模式
- 用了 RecyclerView.Adapter?——适配器模式
- 用了 Intent 跳转?——命令模式
- 封装了一个统一入口来操作多个子系统?——外观模式
- 用了 LayoutInflater?——工厂方法
- Intent.clone()?——原型模式
- override Activity.onCreate?——模板方法
- 换 LayoutManager?——策略模式
- OkHttp Interceptor?——责任链
- AIDL 跨进程?——代理模式
- onSaveInstanceState?——备忘录模式
设计模式不是让你刻意去用的,是你在解决问题时自然用到的。 学设计模式是为了有意识地选择,而不是无意识地堆代码。
真正有用的不是模式本身,是模式背后的思想:
- 单例 → 全局唯一,共享状态
- 构造者 → 复杂对象分步构建,隐藏细节
- 工厂 → 把创建和使用分开
- 原型 → clone 比 new 省,保护性拷贝
- 适配器 → 不兼容的接口对接
- 代理 → 控制访问,延迟加载
- 外观 → 简化复杂系统,SDK 常用
- 观察者 → 解耦发送方和接收方
- 责任链 → 请求沿链处理
- 策略 → 把变化的部分抽出来
- 模板方法 → 流程固定步骤可变
- 备忘录 → 备份状态,可恢复
- 解释器 → 定义语法 + 解析执行
- 状态 → 行为随状态变化
- 命令 → 操作封装,可撤销
知道思想,用不用模式都行。不知道思想,用了模式也白搭。

:OOP、SOLID 与设计模式&spm=1001.2101.3001.5002&articleId=148036409&d=1&t=3&u=11f2e271bae445d38b26e648a352460e)
1213

被折叠的 条评论
为什么被折叠?



