Fork me on GitHub

让接口难被误用

Google代码健康

Posted by Kaelzhang on July 14, 2020

Google代码健康:让接口难被误用

本文源于Google公司的马桶上的健康代码,作者:Marek Kiszkis

我们都试图避免代码中的错误。 但是,由代码调用方产生的错误呢? 好的接口设计可使调用方易于执行正确的操作,而使调用方难以执行错误的操作。 不要将维护类的不变需求的责任推给其调用方。

你看下列代码可能引起的问题吗?

class Vector {
  explicit Vector(int num_slots);  // Creates an empty vector with `num_slots` slots.
  int RemainingSlots() const;  // Returns the number of currently remaining slots.
  void AddSlots(int num_slots);  // Adds `num_slots` more slots to the vector.
  // Adds a new element at the end of the vector. Caller must ensure that RemainingSlots()  
  // returns at least 1 before calling this, otherwise caller should call AddSlots().
  void Insert(int value);
}

如果调用方忘记调用AddSlots(),则在调用Insert()可能会触发未定义的行为。 该接口将复杂性转嫁给了调用方,将调用方暴露在实现细节中。

由于维护插槽与该类的调用方可见行为无关,因此不要在接口中公开它们; 使得当Insert()需要添加插槽不可能触发未定义的行为

class Vector {
  explicit Vector(int num_slots);
  // Adds a new element at the end of the vector. If necessary,
  // allocates new slots to ensure that there is enough storage
  // for the new value.
  void Insert(int value);
}

编译器强制执行的契约通常比运行时强制执行的契约要好,或更糟糕的情况是,仅依靠调用方通过契约文档来做正确的事情。

以下是一些其他示例,它们可能表明接口易于滥用:

  • 要求调用方调用初始化函数(或者:公开返回对象完全初始化的工厂方法)。
  • 要求调用方执行自定义清理(替代方法:使用语言专属构造器,以确保在对象超出范围时自动进行清理)。
  • 允许代码路径创建没有必需参数的对象(例如,没有ID的用户)。
  • 允许仅对某些值有效的参数,尤其是在可以使用更合适的类型的情况下(例如,建议使用Duration timeout而不是int timeout_in_millis)

具有万无一失的接口并不总是可行的。在某些情况下,必须依靠静态分析或文档,因为某些要求无法在接口中表示出来(例如,回调函数必须是线程安全的)。

不要执行无需执行的操作-避免使用过度防御的代码。例如,对功能参数的广泛验证会增加复杂性并降低性能。


  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!