Item 18: Make interfaces easy to use correctly and hard to use incorrectly.

“让接口容易被正确使用,不易被误用”,这也是面向对象设计中的重要概念,好的接口在工程实践中尤其重要。 在使用优秀的第三方组件时,常常能够切身感受到好的接口原来可以这么方便,甚至不需记住它的名字和参数就能正确地调用。 反观自己写的API,常常会有人三番五次地问这个参数怎么设置,真是失败。人非圣贤孰能无过,只能在这种痛苦的驱动下努力的重构和学习!

虽然我已经脱离了很久的Windows开发,但想起来.NET API良好的设计,还是会五体投地。

言归正传。

在C++中,可以说到处都是接口,接口定义了客户如何与你的代码进行交互。如果用户误用了你的接口,你至少也要承担一部分的责任。 理想情况的接口是这样的: 如果用户误用了接口,代码不会正常编译;如果代码通过了编译,那么你的接口就要完成客户想要的操作。

正确地构造一个Date

来个通俗的例子:Date对象的构造函数需要传入月、日、年。但客户在调用时常常传错顺序,这时可以将参数封装为对象来提供类型检查:

class Date{
public:
    Date(const Month& m, const Day& d, const Year& y);
};

Date d(Day(30), Month(3), Year(1995));    // 编译错:类型不兼容!
Date d(Month(3), Day(30), Year(1995));    // OK

即使这样,用户的Month构造函数仍然会传入一个不合理的参数(例如32),或者搞不清楚下标从0还是1开始。 解决方案是预定义所有可用的Month

class Month{
public:
    static Month Jan(){ return Month(1); }
    static Month Feb(){ return Month(2); }
};
Date d(Month::Jan(), Day(30), Year(1995));

从上述Date的例子中可以看到,可以将运行时的数据转换为编译期的名称,可以将错误检查提前到编译期。以此解决参数顺序和范围的误用。

限制类型的操作

另外一个例子来自Item 3:尽量使用常量,乘法运算符返回值设为const,以防止误用赋值:

if(a * b = c) ... // 用户的意图本来是判等

提供一致的接口

除此之外,提供一致的接口也很重要。例如STL容器封装了互不兼容的基本数据类型,为STL算法提供了非常一致的接口。

比如STL提供了size属性来标识容器的大小,容器可以是数组、链表、字符串、字典、集合。.NET中所有这些大小都叫Count属性。 采用哪种命名并不重要,重要的是提供一致的接口。不仅便于应用中使用,也便于库的扩展。

好的接口不会要求用户去记住某些事情。比如Investment* createInvestment()要求客户记住及时去销毁, 那么客户很可能忘记了去deletedelete了多次。解决方案便是返回一个智能指针而不是原始资源,参见:Item 13:使用对象来管理资源 尤其是当销毁操作不是简单的delete时,客户还需要记住如何去销毁它。而我们返回智能指针时就能指定deleter来自定义销毁动作:

shared_ptr<Investment> createInvestment(){
    // 销毁一项投资时,需要做一些取消投资的业务,而不是简单地`delete`
    return shared_ptr<Investment>(new Stock, getRidOfInvestment);
}

shared_ptr带来的好处还不仅仅是移除了客户的责任,同时还解决了跨DLL动态内存管理的问题。 在DLL中new的对象,如果在另一个DLL中delete往往会发生运行时错误,但shared_ptr进行资源销毁时, 总会调用创建智能指针的那个DLL中的delete,这意味着shared_ptr可以随意地在DLL间传递而不需担心跨DLL的问题。

总之,好的接口容易被正确使用,不易被误用。可以为内置类型提供一致的接口来方便正确的使用。识别误用的手段包括: 创建新的类型、限制类型的操作、限制对象的值、移除客户的资源管理责任。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2015/08/09/effective-cpp-18.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。