C++中的接口设计准则

转载自:C++中的接口设计准则

1. 接口设计的重要性

软件设计就是让软件做你想做的事,软件设计一定需要接口(interface)设计,最后用C++实现。今天讨论的可能是其中最重要的一条守则,把你的接口设计得容易用对,不容易用错。

所谓的接口即你提供给用户使用你代码的途径。C++到处都充满了接口的概念,比如函数接口,类接口,模板接口。理想条件下,如果我们用错了接口,编译器会报错,而如果编译器没有报错,那么我们用的接口就是对的。

##2. 接口设计准则准则

2.1 要把接口设计得好用对、难用错

需要预先考虑到用户可能犯的各种错误。假如你正在设计一个表示日期的类:

class Date{
public:
   Date(int month, int day, int year);   // 美式标准表示年、月、日顺序
};

第一眼看起来是没问题的,但用户可能会出现这样的错误。例如,有个英国人输入了错误的格式;有个美国人打错了字,输入了非法日期。

Date d(30, 3, 1995);  // 规定输入美式标准的月,日,年
Date d(3, 40, 1995); // 把3打成了4

对于上面出现的问题,我们可以定义新的类型,使用简单的包装类(wrapper class),让编译器对错误类型进行报错:

// 3个包装类
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(30, 3, 1995);   // int类型不正确报错
Date d(Day(30), Month(3), Year(1995)); // 格式错误报错
Date d(Month(3), Day(30), Year(1995)); // 正确
-----------------------------------

上面接口中我们保证了格式的正确,下一步就是要对取值做出规范,例如月份只能有1到12。使用enum可以满足功能上的要求,但enum不是类型安全的(type safe),因为在前面的文章尽量以const、enum、inline替换#define中已经展示过enum可以被用来当作int类型使用。因此,我们需要定义包含所有月份的集合。

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);  // explicit禁止参数隐式转换,private禁止用户生成自定义的月份 
    // ...
};

Date d(Month::Feb(), Day(29), Year(2020));  // 正确
-----------------------------------

上面的这种方法虽然略显繁琐,但保证了数据的正确性,并且在提升网页脚本安全性的实践中,这是一种常用的防止恶意用户注入代码的思路。

2.2 要把接口设计得具有一致性

C++STL容器接口相比其它语言可以说是达到了高度一致性,因此这些接口也相比更加易用,例如每一个STL容器类都有一个成员函数size()来返回容器当前包含的对象数量。

不像Java,对于数组要使用length属性;对于字符串要使用length方法;对于List要使用size方法,总之各种各样的接口。在.NET中,对于数组要使用Length属性,对于ArrayList又要使用Count属性。可能有些开发者会认为,使用了集成开发环境(IDE),这些不一致性就显得不那么重要。但是,不一致的接口仍然会带来心理上的困难感,因为明显你需要记住更多东西,这是IDE不能弥补的。

接口设计具有一致性也有另外一层意思,是指行为上的一致性,就是要把功能做得与原始类型或是其它标准类型的逻辑一致。前面的文章尽可能使用const修饰符,展示了*运算符用const修饰返回值来避免因为打错字所带来的无意义赋值。

if(a * b = c) // 应该是a * b == c

像上面那样无意义的赋值错误难以察觉,我们当然希望这样的错误在编译时就能被发现。要做到这样的一致性,我们只需要跟着原始类型的逻辑走,例如不允许给右值赋值,来防止可能造成的一系列误用。

2.3 任何要求用户记住东西的接口都更容易造成误用

任何要求用户记住东西的接口都更容易造成误用,因为用户也不是电脑,只要是人类就会忘掉东西。例如动态分配了一个资源,要求用户以某种特定的方式释放资源。在前面的文章C++中基于对象来管理资源中,我们引入了一个工厂函数createInvestment()。

Investment* createInvestment();

为了防止资源泄漏,这个动态分配的资源必须在使用完后删除,但要求用户这样做可能会产生两种情景:

a.用户忘记删除
b.多次删除同一个指针 在前面的文章C++中基于对象来管理资源中解决方法是使用智能指针自动管理资源,但如果用户忘记把这个函数的返回值封装在智能指针内呢?所以,我们最好让这个函数直接返回一个智能指针对象:
std::shared_ptr<Investment> createInvestment();

事实上,返回一个智能指针还解决了一系列用户端资源泄漏的问题。前面的文章C++当心资源管理类中的拷贝行为中提到,如果默认的删除器(deleter)不好用,我们可以给shared_ptr绑定一个自定义的删除器,从而来自动实现我们想要的析构功能。不仅仅是内存资源,通过绑定删除器,我们还可以管理更多种类的资源。例如,前面的文章C++当心资源管理类中的拷贝行为中提及的Mutex锁。

假设我们规定:如果用户从这个工厂函数得到了一个Investment*对象,在析构时要用另一个getRidOfInvestment()函数来释放资源,而不是单独使用delete。这就可能会导致用户由于忘记而使用了错误的释放机制。要防止这种错误,我们把getRidOfInvestment()绑定到shared_ptr的删除器,这样shared_ptr就会在使用完成后自动帮用户调用释放函数。

绑定删除器另一个好处是避免了DLL交叉问题(cross-DLL problem)。这个问题是发生在当一个对象从一个DLL(动态链接库)中生成,在另一个DLL中释放时,在许多平台上就会导致运行时的问题。这是由于不同DLL的new和delete可能会被链接到不同代码。但是,shared_ptr的删除器则是固定绑定在创建它的DLL中。例如,我们有Stock类继承自Investment:

std::shared_ptr<Investment> createInvestment(){
  return std::shared_ptr<Investment>(new Stock);
}

上面代码段中,createInvestment()工厂函数返回的Stock类型智能指针就能在各个DLL中传递,智能指针会在构造时就固定好当引用计数为零时调用哪一个DLL的删除器,因此不必担心DLL交叉问题。

总结

  1. 好的接口很容易被正确使用,不容易被误用。在所有接口设计中都应该秉行这条准则。
  2. 让接口更容易用对,就要把接口做得一致;易于记忆,逻辑上也要与原始类型和标准类型保持一致。
  3. 预防接口误用的方法:包括定义新的包装类型、限制运算符操作、限制取值范围、不要让用户负责管理资源。
  4. shared_ptr支持自定义的删除器,实现我们想要的析构机制。此外,它还能防止DLL交叉问题,而且也能被用来管理其它资源(例如Mutex锁等)。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值