| 2021-03-02
这是《Effective C++》中的第18条原则。
我们都在试图避免代码中的错误。 那么,如何避免调用者以错误的方式调用了你的这些代码呢?
让你接口提供的行为与内置类型的一般性行为一致。
良好的接口设计可以使调用者轻松做正确的事情,并且让调用者很难做错事( Make interferences easy to use correctly and hard to use incorrectly )。也就是说,你要限制用户能做什么,不能做什么。
不要将你编写的Class的责任推给调用者。
**让接口对客户提出最少的要求。**许多接口总是要求用户注意这注意那,要求一多,用户就容易头晕,这样使用接口就更容易出错。所以一定要让接口被傻瓜式地使用。在这里作者又举了智能指针的例子。
下面的代码中,你看出问题来了么?
class Vector {
explicit Vector(int num_slots); // 创建一个空的vector,且分配`num_slots`个席位.
int RemainingSlots() const; // 返回目前还剩余多少席位.
void AddSlots(int num_slots); // 向Vector中再增加`num_slots`个席位。
// 在Vector的末尾增加一个新元素.调用者必须通过调用RemainingSlots(),确保的确有剩余席位可用。
// 在调用Insert()之前至少要有一个席位,否则调用者应该先调用增加席位的函数AddSlots().
void Insert(int value);
}
如果调用者忘记了调用AddSlots()
,当Insert()
被调用时,就可能会发生不可预期的行为。为个接口将复杂性推给了它的调用者,将实现细节暴露给了调用者。
在这个类中,对slots的维护职责与调用者的可视行为无关,因此,不要将其暴露给外界。通过将增加席位的功能放在Insert()
函数中,使调用者不可能触发不可预期的行为。
class Vector {
explicit Vector(int num_slots);
// 如果必须,自动在Vector的末尾增加一个新元素,
// 分配更多的席位,以确保有足够的存储空间用于保存新元素。
void Insert(int value);
}
由编译器强制对契约( Contracts )进行检查,通常比运行时强制进行契约检查更好。更糟糕的情况是,依赖于调用者查看了文档中的契约描述后,能够执行正确的操作。
下面是另外一些容易发生接口误用的情况:
- 要求调用者调用一个初始化函数(替代方法:公开使对象完全初始化的工厂方法)。
- 要求调用者执行定制化的清理任务(替代方法:使用特定于语言的构造,以确保在对象超出范围时自动清理)。
- 允许创建没有必需参数对象的代码实现(例如,没有ID的用户)。
- 限制参数的取值范围,尤其是当该参数可以使用多种类型时(例如,使用
Duration timeout
, 而不是int timeout_in_millis
)
我们可能无法100%地保证,所有的接口都能设计得万无一失。在某些情况下,由于某些要求无法在接口中表达,因此必须依赖静态分析或者文档(例如,某个回调函数必须是线程安全的)。
不要强制执行您不需要强制执行的操作,避免使用过于防御性的代码。例如,功能参数的广泛验证会增加复杂性,并且可能会降低性能。
发表时间:July 25, 2018
原文作者:Marek Kiszkis