真正的平衡点在于只封装当前需要的变化、只设计当前能验证的扩展点;接口/抽象类应在已有两个以上实现或明确新增需求时引入;封装边界由协作关系决定,而非代码行数;设计需附带“失效条件”,用重构和测试覆盖替代过度预设。

过度封装和过度设计不是“做多了”,而是“为不存在的问题提前铺路”。真正的平衡点在于:只封装当前需要的变化,只设计当前能验证的扩展点。
从具体需求出发,而非抽象可能性
很多团队在类刚写完就急着加接口、抽象父类、引入策略模式——但业务逻辑还没跑通一次。这时的“可扩展性”只是幻觉。
- 先让一个具体实现跑起来(比如用硬编码或简单 if 判断处理两种支付方式)
- 等出现第三个相似分支、或同一逻辑被复制两处以上时,再提取共性
- 接口/抽象类的诞生时机,应是“已有两个以上不同实现”或“明确知道下周要接入新合作方”
封装边界由协作关系决定,而非代码行数
不是把所有字段设为 private 就叫封装好,也不是每个方法都加 getter/setter 就叫暴露合理。关键看谁在用、怎么用、会不会因内部改动而连带修改。
- 对外暴露的 API 应反映“它能做什么”,而不是“它怎么做的”(例如 saveOrder() 比 writeToDb() + updateCache() + sendMQ() 更合适)
- 内部状态是否需要封装,取决于是否有外部代码依赖该状态的格式或生命周期(如日期字段用 LocalDate 而非 String,不是因为“更类型安全”,而是避免调用方反复解析)
- 工具类、配置类、DTO 这些本就不承担行为的类型,过度封装反而增加调用成本
设计决策必须附带“失效条件”
每一个抽象层、每一个接口、每一个配置开关,都应该回答一个问题:“在什么情况下,这个设计会变成累赘?”并记录下来。
- 如果新增一种折扣类型只需改一个 switch 分支,就别急着上策略模式
- 如果数据库换为图数据库的概率低于 5%,就别把 DAO 层强行抽象成通用查询引擎
- 如果日志格式未来三年都不会变,就别为 log 字段预留 JSON 扩展字段
用重构代替预设,用测试覆盖代替防御式设计
比起一开始画满类图、定义七八个接口,更可持续的做法是:写出清晰的函数+单元测试,等变化来临时,靠测试保障快速安全地重构。
- 测试覆盖率高,意味着你敢删代码、敢合并类、敢拆接口——这才是封装松耦合的真实体现
- 每次重构后问一句:现在这个类/模块,是否仍只做一件事?它的依赖是否仍可解释?
- 当发现 3 次以上为了加一个字段要改 4 个地方,才是引入 Builder 或 DTO 的信号,而不是一开始就上 Lombok + Record + MapStruct 套餐
不复杂,但容易忽略:好的面向对象设计,是让代码随着问题一起生长,而不是替未来的问题提前建好整座空城。










