□第一节 什么是接口?
什么是接口?
百度百科的解释:两个不同系统(或子程序)交接并通过它彼此作用的部分。
接口的概念贯穿于整个软件开发过程中,本文讨论的接口概念仅局限于以C++实现的class,function等。
class接口
以OOP的思想,现实的问题总能抽象成一个个class,这些class包含哪些属性,方法其实就是class接口要考虑的问题。
function接口
class接口的操作就是通过一个个function来实现的,function的参数传递,返回值是function接口要考虑的问题。
本条款“让接口容易被正确使用,不易被误用” 是设计优良接口重要准则之一,并在开始的时候就需要考虑的。
“定义输入”
要开发一个“让接口容易被正确使用,不易被误用”的接口,首先要考虑使用该接口可能产生的错误。考虑用来表示时间的Date类。
class Date { private: int year; //年份 short month; //月份 short day; //天数 public: Date(int year, short month, short day) { this->year = year; this->month = month; this->day = day; } };
今天是2013/02/28,今天的日期可以表示为
Date d(2013,02,28);
那明天的日期可以表示为
Date d(2013,13,01);
问题来了,刚才手抖一下,把“03”敲成“13”。
还有一个问题,假如不小心也可能出现情况:
Date d(2013,28,02); //天数与月份反了
上面的两个问题系统都不会被系统检测,也就是我们的接口接收到错误输入而没有做出任何阻止。
“使用新类型”
导入新类型来覆盖已有的数据类型是一个不错的方案。
struct Year { explicit Year(int y):val(y) { } int val; }; struct Month { explicit Month(int m):val(m) { } int val; }; struct Day { explicit Day(int d):val(d) { } int val; };
Date的构造函数可以是下面的样子:
Date(const Year& y, const Month& m, const Day& d); Date d(Year(2013), Month(03), Day(01)); // OK Date d(Year(2013), Day(01), Month(03)); // NG,类型错误
另外一个问题的解决很容易想到的方法是使用枚举:
关于年份,似乎输入任何一个数值都是合法的,所以不需要枚举。
关于月份,一年中只有12个月,所以可以枚举1~12的值。
比如月份的枚举如下:
enum Month { month1= 1, month2, ... month12, };
那么修改后的Date类应该是下面的样子
Date(Year year, Month month, Day day);
这样做法可以解决一部分问题,至少不会出现月份越界的情况。
于是明天的日期可以这么写:
Date d(Year(2013),month(3) , Day(01));
但很不幸C++中的枚举只是被拿来当作int值来用,例如下面的情况是可以通过编译的:
Date d(Year(2013), (Month)13, Day(28));
幸好,还有更厉害的一招:预定义所有有效的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(Year(2013), Month::Feb(),Day(28));
但这种做法的看起来很诡异,而且当要枚举的集合中有很多元素时,势必影响到代码生产效率和系统性能。
总结
在接口设计的时候引入新类型,限定边界值是保证接口的正确性的有效措施之一。
但过分追求接口的正确性往往会带来系统逻辑复杂度的增加。
□第二节 让接口更容易些
“望文生义”
每个人都有名字,名字的作用就是用来区分个体的。名字往往能给我们带来很多信息。比如我的名字叫“小明”,很容易知道
我名字的读音“xiaoming”,是个男的。假如有个人叫“李芳”我说他是个男的,你信吗?
函数也有名字,这个名字是给人读的。所以当给接口定义名称的时候最好通俗易懂,"见名知意"。
“多重性”
用过.NET的都知道,Arrays有个属性名为Length用于求数组中元素个数,ArrayLists中有个属性为Count,也是干这事的。
相信肯定有过这样的经历:当使用元素个数的时候总是先尝试用Length,发现没有再换Count。是不是有点郁闷?
“自己动手”
有些事情自己能做,那就不要依赖别人。当接口的某些行为依赖于调用者来完成,那就犯了大忌。
比如我们的ConductorFactory,创建完Conductor就不管了。比如下面的代码:
cgi_CallStatus cgi_ConductorFactory::cgi_CreateSetSyslogConductor(cgi_SettingConductor *&conductor, cgi_Status &status) { status.okNg = CALL_OK; conductor = MMI_NEW cgi_SettingConductor( cgi_Translate807a::Request, cgi_Translate807a::Response, cgi_Translate807a::GetSize, NULL, NULL, NULL); if (conductor == NULL) { cgi_TranslatorCommon::SetErrorStatus(CGI_ERROR_NEW, CGI_CONDUCTOR_FACTORY, CGI_FACTORY_SETSYSLOGCONDUCTOR, 1, status, NULL); return CALL_ERROR; } return CALL_OK; }
conductor的销毁显然不是由ConductorFactory来负责,假如使用完没有delete,很显然内存泄漏就产生了。
我们可以使用智能指针来解决:
cgi_ConductorFactory::cgi_CreateSetSyslogConductor(std::tr1::shared_ptr<cgi_SettingConductor> conductor, cgi_Status &status) { ... }
什么是接口应该做的,什么是调用者要做的,在设计时一定要多加思考。
总结
容易理解的名字,一致的行为表现会让接口更容易使用,并且不会发生错误。接口尽量不要试图让调用者做些事情,因为它可能不听话。
■总结■
1.合理考虑接口的输入,并尽可能的阻止错误数据的输入
2.阻止错误的方法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
3.给接口一个容易理解的名字,告诉别人接口是干什么用的
4.让接口的功能与其行为保持一致
5.在系统中如果是做同一件事的,尽可能的保持接口名字的一致
6.尽可能少的期待调用者要做的事情,因为不能保证调用者会按照要求去做