前言
</br>
作为接口设计者,我们应该明确客户可能并不了解实现,也就是说,客户会大概率地不按期望方式使用接口。我们应当以下面的句子作为目标时刻鞭策自己:
- 如果客户使用了某个接口但却不能得到预期行为,该代码不应该被正确编译
- 如果某段代码通过了编译,它的行为必然是客户所期望的
问题实例
</br>
假定我们正在为一个表现日期的class设计构造函数:1
2
3
4
5class Date{
public:
Date(int month,int day,int year);
...
}
客户至少可能会针对这个看似不错的接口犯下两个错误:
- 以错误的次序传递参数
1
Date d(11,4,2018);
- 传递无效的日期
1
Date d(2,30,2018);
解决方案
导入新类型以预防
建立wrapper types以区分年月日
1 | struct Day{ |
如此一来,只有正确的类型才能导致正确的初始化。
限制wrapper types的取值
举例而言,Months的个数应当只有12个,那么使用enum来表征Month看起来是合理的。但是,enum不具备类型安全性(总可以转为int 详见Effective C++ 3),因此比较安全的写法是预先定义所有有效的Months:1
2
3
4
5
6
7
8
9class 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函数返回的指针最终必须要被删除。这给了用户两个错误机会:
- 忘了delete
- 重复delete
我们采用RAII避免了资源泄露,但前提是用户必须了解和记得使用智能指针来管理返回的指针。因此,factory函数可以先发制人,直接就返回一个智能指针:
1
shared_ptr<Investment> createInvestment();
shared_ptr默认的析构行为是delete raw point,这可能与客户所需要的删除操作(deleter)不符,因此我们干脆初始化一个指向nullptr并且删除器是deleter的shared_ptr,最终,factory函数如下:1
2
3
4
5shared_ptr<Investment> createInvestment(){
shared_ptr<Investment> retVal(nullptr,deleter);
retval=...;//指向需要管理的资源
return retval;
}
以上是原始指针未能确定的情况,实际上把原始指针直接传给shared_ptr构造函数的话效果更佳。
总结
- 接口应当易于使用,不易误用。
- 促进正确使用的方法包括语义一致性,以及行为兼容性。
- 阻止误用的办法包括建立新外覆类型,限制类型操作,束缚对象值,以及改进RAII。
- shared_ptr支持定制删除器,这样可以防范dll问题(跨DLL删除,自动解除互斥锁)。