Java 标准库/Android/Spring 库 中的设计模式应用:每个模式只记录一个典型案例

Java 标准库/Android/Spring 库之所以能成为无数应用的基石,除了功能完备之外,一个常被忽视却至关重要的原因在于:它的设计本身就是设计模式最经典的“活教材”。GoF 的 23 种模式并非纸上谈兵,在 java.lang、java.util、java.io 这些开发者每天打交道的包中,几乎都能找到它们的影子。下面从创建型、结构型、行为型三个维度,盘点那些藏在标准库(及主流平台库)源码中的设计模式,并在每个模式后附上调用方视角的代码——因为模式的价值,最终体现在调用方能够以多小的代价获得多大的灵活性。


创建型模式:控制对象的诞生方式

单例模式:java.lang.Runtime

每个 Java 应用都运行在一个 JVM 进程之上,Runtime 类正是这个进程的唯一句柄。它的实现方式是经典的饿汉式单例:

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();
    private Runtime() {}
    public static Runtime getRuntime() {
        return currentRuntime;
    }
}

私有构造器杜绝了外部实例化,getRuntime() 始终返回同一个 currentRuntime 引用。

调用方代码:

public class MemoryReporter {
    public static void main(String[] args) {
        Runtime rt1 = Runtime.getRuntime();
        Runtime rt2 = Runtime.getRuntime();
        System.out.println(rt1 == rt2);           // true

        long free = rt1.freeMemory();
        long total = rt1.totalMemory();
        System.out.println("used = " + (total - free) / 1024 / 1024 + " MB");

        rt1.addShutdownHook(new Thread(() -> System.out.println("bye")));
    }
}

调用方无法 new Runtime(),只能通过 getRuntime() 获取唯一实例。模式在调用方这里的体现就是:获取实例的入口唯一,且拿到的引用始终相同。

工厂模式:List.of() / Set.of() / Map.of()

标准库里静态工厂方法随处可见,而 List.of、Set.of、Map.of 是调用方每天都在用、却很少意识到“这是工厂”的一组设施。它们把“创建集合”从构造器调用变成了对接口的工厂调用:调用方只声明自己要一个 List、Set 或 Map,至于背后是哪个实现类、按元素个数选用哪种内部结构,全部由工厂方法决定。

// 调用方只面向接口,不出现 new ArrayList<>() / new HashSet<>() / new HashMap<>()
List<String> names = List.of("alice", "bob", "carol");
Set<Integer> ids = Set.of(1, 2, 3);
Map<String, Integer> scores = Map.of("alice", 90, "bob", 85);

// 变量类型可以是更抽象的父接口,具体实现对调用方完全透明
Collection<String> c = names;
System.out.println(c.contains("bob"));      // true
System.out.println(scores.get("alice"));    // 90

// 工厂方法按参数个数返回不同的内部实现
List<String> empty = List.of();             // 共享的空实例
List<String> one   = List.of("x");          // 针对 1~2 个元素的紧凑实现
List<String> many  = List.of("x", "y", "z");// 元素更多时的通用实现

// 不可变性也由工厂保证,调用方无需自己包一层
try {
    names.add("dave");                      // 抛 UnsupportedOperationException
} catch (UnsupportedOperationException e) {
    System.out.println("immutable");
}

如果打印 List.of()、List.of("x")、List.of("x","y","z") 的 getClass(),会看到不同的内部类名(如 ImmutableCollections$ListN、List12),但这些类并不在公开 API 中,属于实现细节——调用方拿到的只是 List 接口,具体类型由工厂内部选择。创建入口是 of(...) 这样的具名静态方法,返回类型是抽象接口,实现类的选择、实例的复用、不可变性的施加,全部封装在工厂方法内部。

抽象工厂模式:JAXP DocumentBuilderFactory

工厂方法关注“创建一个产品”,抽象工厂关注“创建一族相关产品”。JAXP 中的 DocumentBuilderFactory 正是后者:它本身是抽象类,newInstance() 根据系统属性或 SPI 返回具体工厂,再由这个工厂创建 DocumentBuilder、Document 等一整套 XML 处理对象。

// 调用方通过抽象工厂的静态入口获取具体工厂
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);

// 由工厂创建一族产品:DocumentBuilder 及其产出的 Document
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("pom.xml"));

// 同一族中的另一个产品:TransformerFactory → Transformer
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(System.out));

调用方代码中不出现任何具体实现类(如 com.sun.org.apache.xerces...),更换 XML 处理器只需调整系统属性或 classpath 中的 SPI 配置。一族产品的创建被收进同一个工厂,调用方只面向抽象工厂编程。

原型模式:Cloneable / Object.clone()

原型模式通过复制已有实例来创建新对象,而不是调用构造器。Java 用 Cloneable 标记接口和 Object.clone() 提供了语言级支持,ArrayList、HashMap 等集合类都实现了它。

ArrayList<String> original = new ArrayList<>(List.of("a", "b"));
@SuppressWarnings("unchecked")
ArrayList<String> copy = (ArrayList<String>) original.clone();

copy.add("c");
System.out.println(original);  // [a, b]
System.out.println(copy);      // [a, b, c]

// 数组同样支持 clone
int[] src = {1, 2, 3};
int[] dst = src.clone();
dst[0] = 99;
System.out.println(Arrays.toString(src));  // [1, 2, 3]

调用方用 clone() 得到新对象,无需知道 ArrayList 内部数组的扩容策略或 HashMap 的桶结构。复制的细节由被复制对象自己承担,调用方只声明“要一个副本”。需要注意 clone() 默认是浅拷贝,深层复制仍需子类覆写。

建造者模式:StringBuilder

StringBuilder 是一个流式建造者。append() 返回 this,使得多次链式调用可以逐步“建造”出最终的字符串,而建造过程本身与最终产品的表示分离。

String sql = new StringBuilder()
        .append("SELECT id, name FROM user")
        .append(" WHERE age > ").append(18)
        .append(" ORDER BY ").append("id")
        .toString();

// 条件化建造:每一步都可选、可跳过
StringBuilder sb = new StringBuilder();
sb.append("Hello");
if (Boolean.TRUE) sb.append(", World");
sb.append('!');
String msg = sb.toString();

调用方通过链式 append 逐步组装,最后 toString() 才产出产品。建造过程与最终对象分离,调用方掌控建造的顺序和内容。


结构型模式:组装与增强的艺术

装饰器模式:Java IO

java.io 包是装饰器模式最 unmistakable 的实现。FileInputStream 是被装饰的组件,BufferedInputStream 是具体装饰器,两者都继承自 InputStream,装饰器在持有被装饰对象的同时为其添加缓冲功能。

// 逐层包裹:字节文件 → 缓冲 → 解压 → 字符解码
try (Reader reader = new BufferedReader(
        new InputStreamReader(
            new GZIPInputStream(
                new FileInputStream("data.gz")),
            StandardCharsets.UTF_8))) {

    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

// 同一套代码换个装饰组合,能力就变了
InputStream in = new BufferedInputStream(new FileInputStream("a.bin"));

调用方通过嵌套构造叠加功能,而变量类型始终是顶层抽象 Reader / InputStream。每包一层就多一项能力,调用方的读取代码一行不改。FilterInputStream 作为抽象装饰器角色,将所有方法默认委托给被包装的流,子类只需覆写需要增强的方法。

适配器模式:Arrays.asList() / InputStreamReader

Arrays.asList() 把数组适配成 List 接口;InputStreamReader 把字节流的 InputStream 适配成字符流的 Reader。

// 数组 → List 接口
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);
System.out.println(list.contains("b"));   // true

// 字节流 → 字符流接口
try (Reader r = new InputStreamReader(
        new FileInputStream("notes.txt"), StandardCharsets.UTF_8)) {
    char[] buf = new char[256];
    int n = r.read(buf);
}

调用方想要的是 List 或 Reader 的能力,手上却只有数组或字节流。适配器让原本接口不匹配的两端在调用方这里无缝衔接。

桥接模式:JDBC

JDBC API 是桥接模式在平台级设计中的经典体现。java.sql.Driver、Connection、Statement、ResultSet 这些接口构成了抽象部分,各数据库厂商的驱动实现构成了实现部分。

Class.forName("org.postgresql.Driver");

try (Connection conn = DriverManager.getConnection(
        "jdbc:postgresql://localhost:5432/demo", "user", "pwd");
     PreparedStatement ps = conn.prepareStatement(
         "SELECT id, name FROM t WHERE id = ?")) {

    ps.setInt(1, 42);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getInt("id") + " " + rs.getString("name"));
        }
    }
}

换成 MySQL 只需改驱动类名和 URL,调用方代码一个字都不用动。抽象的 Connection/Statement 与具体数据库实现各自独立演化。

组合模式:Android View / ViewGroup

Android 的视图体系是组合模式在移动端的标准实现:ViewGroup 继承自 View,因此它既是一个可被绘制的“叶子”式组件,又是一个可以容纳其他 View 的“树枝”式容器。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);

        TextView title = new TextView(this);
        title.setText("Settings");

        LinearLayout row = new LinearLayout(this);   // ViewGroup 也是 View
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.addView(new Button(this));
        row.addView(new Switch(this));

        root.addView(title);   // 叶子
        root.addView(row);     // 树枝

        setContentView(root);

        // 统一操作:无需区分叶子与容器
        root.setVisibility(View.VISIBLE);
        root.setAlpha(0.95f);
        root.setOnClickListener(v -> Log.d("demo", "root clicked"));
    }
}

addView(View child) 的参数类型是 View,而 LinearLayout、FrameLayout、RecyclerView 等容器本身也是 View。调用方对 root 调用 setVisibility、setAlpha、setOnClickListener 时,既作用于单个组件,也能递归作用于整棵子树。叶子与树枝共享同一接口,调用方不需要为“单个组件”和“组件集合”写两套逻辑。

外观模式:Spring JdbcTemplate

JdbcTemplate 是 Spring 对 JDBC 子系统的门面。获取连接、创建 Statement、执行 SQL、遍历 ResultSet、处理 SQLException、释放资源这一整套流程,在原生 JDBC 中需要调用方逐步完成;JdbcTemplate 把这些步骤收进一个高层接口。

@Repository
public class UserDao {

    private final JdbcTemplate jdbc;

    public UserDao(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    // 调用方不出现 Connection / PreparedStatement / ResultSet / try-finally
    public List<String> findNamesOlderThan(int age) {
        String sql = "SELECT name FROM user WHERE age > ?";
        return jdbc.queryForList(sql, String.class, age);
    }

    public int updateEmail(int id, String email) {
        String sql = "UPDATE user SET email = ? WHERE id = ?";
        return jdbc.update(sql, email, id);
    }

    public User findById(int id) {
        String sql = "SELECT id, name, email FROM user WHERE id = ?";
        return jdbc.queryForObject(sql, (rs, rowNum) ->
                new User(rs.getInt("id"), rs.getString("name"), rs.getString("email")),
                id);
    }
}

对比原生 JDBC:调用方需要 getConnection()、prepareStatement()、setInt()、rs.next()、while 循环、try-catch-finally 关闭资源,还要处理受检的 SQLException。JdbcTemplate 把所有这些“脏活”挡在门面之后,调用方代码里只出现 SQL 和参数。子系统越复杂,外观模式在调用方一侧的收益越明显。

享元模式:Android Message.obtain()

Android 的 Message 类在文档中明确建议:虽然构造器是公开的,但获取 Message 实例的最佳方式是从对象池中“obtain”一个,而不是 new。Message.obtain() 内部维护了一个消息池,已经用过的 Message 在回收后可以再次取出、重置字段后复用。

public class PlaybackService extends Service {

    private static final int PLAY = 1;
    private static final int STOP = 2;

    private final Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case PLAY:  startPlayback();  break;
                case STOP:  stopPlayback();   break;
            }
        }
    };

    void requestPlay() {
        Message msg = Message.obtain();       // 来自全局消息池
        msg.what = PLAY;
        msg.obj = "track-001";
        handler.sendMessage(msg);
    }

    void requestStop() {
        Message msg = Message.obtain(handler, STOP);
        handler.sendMessage(msg);
    }
}

调用方写的是 Message.obtain(),而不是 new Message()。对象池、回收、重置这些享元逻辑全部由 Message 内部完成,调用方只负责“设置本次需要变化的字段”然后发送。池中的对象可以被反复借出和归还,但调用方看不到池,只看到一个轻量的 obtain() 入口。

代理模式:java.lang.reflect.Proxy

JDK 动态代理是代理模式在语言层面的支持。Proxy.newProxyInstance() 在运行时生成一个实现指定接口的代理对象,所有方法调用都被转发到 InvocationHandler.invoke()。

interface UserService {
    String find(int id);
    void save(String name);
}

UserService target = new UserService() {
    public String find(int id) { return "user-" + id; }
    public void save(String name) { System.out.println("saved " + name); }
};

InvocationHandler handler = (proxy, method, args) -> {
    long t0 = System.nanoTime();
    try {
        return method.invoke(target, args);          // 转发给真实对象
    } finally {
        System.out.printf("%s took %d ns%n",
                method.getName(), System.nanoTime() - t0);
    }
};

UserService proxy = (UserService) Proxy.newProxyInstance(
        UserService.class.getClassLoader(),
        new Class<?>[]{UserService.class},
        handler);

proxy.find(7);        // 调用方写的是接口方法,实际经过代理增强
proxy.save("alice");

调用方拿到的变量类型是接口,但运行期是生成的代理类。增强逻辑由 handler 统一插入,调用方代码保持原样。这一机制后来成为 Spring AOP 的底层基础之一。


行为型模式:算法与职责的分配

策略模式:Comparator

Comparator 是标准库中最清晰的策略接口。Arrays.sort(T[], Comparator) 将排序算法的可变部分——比较规则——抽取为一个独立对象。

String[] names = {"bob", "Alice", "carol", "Dave"};

// 同一份排序骨架,传入不同策略
Arrays.sort(names);                                        // 自然序
Arrays.sort(names, Comparator.reverseOrder());             // 逆序
Arrays.sort(names, String.CASE_INSENSITIVE_ORDER);         // 忽略大小写
Arrays.sort(names, Comparator.comparing(String::length));  // 按长度

// 策略对象还可交给有序映射长期持有
Map<String, Integer> map = new TreeMap<>(Comparator.reverseOrder());
map.put("a", 1);
map.put("b", 2);
System.out.println(map.keySet());   // [b, a]

调用方每次只是替换传入的比较器对象,排序算法本身固定不变。比较规则与排序过程在调用点完成解耦。

模板方法模式:AbstractList

AbstractList 定义了集合操作的算法骨架。以 addAll(int index, Collection c) 为例,它规定了“检查索引 → 遍历集合 → 逐个调用 add(index, e)”的固定流程,但 add(int, E) 本身留为抽象方法,由子类提供具体实现。

// 自定义容器:只实现 get / size / add(int,E),其余骨架由 AbstractList 提供
public class FixedList<E> extends AbstractList<E> {
    private final Object[] data;
    private int size = 0;

    public FixedList(int capacity) { data = new Object[capacity]; }

    @Override public E get(int index) { return (E) data[index]; }
    @Override public int size() { return size; }
    @Override public void add(int index, E e) { data[index] = e; size++; }
}

// 调用方:addAll 的"检查索引→逐个插入"骨架来自 AbstractList
List<String> list = new FixedList<>(8);
list.addAll(0, List.of("a", "b", "c"));
System.out.println(list);   // [a, b, c]

调用方享用的是 AbstractList 已固化的算法流程,子类只填空缺步骤。调用点看不到流程,但流程被模板牢牢约束。

解释器模式:java.util.regex.Pattern / Matcher

解释器模式定义一种语言,用语法树表示其中的句子,并提供一个解释器来执行这些句子。标准库中最典型的实现就是正则表达式:Pattern.compile() 将正则表达式解析为内部 Pattern.Node 树(如 GroupCurly、CharProperty、Branch 等节点),Matcher 遍历这棵树对输入进行解释匹配。

// 调用方用 compile 构建"解释器",用 matcher 执行"解释"
Pattern pattern = Pattern.compile("(\\d{3})-(\\d{4})");
Matcher matcher = pattern.matcher("Call 555-1234 or 666-7890 now");

while (matcher.find()) {
    System.out.println("area=" + matcher.group(1)
            + ", number=" + matcher.group(2));
}

// 同一棵语法树可以反复用于不同输入
Matcher m2 = pattern.matcher("Fax: 111-2222");
System.out.println(m2.matches());   // false
System.out.println(m2.find());      // true

调用方看不到 Pattern.Node 这棵语法树,只面对 compile / matcher / find / group 这几个概念。语法规则与解释执行被封装在 Pattern/Matcher 内部,调用方只负责提供表达式和输入。java.text.DateFormat/SimpleDateFormat 对日期格式串的解释,javax.script 引擎对脚本语言的解释,也都属于同一模式在不同层次上的体现。

责任链模式:java.util.logging / Servlet FilterChain

责任链让多个处理者依次有机会处理请求,处理者之间通过“下一个”引用串联。java.util.logging.Logger 在记录日志时,会先交给自己的 Handler,再沿父 Logger 的层次向上传递;Servlet 的 FilterChain 则把请求依次交给每个 Filter。

// java.util.logging:Handler 链 + 父 Logger 链
Logger logger = Logger.getLogger("com.example");
logger.addHandler(new ConsoleHandler());
logger.addHandler(new FileHandler("app.log"));
logger.info("hello");   // 消息沿 Handler 链和父 Logger 传递

// Servlet FilterChain:每个 Filter 决定是否继续传递
public class AuthFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        if (!authenticated(req)) {
            ((HttpServletResponse) res).sendError(401);
            return;                 // 中断链
        }
        chain.doFilter(req, res);   // 交给下一个 Filter
    }
}

调用方只需注册处理者、发起一次请求,请求沿链传递的过程由链本身驱动,调用方不关心链上有几个节点、谁先谁后。

备忘录模式:java.sql.Savepoint

备忘录模式在不破坏封装的前提下保存对象内部状态,以便之后恢复。JDBC 的 Savepoint 是标准库中最直接的实现:setSavepoint() 记录事务当前状态,rollback(Savepoint) 恢复到该状态。

try (Connection conn = dataSource.getConnection()) {
    conn.setAutoCommit(false);

    Savepoint before = conn.setSavepoint();   // 保存当前状态(备忘录)
    try {
        conn.createStatement().executeUpdate(
                "UPDATE account SET balance = balance - 100 WHERE id = 1");
        conn.createStatement().executeUpdate(
                "UPDATE account SET balance = balance + 100 WHERE id = 2");
        conn.commit();
    } catch (SQLException e) {
        conn.rollback(before);                 // 恢复到备忘录记录的状态
        System.out.println("rolled back to savepoint");
    }
}

调用方只持有 Savepoint 这个不透明句柄,状态的保存与恢复由数据库连接内部完成,调用方不需要知道事务日志或回滚段的任何细节。

访问者模式:java.nio.file.FileVisitor

访问者模式把“对一组对象的操作”从对象结构中分离出来。Files.walkFileTree() 遍历文件树,每遇到文件或目录就调用 FileVisitor 的对应方法,遍历逻辑固定,而“遇到节点做什么”由调用方提供的访问者决定。

Path start = Paths.get("/tmp");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println("dir:  " + dir);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("file: " + file + " (" + attrs.size() + " bytes)");
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) {
        System.out.println("skip: " + file);
        return FileVisitResult.SKIP_SUBTREE;
    }
});

调用方实现访问者接口,walkFileTree 负责遍历结构并在合适的节点上回调。遍历算法与节点操作被拆开,同一棵树可以配上不同的访问者执行不同任务。javax.lang.model.element.ElementVisitor 对注解处理中的元素树也采用了同样的结构。

观察者模式:Observer / Observable

java.util.Observer 接口只有一个 update() 方法,Observable 维护观察者列表并在状态变化时通知它们。

class Stock extends Observable {
    private double price;
    void setPrice(double p) {
        this.price = p;
        setChanged();
        notifyObservers(p);
    }
}

class Logger implements Observer {
    public void update(Observable o, Object arg) {
        System.out.println("price changed to " + arg);
    }
}

Stock stock = new Stock();
stock.addObserver(new Logger());
stock.setPrice(10.5);   // 观察者被自动回调
stock.deleteObservers();
stock.setPrice(11.0);   // 不再有输出

调用方无需知道谁会收到通知,只需 addObserver 注册、notifyObservers 触发,发布者与订阅者在注册那一刻建立松耦合连接。虽然 Observable 在 Java 9 中被标记为过时,但它仍然是理解观察者模式最直接的 JDK 范本。

迭代器模式:Iterator

Collection 的 iterator() 返回 Iterator,将“遍历”从“容器”中分离出来。

List<String> list = new ArrayList<>(List.of("a", "b", "c"));

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) it.remove();   // 通过迭代器安全删除
}
System.out.println(list);   // [a, c]

// 客户端代码完全不依赖 ArrayList 的内部表示
for (String s : Set.of("x", "y")) {
    System.out.println(s);
}

调用方只依赖 Iterator 的两个方法。遍历协议从容器内部剥离出来,成为可互换的独立对象。

命令模式:Runnable

Runnable 将“一个计算或动作”封装为对象,使得动作可以被传递、排队、组合。

Runnable logTask = () -> System.out.println("write log");
Runnable mailTask = () -> System.out.println("send mail");

ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(logTask);
pool.execute(mailTask);

new Thread(logTask).start();

pool.shutdown();

logTask 这个变量就是一个命令对象:请求(做什么)与执行者(谁来执行、何时执行)在调用方这里被彻底分开。

中介者模式:Android Activity 协调 View

GoF 的中介者模式通常这样描述:一组同事对象(Colleague)之间需要通信,但若彼此直接持有引用,耦合会随对象数量呈网状增长;于是引入中介者(Mediator),同事只与中介者交互,由中介者负责转发和协调。

Android 的 Activity 正是这种角色。一个界面上的 EditText、Button、TextView 彼此并不持有对方的引用,它们之间的联动全部经由 Activity 中注册的回调完成:

public class FormActivity extends Activity {

    private EditText nameInput;
    private Button submitButton;
    private TextView statusText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        nameInput = new EditText(this);
        submitButton = new Button(this);
        statusText = new TextView(this);

        // Button 不知道 EditText / TextView 的存在,只把点击事件交给 Activity
        submitButton.setOnClickListener(v -> {
            String name = nameInput.getText().toString();
            if (name.isEmpty()) {
                statusText.setText("name required");
            } else {
                statusText.setText("hello, " + name);
            }
        });

        // EditText 也不知道 Button 的存在,只把文本变化交给 Activity
        nameInput.addTextChangedListener(new TextWatcher() {
            @Override public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
            @Override public void onTextChanged(CharSequence s, int a, int b, int c) {}
            @Override public void afterTextChanged(Editable s) {
                submitButton.setEnabled(s.length() > 0);   // 由 Activity 协调另一个 `View`
            }
        });

        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.addView(nameInput);
        root.addView(submitButton);
        root.addView(statusText);
        setContentView(root);
    }
}

在这段代码里,submitButton、nameInput、statusText 三个同事对象之间没有任何直接引用。Button 的点击逻辑里出现了 nameInput 和 statusText,但那是 Activity 的字段访问,不是 Button 持有它们;EditText 的文本监听里出现了 submitButton,同样是在 Activity 的匿名内部类中通过外部类字段访问。所有跨 View 的协调都发生在 Activity 这个中介者内部。

与 GoF 标准结构对照:

  • Mediator:Activity(更准确地说,是 Activity 中承载协调逻辑的那部分)。
  • ConcreteColleague:Button、EditText、TextView。
  • 交互方式:GoF 的典型实现中,Colleague 持有 Mediator 引用并调用 mediator.changed(this);Android 这里 View 并不持有 Activity 引用,而是通过 setOnClickListener、addTextChangedListener 把回调注册给 Activity,由 Activity 在回调中读取/修改其他 View。这是回调式中介者而非引用式中介者,但“同事之间不直接通信、协调逻辑集中在中介者”这一本质完全一致。

这种设计带来的收益也和中介者模式的目标吻合:新增一个 View 时,只需在 Activity 里加一个字段和相应回调,不需要修改已有 View 的代码;移除某个 View 时,只要 Activity 中不再引用它,其他 View 不受影响。耦合从“View 之间的网状引用”变成了“View 与 Activity 之间的星形回调”。

JDK 自身也有中介者思想的应用,但都不如 Android Activity 这样典型:ExecutorService 可以看作生产者与消费者之间的中介者,java.util.Timer 协调多个定时任务,javax.swing.ButtonGroup 协调一组按钮的选中状态——它们都把“协调”内建在设施中,没有暴露独立的 Mediator 接口,属于中介者思想的应用而非教科书式实现。

状态模式:JDK 中的“弱存在”

状态模式在标准库中没有严格意义上的 GoF 实现。java.lang.Thread 的 Thread.State 枚举描述了线程的生命周期状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED),状态转换由 JVM 驱动,调用方通过 getState() 观察。但线程的行为并非由状态对象多态分发——Thread 没有把“每个状态下的行为”委托给独立的 State 子类,因此只能算“状态思想”的体现,而非严格的状态模式。真正接近状态模式的代码往往出现在应用层(如订单状态机、工作流引擎),而不是 JDK 内部。


写在最后

标准库对设计模式的使用有一个共同特征:模式服务于库的扩展性与稳定性,而非为了模式而模式。Java IO 用装饰器让流的功能可以任意叠加;集合框架用迭代器和策略让遍历与比较从容器中解耦;AbstractList 用模板方法把公共算法固化在抽象层;Proxy 用动态代理把横切关注点的插入推迟到运行时;JAXP 用抽象工厂统一一族 XML 产品的创建;Pattern/Matcher 用解释器把正则语法与匹配执行封装在一起;FileVisitor 用访问者把遍历算法与节点操作拆开;Android 用 ViewGroup 把“整体与部分”统一为同一接口,用 Message.obtain() 把高频对象的复用藏进池中,用 Activity 把 View 之间的网状通信收敛为星形回调;Spring 用 JdbcTemplate 把 JDBC 的复杂度隔离在门面之后。这些设计使得标准库与平台库在保持核心接口稳定的同时,允许实现者、调用者、扩展者各自在恰当的层次上参与进来。

而调用方代码最能说明设计模式的实际价值:模式的存在感往往不在调用处,而在调用方能够以多小的代价获得多大的灵活性。new BufferedReader(new FileInputStream(...)) 只多一层括号就换来了缓冲能力;Arrays.sort(arr, cmp) 只多一个参数就换掉了排序规则;Pattern.compile(...) 只多一行就把一套匹配语言交给调用方;root.addView(row) 让容器和组件并排出现、被同一套操作覆盖;Message.obtain() 只换了一个获取入口就绕开了高频分配;setOnClickListener 只注册一个回调就让 View 不必持有彼此的引用。读 JDK 源码时带着“这里用了什么模式”的视角,往往能比单纯记忆模式定义获得更具体的判断力:你会看到模式在真实代码中常常是混合的、半透明的、带折中的——Activity 用回调而非同事持有中介者引用,Observable 被标记过时,Thread.State 只借了状态模式的思想——而这种“不纯粹”恰恰是工程设计的常态。

posted @ 2026-10-20 13:37  绝不原创的飞龙  阅读(10)  评论(0)    收藏  举报