19.接口设计原则:易于使用,不易误用

前言

</br>
作为接口设计者,我们应该明确客户可能并不了解实现,也就是说,客户会大概率地不按期望方式使用接口。我们应当以下面的句子作为目标时刻鞭策自己:

  1. 如果客户使用了某个接口但却不能得到预期行为,该代码不应该被正确编译
  2. 如果某段代码通过了编译,它的行为必然是客户所期望的

问题实例

</br>
假定我们正在为一个表现日期的class设计构造函数:

1
2
3
4
5
class Date{
public:
Date(int month,int day,int year);
...
}

客户至少可能会针对这个看似不错的接口犯下两个错误:

  1. 以错误的次序传递参数
    1
    Date d(11,4,2018);
  2. 传递无效的日期
    1
    Date d(2,30,2018);

解决方案

导入新类型以预防

建立wrapper types以区分年月日

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Day{
explicit Day(int d):val(d) {}
int val;
};
struct Month{
explicit Month(int m):val(m) {}
int val;
};
struct Year{
explicit Year(int y):val(y) {}
int val;
};
class Date{
public:
Date(const Month& m,const Day& d,const Year &y);
...
};

如此一来,只有正确的类型才能导致正确的初始化。

限制wrapper types的取值

举例而言,Months的个数应当只有12个,那么使用enum来表征Month看起来是合理的。但是,enum不具备类型安全性(总可以转为int 详见Effective C++ 3),因此比较安全的写法是预先定义所有有效的Months:

1
2
3
4
5
6
7
8
9
class Month
public
static Month Jan() {return Month(1);}
static Month Feb() {return Month(2);}
.......
private:
explicit Month(int m);//禁止用户构造
}
Date d(Month::Mar(),Day(30),Year(1995))

值得注意的是这里是以函数替换对象,具体原因见Effective C++ 5 non—local static对象的初始化顺序一节。

限定类型可以执行的任务

最常见的限制是加上const。
比如我们可以对operator*的返回值加上const,从而让诸如if(a*b=c)这样的东西失去了意义。(用户再也不用担心把==写作=了)


令自定义类型的行为与内置类型保持一致

</br>
该原则的本质是提供行为一致的接口​。一致性是快速学习与开发的前提。(写算法时也应该注意语义一致)


接口不应当要求用户必须做某事

</br>
任何接口如果要求客户必须记得做某件事,那么说明它必然存在“不正确使用”的可能,因为墨菲定律,因为用户的操作无法由开发者决定。

RAII的进一步优化(factory模式)

factory函数总是返回指向一个动态分配的object的指针,仍以Investment举例:

1
Investment* createInvestment();

为了避免资源泄漏,factory函数返回的指针最终必须要被删除。这给了用户两个错误机会:

  1. 忘了delete
  2. 重复delete

我们采用RAII避免了资源泄露,但前提是用户必须了解和记得使用智能指针来管理返回的指针。因此,factory函数可以先发制人,直接就返回一个智能指针:

1
shared_ptr<Investment> createInvestment();

shared_ptr默认的析构行为是delete raw point,这可能与客户所需要的删除操作(deleter)不符,因此我们干脆初始化一个指向nullptr并且删除器是deleter的shared_ptr,最终,factory函数如下:
1
2
3
4
5
shared_ptr<Investment> createInvestment(){
shared_ptr<Investment> retVal(nullptr,deleter);
retval=...;//指向需要管理的资源
return retval;
}

以上是原始指针未能确定的情况,实际上把原始指针直接传给shared_ptr构造函数的话效果更佳。


总结

  1. 接口应当易于使用,不易误用。
  2. 促进正确使用的方法包括语义一致性,以及行为兼容性。
  3. 阻止误用的办法包括建立新外覆类型,限制类型操作,束缚对象值,以及改进RAII。
  4. shared_ptr支持定制删除器,这样可以防范dll问题(跨DLL删除,自动解除互斥锁)。