客户端设计(中):OOP、SOLID 与设计模式

客户端架构:为什么、什么时候、怎么做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)

是什么

子类重新实现父类的方法

同一个类里方法名相同、参数不同

签名

必须完全一致(方法名 + 参数 + 返回值)

方法名相同,参数类型/个数/顺序不同

返回值

相同或是协变返回类型(子类)

随意,跟重载无关

访问修饰符

不能比父类更严格

随意

异常

只能抛父类声明的异常或其子类

随意

绑定时机

运行时(多态)

编译时(静态)

注解

@Override

无特殊注解

// 重写:子类改了父类的行为
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)

定义:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

实现方法

  1. 细节应依赖于抽象。面向接口编程,面向抽象编程
  2. 高层模块不应该依赖低层模块,两者都应该依赖于抽象
  3. 抽象不应该依赖于细节,细节应该依赖抽象

为什么要用

解耦、高可拓展性。如果类与类依赖于细节,之间就会存在直接耦合,当具体实现需要变化时,意味着要同时修改依赖者的代码,限制系统的可拓展性。

实例: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)

定义:客户端不应该依赖它不需要的接口,类之间的依赖关系应该建立在最小的接口上。

实现方法:将接口颗粒化。将非常庞大、臃肿的接口拆分成更小的更具体的接口。

为什么要用

  1. 让客户端依赖的接口尽可能的小,使用方只需要他们使用的接口
  2. 系统解开耦合,从而更容易重构、更改和重新部署

违反 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;
    }
}

为什么要 volatilesingleTon = 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,需要关注:

  1. View 强引用问题——一般可用 WeakReference 解决
  2. 数据并发问题——修改数据后,多个观察者再获取数据时如果 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 常用
  • 观察者 → 解耦发送方和接收方
  • 责任链 → 请求沿链处理
  • 策略 → 把变化的部分抽出来
  • 模板方法 → 流程固定步骤可变
  • 备忘录 → 备份状态,可恢复
  • 解释器 → 定义语法 + 解析执行
  • 状态 → 行为随状态变化
  • 命令 → 操作封装,可撤销

知道思想,用不用模式都行。不知道思想,用了模式也白搭。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值