欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。
现在假设我们为一个用来表现日期的class设计构造函数:
class Date {
public:
Date(int month, int day, int year);
// 其中 month = 1, 2, ..., 12
// day = 1, 2, ..., 28, 29, 30, 31
// year > 0
...
};
- 当出现Date d(2022, 2, 12),即时间为2022.2.12时是否正确?
- 即使每个参数传入的位置正确,那么出现Date d(13, 12, 2022)时,是否又正确呢?
那么此时这个构造函数出现的问题如下:
1.传递参数的次序出现错误
2.可能传入一个无效的月份或天数
很多客户端错误可以因为导入新类型而获得预防。那怎么才能==区分年、月、日呢,而不是都是统一的int类型?我们可以通过导入简单的外覆类型(wrapper types)==来区别天数、月份及年份,然后于Date构造函数中使用这些类型,主要操作如下:
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);
...
};
Date d(20, 2, 2002); //error,不正确的类型
Date d(Day(30), Month(3), Year(2002)); //error,不正确的类型
Date d(Month(3), Day(30), Year(2002)); //OK
令Day,Month,Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的structs好(见条款22)。
当正确的类型定位好后,限制它的值便简单多了。例如一年有12个月,那么我们所设置的类型Month就会反应出这个限制。办法之一,我们可以使用enum表示月份,但是enums不具备我们希望拥有的类型安全性,例如enums可被拿来当一个ints使用(见条款2),比较安全的解法是预先定义所有有效的Months:
class Month {
public:
static Month Jan() {return Month(1)};
static Month Feb() {return Month(2)};
...
static Month Dec() {return Month(12)};
...
private:
explicit Month(int m); //阻止生成新的月份,这是月份专属数据
};
Date d(Month::Mar(), Day(3), Year(2002));
如果我们对以函数替换对象,表现某个特定月份感到奇怪,或是因为忘记non-local static对象的初始化次序有可能出问题,那我们可以阅读条款4
预防客户错误的另一个办法是,限制类型内什么事可做,什么事不能做。常见的是加上const限制符,可阅读条款3
——————————————————————————————————————
接下来另一个准则就是“让types容易被正确使用,不容易被误用”的表现形式:除非有好的理由,否则应该尽量令我们的types的行为与内置types一致。因为内置类型大家都很熟悉,我们建立的类型行为应该与其保持一致,也方便大家的使用。
STL容器的接口十分一致,这使得它们非常容易被使用。
任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用的倾向,因为客户很可能忘记做这件事情,如条款13中展示的。
总结
好的接口很容易被正确使用,不容易被误用;
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容;
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;
tr1::sharel_ptr支持定制型删除器(custom deleter)。这可预防DLL问题,可被用来自动解除互斥锁(mutexes,见条款14)等。