代码健康(2):接口设计,要让其很难被误用

| 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

原文链接Code Health: Make Interfaces Hard to Misuse