Skip to content
平兄聊技术
Go back

接口设计之我见

这里的接口,并非java 中的interface 关键字。不过也是,众所周知,c++ 是没有这个关键字的。那么我们怎么声明一个类,这个类必须满足某种特定的操作呢,即我们怎么去声明一个接口呢?稍熟悉c++语法的同学都知道,在一个类中,声明一个纯虚函数,这时这个类就变成了一个纯虚类,即接口。

class MyInterface {
 public:
 virtual void totalVirtualFunc() = 0; // 子类必须实现的,接口
 virtual void normalVirtualFunc(); // 子类可重写的方法
 void unOverloadFunc(); // 子类重写了也没用的方法(使用父类指针/引用调用无效)
};

好了,基础知识就复习到这里。

其实刚入门时,我遇见publicprotectedprivate 关键字时,心里也曾犯嘀咕:为什么要把好端端的一个类,分成3个不同的访问权限呢。把所有的方法、数据都声明为public,让需要的模块能够顺利的拿到数据,不需要的模块,也不会轻易去碰那些数据,这样不好(香)吗?当时的想法颇有一点“无为而治”的味道,充分信任每一个人/每一个模块,不会乱来。既然人家能够 access 我的源码,那么人家想要乱来也可以来改我的源码来达到其目的啊,何必多此一举呢?

我的思路的改变,是从听到设计模式老师的一句话开始的。“接口是框架的设计者与使用者的沟通方式”。

大家平时码代码时都会有用到别人框架的时候。或者把范围缩小一点,有一个功能两个人协同开发,一个人写后面的数据逻辑,另一个人写UI展示。两个人分别产出class Logicclass Display 两个类。当逻辑完成后,UI类应该如何调用逻辑类产生的数据呢?自然,两人可以 1)约定好调用的方式,也可以 2)写个文档,或者注释来说明一下。鉴于程序员大多都很讨厌写文档,写注释(大家都谜之自信:我的代码写的如此清晰,一眼就能看懂何须注释。然后三个月后看自己的代码,遂卒),第二点不太实际。那么第一点呢?第一点就变成了我们说的“口口相传”。模块的调用,需要靠模块的维护者反复回答别人的口头提问,来告诉别人其调用逻辑。这时候,再看看通过合理的声明访问域,来如何达到我们的目的吧。即大神们通常说的,“自描述的代码”/“代码即文档”。

那么先看看作为调用者视角,我们应该来怎么看代码。调用者就好比是这一系列功能的需求方,了解了需求者的期望,方能理解这个需求的本质。作为调用者,一个类的public域是你唯一能够访问到的域,那么显然,这些才是你该关心的;而private中域,是你完全接触不到的,忽略之。如果是框架调用者,你会发现框架的设计者(已经写好的框架代码)具有调用你的代码的能力(当时还未完成的代码),那么这便是protected的作用所在了。显然框架设计者是通过父类指针/引用来调用了子类,即你的实现。通过protected,框架设计者告诉后续的实现者,这些是你在实现你的子类时需要用到的方法。至于数据,通常是不暴露给外部的。

这些调用者该如何使用的这个模块的信息,都是通过“接口”,来告诉对方的,而不是文档或者口口相传。

通过访问层级的划分,其实你相当于给了调用者一份简要的文档,告诉他该如何调用,如何继承。让他们能从头文件中就快速得以一窥他们所需要的信息。

但还有些问题没有解决。访问层级的划分,只得其一窥,若想得其全貌,还有其他信息要获得,比如调用顺序,即调用getMyVariable 的前置条件。譬如,获取一个文件内容,自然要先要打开文件,这是调用者非常intuitive的想法吧,那么他就期望在公开接口中看到类似open/init这样的函数,来做初始化。否则,那只能默认为在构造函数中已经完成了所必须的初始化。

调用限制应该在接口名称中就有所体现。所谓“上兵伐谋”,在代码中应如是观。

“其次伐交”者,如果情况复杂,名称难以体现,则应该把接口做得足够健壮。进入接口的第一件事应该是检查是否符合调用的约定,前置条件是否满足。如果不满足前置条件,则应该拒绝服务,必要时给出错误码(或你认为能够足够表达原因的出错表达)。

“其下攻城”者,如果又没有办法来检查前置条件,那就只好在接口的声明处,注释说明下,该调用的前置信息。现代IDE通常都可以在鼠标悬浮处显示出声明处的注释的。

可不要一言不合就crash。不过,响亮的出错,也好过用错误的数据继续执行。

以上便是我在设计模块接口时的一些考虑。若有其他好的经验,欢迎后台分享,我帮你们po出来。(新开的公众号貌似都不给评论了)


Share this post on:


Previous Post
拾捯荒草
Next Post
Proactor & Reactor