C++/Java编程小论——方法设计与接口原则总结
C++/Java —— 方法设计与接口原则(详细指南)
笔者的秋招应当是告一段落了,这里决定将自己的一些接口设计方法等总结一下,算是自己的Coding要求了,当然示例代码因为最近正在接触Java,这里都是用Java编写的。
设计方法之前的思考:职责与契约
再我们行动起来,编写代码的时候,我们往往需要问自己一些问题。就是在我们自己的需求准备得到解决之前,我们需要确定好几个基本的问题:
这个方法的单一责任是什么?它最好只做一件事(或一组高度相关的事情)。实际上在C/C++编程的时候,你也需要搞定这个。比如说——我们尽可能拆分一些短的类,他只是简单的做一个事情,我们的复杂的流程是这些简单流程的组合。
我们设计的方法对调用者有什么明确承诺?输入是什么格式、返回值语义如何、什么时候抛异常、是否是线程安全的、是否可重入、是否会修改参数或全局状态?
笔者一般会先搞清楚这个,然后逐一的突破和实现。
方法签名详解
Java和C++类似。我们先从左到右一个一个说清楚来。
返回值类型
- 优先使用具体而明确的返回类型(例如
Optional<T>
、集合接口List<T>
而不是实现类ArrayList<T>
)。 - 若方法可能没有结果而结果的缺失是正常情况,咱们优先采用
Optional<T>
而不是丢一个NULL回来,因为这侵占了可能的合法空指针。 - 对于布尔方法,用
is
,has
,can
等前缀(isEmpty()
,hasNext()
)。
Optional<User> findUserById(String id);
boolean isValidEmail(String email);
List<String> listNames();
参数设计
- 参数数量尽量少(理想 <= 3)。若逻辑上需要多个参数,考虑将它们封装成对象(参数对象 pattern)。
- 喜欢使用 不可变 参数(传入
Collections.unmodifiableList(...)
或传入不可变对象)以降低副作用。 - 对于可变的集合参数,选择 defensive copy 或在 Javadoc 中清楚说明会修改。
- 避免使用
null
表示“缺省”,考虑Optional<T>
(但不要把Optional
用作方法参数的常规做法——多数建议是 不要 把Optional
用作参数类型)。
示例(参数对象):
// 不好(长参数列表)
void createUser(String username, String email, String phone, boolean sendWelcome);// 好:参数对象,便于扩展
void createUser(CreateUserRequest req);
可见性(visibility)
- 首先把方法设为
private
,然后仅在必要时提高为protected
/public
。最小暴露原则(Least Privilege)。 - 对类库的 API,使用
public
且稳定的方法签名,避免频繁改动。
throws(异常声明)
- 明确:什么时候抛异常、哪种异常(检查型 checked vs 非检查型 unchecked)。
- 现代 Java 倾向将 编程错误(非法参数、非法状态)使用
RuntimeException
(如IllegalArgumentException
、IllegalStateException
);将可预见的外部错误(IO、网络、解析失败)使用 checked 异常或包装成 domain-specific exceptions。 - Document exceptions in Javadoc using
@throws
。
示例:
/*** @throws IllegalArgumentException if id is null or empty* @throws DataAccessException if repository fails*/
User getUserById(String id) throws DataAccessException;
重载 vs 泛型 vs 可变参数
- 使用方法重载要保持语义一致,避免仅因返回类型不同而重载(Java 不允许仅靠返回类型重载)。
- Varargs (
T... args
) 在 API 便利性上很好,但要注意与数组重载的歧义。 - 若方法和类型有关,用泛型将类型信息向外暴露而非使用
Object
。
示例:
public <T> List<T> toList(T... elements) { ... }
方法行为与语义:纯函数、副作用、异常策略
纯函数(Pure functions)
- 定义:相同输入永远返回相同输出、无副作用(不改变外部状态)。
- 优点:易测试、并发友好、可缓存(memoize)。
- 适用场景:计算、格式化、校验、映射等。
示例:
int add(int a, int b) { return a + b; } // 纯函数
有副作用的方法
- 对外部状态有影响(修改参数、写日志、写 DB、发送网络请求)。需要在文档中明确,并尽量最小化副作用的可见面。
- 如果可能,把副作用与纯逻辑分离(先返回结果,再有 caller 负责持久化/提交)。
异常策略总结
- 校验失败:
IllegalArgumentException
或自定义ValidationException
(unchecked); - 状态不允许:
IllegalStateException
; - 资源/IO问题:checked exceptions 或自定义 unchecked wrapper(例如
DataAccessException
); - 不要滥用 checked exceptions 导致调用方大量 try/catch 泄露到业务逻辑中。
性能、可测性与并发考虑
性能评估
我们可以使用Profiling(笔者VS用过profile小工具),我们在搞定了性能热点路径的时候,需要避免在热路径中分配大量临时对象(例如在循环中频繁创建字符串或集合),使用 StringBuilder
、对象池(审慎)或复用缓冲。
若方法暴露为 API,并且会被频繁调用,文档或注释应明确性能特性(例如“此方法 O(n)”, “此方法会创建新集合”)。
可测性
- 方法应便于单元测试:尽量少使用静态全局状态;若依赖外部系统,使用依赖注入(DI)或接口抽象来替换 mock。
- 对副作用明确契约,单元测试时可以断言状态变化或 mock 相应依赖。
并发与线程安全
- 首选:无状态与不可变:方法内部不使用可变静态字段或共享可变状态。
- 如果方法需要修改共享状态:
- 使用线程安全的并发工具(
ConcurrentHashMap
,Atomic*
系列)。 - 说明是否是同步/原子的,或由调用者保证同步。
- 使用线程安全的并发工具(
- 对于可变返回值(如返回集合),确定是否返回防御性拷贝或不可变视图(
Collections.unmodifiableList(...)
)。
示例(线程安全约定):
/*** Thread-safe: method is stateless and uses local variables only.*/
public int compute(int x) { ... }
方法实现细节与风格建议
命名
方法名应清晰表达行为:calculateTotal
, findUser
, saveToDatabase
。这样像读英语一样读出来我们的语义,会比其他乱七八糟的命名要舒服的多。
长度与职责
建议方法行数短(例如 < 50 行),单一职责。若方法不同抽象层次混杂,咱们需要做拆分。
防御式编程
- 对外部输入进行校验并在顶部早期返回(fail-fast)。
- 使用
Objects.requireNonNull(arg, "msg")
做空检查。
使用注解
@Nullable
/@NotNull
(或javax.annotation
/org.jetbrains.annotations
)来记录可空性。@Deprecated
与@Deprecated(forRemoval = true, since = "1.2")
标注弃用 API。@Override
总是在覆盖父方法时使用,避免拼写错误导致不覆盖。
异常处理风格
- 捕获只有当你能处理异常或添加更有意义的上下文时才捕获(不要吞掉异常)。
- 如果包装异常,保留原始异常为 cause:
throw new MyDomainException("msg", e);
接口基础:为什么用接口?什么时候用抽象类?
接口 表示能力或契约(what),抽象类 可包含状态和默认实现(how)。现代 Java(8+)的接口更强大,但仍有判断标准:
- 若希望定义多个不相关类能实现的能力(例如
Comparable
,Iterable
),使用接口。 - 若需要共享状态(字段)或构造器逻辑,使用抽象类。
- 若设计可演化的 API,优先接口 + 默认方法(谨慎使用),避免抽象类的单继承限制。
接口设计原则与模式
接口隔离原则(ISP)
- 把大接口拆成小接口,让实现者只依赖其需要的方法。
- 例如不要把
Printer
做成print
,scan
,fax
三合一接口;而是拆成Printable
,Scannable
,Faxable
。
里氏替换原则(LSP)
- 子类型应能替换父类型而不破坏客户端行为。换言之,子实现不要改变接口声明的语义(预条件不能更强,后置条件不能更弱,不能抛出额外的未声明的检查异常等)。
依赖倒置原则(DIP)
- 高层模块不应该依赖低层模块,两者都应该依赖抽象(接口)。通过接口注入依赖,便于测试和替换实现。
设计契约(Design by Contract)
- 在接口上清晰写出前置条件、后置条件、不变式(使用 Javadoc 的
@throws
、@implSpec
、@implNote
来说明实现者或调用者责任)。
常见模式
- 策略模式(Strategy):使用接口定义可替换算法;调用端注入具体实现。
- 适配器模式(Adapter):接口适配不同实现以统一调用。
- 装饰器模式(Decorator):接口+组合用于动态增强行为。
Java 接口的现代用法(Java 8+)
默认方法(default)
允许在接口中提供方法实现,从而实现向后兼容地扩展接口。使用场景:
- 为现有接口添加新行为而不破坏旧实现。
- 提供便捷的组合方法。
注意:默认方法不应该含有大量状态逻辑,且可能造成菱形继承冲突(类实现多个接口时需要显式解决冲突)。
示例:
public interface Logger {void log(String msg);default void logInfo(String msg) { log("[INFO] " + msg); }
}
私有方法(private) — Java 9+
可在接口内部抽取默认方法的公共实现逻辑,而不暴露给实现者。
函数式接口(Functional Interface)
带有单个抽象方法(SAM)的接口,可用作 Lambda 的目标,适用于策略或回调:
@FunctionalInterface
public interface Transformer<T,R> {R apply(T t);
}
与 java.util.function
包中的标准函数式接口(Function
, Consumer
, Supplier
等)配合使用能提升互操作性。
标记接口(Marker Interface)
如 Serializable
,表示类型具备某种能力。现代替代方式可是注解(更清晰)。
API 演化、版本兼容与退役策略
- 向后兼容(backward compatible):新增
public
方法或重载一般是安全的;修改已有方法签名会破坏兼容。 - 接口扩展:首选用
default
方法来添加新方法(注意行为一致性与可能冲突)。 - 弃用(deprecation):用
@Deprecated
标注并在 Javadoc 说明替换方案与移除时间窗口。 - 语义变更:即使签名不变,改变返回值语义或异常行为也会破坏客户端。尽量避免。
接口实现的测试与文档
Javadoc 要点
- 每个公共接口与方法都应有 Javadoc:说明职责、参数意义、返回值语义、抛出的异常、线程安全约束、性能特性、可变性(是否修改参数或返回可修改集合)。
- 使用标签:
@param
,@return
,@throws
,@implSpec
,@implNote
,@since
。
示例 Javadoc:
/*** Finds user by id.** @param id non-null id of the user* @return Optional containing user if found, otherwise Optional.empty()* @throws IllegalArgumentException if id is null or empty* @implSpec Implementations should perform a lookup in the underlying store.*/
Optional<User> findUserById(String id);
单元测试
- 测试契约而不是实现细节:为接口编写测试用例(抽象测试类)并让每个实现继承这些测试,确保所有实现满足同一契约。
- 测试线程安全约定(并发单元测试)在有并发保证时很重要。
设计检查清单 + 常见反模式
设计检查清单(发布前)
- 方法名字是否清晰表达行为?
- 参数是否最小且不可避免?是否需要参数对象?
- 有无明确的空值策略(允许 null 吗)?是否在 Javadoc 指定?
- 是否是纯函数?是否有副作用?是否文档化副作用?
- 返回值是否应该为 Optional/Collection/Stream?是否应返回防御性拷贝或不可变集合?
- 异常使用是否合理?是否避免过度使用 checked 异常?
- 是否线程安全?谁负责同步?
- 是否易于测试?是否可以 mock/replace?
- 是否易于向后兼容演化?
常见反模式
- 上帝方法(God Method):方法做太多事情,难以复用和测试。
- 大而笨重的接口(Fat Interface):接口中包含大量不相关方法,违反 ISP。
- 返回
null
作为常态:会增加 NPE 风险;PreferOptional
或空集合。 - 隐藏副作用:方法修改全局状态但文档中没有说明。
- 在接口中放状态:接口本应是契约,包含状态会导致实现耦合(除非是默认方法辅助的不可变映射)。