首页/开发/代码质量与评审速查/手法:Encapsulate Collection(封装集合)
重构手法62 条

手法:Encapsulate Collection(封装集合)

返回集合时给出只读视图,修改走专用方法:调用方无法绕过校验与通知,集合的所有变更点收敛到类的内部;对数组、字典同样适用。

Encapsulate Collection 是什么

返回集合时给出只读视图或副本,修改走专用方法。这样调用方无法绕过校验与通知就地增删,集合的所有变更点收敛到类内部;把「谁能改、改的时候要做什么」明确下来,集合就不再是隐形的公共可变状态。

基本信息

规则读给只读视图或副本,写走显式方法
场景实体暴露内部列表、调用方拿到后就地增删
配套把入参与出参一并封装,校验集中在专用方法
权衡要不要防御性拷贝取决于集合大小与调用频率

要点与选择

  • getter 返回不可修改视图,写操作通过 add/remove 方法暴露
  • 需要绝对安全时返回副本,代价是每次调用的复制开销
  • 新建集合的方法比传入集合更安全:由类自己决定如何合并
  • 专用方法里可以顺带做去重、排序、计数与事件通知
  • 元素本身可变时,只读视图挡不住元素被改,必要时再封装元素
  • 暴露集合规模与是否包含等查询方法,避免调用方为了看一眼长度而取全量

Java 示例

Java 示例
public class Course {
    private final List<Student> students = new ArrayList<>();

    // 读:返回不可修改的副本,外部改不动内部结构
    public List<Student> students() {
        return List.copyOf(students);
    }

    public int size() { return students.size(); }

    // 写:校验与通知都收在这里
    public void addStudent(Student s) {
        if (s == null) throw new IllegalArgumentException();
        if (students.contains(s)) return;
        students.add(s);
        events.publish(new StudentEnrolled(s));
    }
}

各语言常见做法

语言只读视图副本
JavaCollections.unmodifiableList / List.copyOfnew ArrayList<>(list)
C#IReadOnlyList<T>list.ToList()
Pythontuple(seq) / MappingProxyTypelist(seq) / dict(d)
Go无内置视图,靠不导出字段append([]T(nil), s...)
不可修改视图与副本语义不同:unmodifiableList 只是视图,底层集合若被内部代码修改,视图内容也会变;List.copyOf 是快照,改动内部集合不会影响已返回的副本。选择取决于调用方是否需要看到最新状态。
只读集合挡住的是「结构被改」,挡不住「元素被改」:如果元素本身可变,调用方仍能修改元素内部字段。需要真正不可变时,还得让元素类型不可变,或者返回只读的投影对象。

易错点

  • 每次 getter 全量拷贝大集合:热路径上复制开销显著,应改为视图或提供查询方法
  • 只读视图允许外部元素继续被改:容器受了保护,元素却能被就地修改
  • 封装了集合却留下 setter 传集合:外部构造一个列表整体替换,校验与通知被绕过
  • 为了测试暴露内部集合的可变引用:封装被侵蚀,实现细节被测试绑定

数据来源与参考

  • 《重构》第 2 版 · 第 7 章(Encapsulate Collection)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌