方法是否应兼具访问器与修改器?JDK案例及选型咨询
这是个非常好的问题,核心其实围绕单一职责原则和API设计的实际权衡——咱们一步步拆解开来聊。
为什么不建议方法同时做访问器和修改器?
最根本的原因是违反了「单一职责」:一个方法同时承担“读取状态”和“修改状态”两个任务,会带来几个明显问题:
- 可读性差:调用者看到方法名时,无法第一时间判断它到底是用来读还是写,必须去看文档或实现,增加理解成本。
- 依赖混乱:如果有人调用这个方法时,只需要修改状态却不小心依赖了返回值,后续如果返回值逻辑变化,会引发意外的bug。反过来,要是有人误以为它是纯访问器,调用后却发现对象状态被修改了,那麻烦更大。
- 调试困难:当对象状态出现异常时,你需要排查这个方法到底是被用作读操作还是写操作,增加排查复杂度。
那JDK里的List.set()、Map.put()为什么要这么设计?
这类方法其实是实用性优先的设计,它们的返回值并不是“副作用”,而是贴合了高频使用场景:
很多时候,我们修改集合元素时,确实需要知道被替换的旧值——比如缓存更新时要判断旧值是否存在、状态迁移时要记录历史状态。把“修改+返回旧值”合并成一个方法,有几个明显的好处:
- 减少冗余代码:不用先写
get()再写set(),一行代码就能完成完整的业务动作。 - 提升效率:避免了两次方法调用的开销,在高频操作场景下更高效。
- 支持原子性操作:如果是并发容器(比如
ConcurrentHashMap.put()),合并操作能避免“读-改”之间的竞态条件,保证操作的原子性。
两种方案的优缺点对比
分离访问器和修改器(纯读/纯写)
优点:
- 职责绝对清晰:
getX()就是读状态,setX()就是改状态,API一目了然,新人接手毫无压力。 - 低耦合:调用者不会因为修改方法的返回值逻辑变化而受影响,比如以后
setX()不需要返回值了,完全不影响只调用修改功能的代码。 - 测试简单:可以单独测试读逻辑和写逻辑,不用考虑两者混合的边界情况。
缺点:
- 代码冗余:如果需要同时完成“读旧值+改新值”,必须写两行代码,增加代码量。
- 原子性风险:在并发场景下,两次方法调用可能被其他线程打断,导致数据不一致(虽然可以加锁,但会额外增加复杂度)。
合并访问器和修改器(改+返回旧值)
优点:
- 高效简洁:一次调用完成两个操作,代码更紧凑,适合高频使用的场景。
- 天然支持原子性:如果方法本身是线程安全的,合并操作能保证“读旧值+改新值”是一个原子动作,避免竞态条件。
- 贴合实际需求:很多业务场景中,修改状态后确实需要知道之前的状态,合并方法更符合“一个动作完成一个完整业务意图”的逻辑。
缺点:
- 可读性打折扣:方法名很难同时体现“修改”和“返回旧值”的双重意图,比如
set(),你必须看文档才知道它会返回旧值,而不是只做修改。 - 容易引发误用:比如有人可能会调用
list.set(0, newVal)却忽略返回值,但如果以后这个方法的返回值含义变更,或者有人误把它当成纯访问器调用(比如oldVal = list.set(0, newVal)其实是修改了列表,这可能不是调用者的本意)。 - 违反单一职责:从长期维护角度,当需要修改其中一个逻辑(比如修改返回值的规则,或修改状态的逻辑)时,可能会影响另一个功能的稳定性。
怎么判断哪种方案更合适?
可以从这几个维度权衡:
- 看使用场景的频率:如果大多数调用者在修改状态时都需要获取旧值,那合并方法更合适;如果读和写是完全独立的操作,那分离更优。比如你的模拟器
run()方法:- 如果
run()的核心是执行模拟逻辑(修改状态),很少需要返回执行前的状态,那应该让run()只做修改,单独加getPreviousState()方法; - 如果几乎每次调用
run()都需要对比模拟前后的状态,那可以让run()返回旧状态,但最好把方法名改得更清晰,比如runAndReturnPreviousState(),避免误解。
- 如果
- 看API的清晰性:如果合并后的方法名能明确表达“修改+返回旧值”的意图,那没问题;如果方法名容易让人混淆,那不如分离。
- 考虑线程安全需求:如果需要保证“读旧值+修改”是原子操作,那合并方法是更好的选择,分离的两次调用可能会被其他线程打断,导致数据不一致。
- 遵循团队规范:如果团队已经有明确的编码规范要求分离访问器和修改器,优先遵循规范,保持代码风格一致。
举个模拟器的具体例子:
- 分离方案(适合大多数场景):
public class Simulator { private State currentState; private State previousState; public void run() { previousState = currentState; // 执行模拟逻辑,更新currentState } public State getCurrentState() { return currentState; } public State getPreviousState() { return previousState; } }
职责清晰,调用者可以灵活选择先读状态再run,或者run后读新旧状态。
- 合并方案(适合每次run都需要旧状态的场景):
public class Simulator { private State currentState; public State run() { State oldState = currentState; // 执行模拟逻辑,更新currentState return oldState; } public State getCurrentState() { return currentState; } }
代码更简洁,不用额外存储previousState,适合需要频繁获取旧状态的业务场景。
内容的提问来源于stack exchange,提问作者Prime624624
相关产品推荐
相关产品推荐

