API设计小手册(下) — 设计流程和设计原则

设计流程

设计一套API可能要几人年的工作量。设计的每一步都是完善的过程,当然也可能是搞砸API的过程。以下的原则可能有助于你更好地设计API。

仔细研究需求

设计之前要仔细研究需求,知道需要的是什么。多咨询大家,比如你老板同事和用户,看他们想要怎样的功能。

Qt4.3的MDI实现之前就在内部开发邮件列表上征求了很多人的意见。大家对以前的MDI框架存在的问题和没有的功能进行了深入的讨论,对API设计的帮助很大。

设计之前先写用例

一般设计API的通病是先实现功能,然后设计API,最后发布。其实应该先设计再实现。

开始设计API之前,先写几个使用这套API的代码片段。在这个阶段先不要考虑实现的难度。用例写完,API的雏形也就出来了。总的原则是“让事情简化,不可能变为可能”。

一个例子就是QWizard。QWizard有两种,一种是简单的线性Wizard,不能跳来跳去的,另一种是复杂的。经过使用用例我们发现简单的Wizard可以看作是复杂Wizard的一个特例,这样就简化了API设计。

研究同一类库中类似的API设计

要设计XmlQuery,一个好的办法就是参考本类库中的SqlQuery。这两个概念很相似,都是完成查询,浏览结果,显示等。熟悉SqlQuery的用户不用费劲就能学会新的API。你也可以参考SqlQuery的构架方式,减轻设计工作量。

当然,完全照抄也是很傻的。你应该批判地继承,加以发展。首先完善这个设计,然后加以学习。

如果要给一套API写新版本,第一件事就是要透彻地了解这套API。不要全盘否定旧的设计,不要试图代替,而要创造性地设计。为了兼容的需要,你可能要包含所有上一版本的功能。

脑残的例子:Qt4.0中,QDockWindow被改名为QDockWidget,没有任何原因;QTextEdit::setOverwriteMode()被取消了,后来4.1又重新加入。

先设计,后实现

实现API之前,要确定API的语法。对于用户很多的库,认可你自己的实现麻烦复杂一些,也要让用户用着简单直接。

Qt4中,可以先生成一个QWidget,以后再设置它的parent。在4.0和4.1中,这将会在后台创造一个窗口句柄,开销很大。在4.2 中,实现了 delayed window creation,解决了这个问题,这是一个很好的API驱动设计的例子。在Qt3里,这个问题是通过QWidget::recreate()解决的。这个API就是纯粹为了实现而实现的。

要记住,API和它的语法才是库提供的最终产品。很多产品的实现变了多次,但是API设计始终如初,如UNIX/POSIX, OpenGL, QFileDialog。

实现API的过程中,要不断写unit test。这样你才能发现很多漏洞和空白,然后细化你的设计。但是不要让实现细节过多影响API的设计,除非是为了一些特殊的原因如性能。

QGraphicsScene::setBspTreeDepth() 就是这样一个例子。这个API纯粹是为了提高性能。用户控制BSP树的深度可以提高性能,但是大多数情况下,系统缺省的树深度也可以满足性能需要。因此这个API用了一个比较专业的词Bsp,表明了这是深入到API实现内部的一个高级API。初哥一看这个词不认识就不会轻易尝试了。

找人帮你评测API

你应该像孙子一样乞求别人多给你的API一些评测意见,特别是负面的。这些意见更能帮助你改进设计。

多写几个例子程序

设计好API后,一定要写几个例子。你可以使用设计之前写的用例。如果有人能帮你写这些例子程序那就更好了。

Qt所带的Class Wizard和License Wizard例子都来自于设计用例。

做好扩展的准备

有两类人会扩展你的API:API维护者:他们会增减你的API接口;用户:他们会通过定制和继承来丰富你API的功能。

扩展性设计要仔细分析实际的目标。对于那些有虚函数的类,至少要试着写3个子类来验证这些API实现了所有需要的功能,这个我们一般叫作“3个原则”。

在设计Qt4.0的时候,QAbstractSocket设计得就不怎么好。Qt4.3要加入QSSLSocket的时候,我们不得不手工降格其中好几个API,因为他们没被设计成虚函数。好在它们是在同一个库中,可以用“手工多态”解决,否则悲剧就无法避免。

内部API没评测之前不要发布

有些API一开始是内部使用的,后来大家觉得很有用,才公开发布。一个常见的错误就是发布之前没有进行完整的测试。比如Qt就曾经发布过带有拼写错误的API,不堪回首。

宁缺毋滥

如果对API的功能不是很确定,万万不要发布,宁可暂时当作内部API,或者日后再说。

用户的反馈很重要,但是实现用户所期待的所有功能是不可能的。一般等3个客户要求同样的功能后再实现是比较明智的。

设计原则

这里罗列了一些API设计的基本原则,大部分都来自实际的API设计经验。其中有些看似冲突,但是其实都有道理。掌握尺度的是你自己,没有什么能替代你自己的思考,原则只是原则而已。

命名

名字要能解释自己,要遵从英语语法。QPainterPath的作者建议,在文档里把它叫做vector path,因为这是大家通用的叫法。另一个例子是MDI,尽管实现的是MDI,在Qt4.2之前却叫做QWorkspace。在4.3之后,改为了QMdiArea。

另外,参数的命名也要清楚明白。尽量少用bool类型的参数,这样的代码不好读。QWidget::repaint()就带了一个bool类型的参数,来指示是否在重画之前擦除背景。如果有repaint(false)这样的代码,就很容易让人误会,到底是不是不要repaint还是怎样?解决的方法之一就是用枚举代替bool,如

repaint(QWidget::eraseBackground);

命名要统一。不要混用类似widget和control这类词语,这会让用户乱猜。参数的顺序也要一致,比如画方框的函数参数为(x,y,width,height),别的地方也要类似,不要弄成(x,width,y,height)。

比如QStackArray,是一种变长的数组。由于用了stack这个词,很容易和QStack混淆。4.1之后,这个类被改为QVarLengthArray。

了解你的用户也很重要。比如你实现了一套关于XML的API,名字里带有XML就是一个很好的主意。如果你自认为API很高档,一定要叫做什么IDREFs或者NCNames,用户会很讨厌的。

命名是API设计的一项重要内容。你设计的名字可能会出现在一些IDE的自动完成功能中,这些名字和参数名必须意义明确,简短有力。尤其要避免一个字母长度的参数名。

避免二义性

一个名字要严格对应一个概念。假如你有两种事件传递机制,一个是同步的,一个是异步的,分别叫做sendEventNow()和 sendEventLater()就不错。如果用户必须了解同步异步概念,你也可以叫做sendEventSynchronously()和 sendEventAsynchronously()。

如果你要鼓励用户多用同步方式,可能会把同步的方法改为sendEvent()。如果你希望用户用异步方式,就可以反过来把异步方法命名为sendEvent()。

Qt中的sendEvent()是同步的,postEvent()是异步的。这里就利用了英语中send和post的微妙语义差别。

在命名复制初始化函数的参数时尤其要注意。下列代码:

Car &Car::operator=(const Car &car) {     m_model=car.m_model;     m_year=car.m_year;     ...     return *this; }

这段代码很不好,两个car很容易混淆。

注意完整性

API设计跟写书一样,要注意对称和前后照应。格式尽量一样,过程尽量一样,这样读者能更容易了解你的意图。比如所有的set函数都用set开头,这样用户更容易习惯。

在 Qt3中,有一个函数QStatusBar::message(text,msecs)能在状态条上显示一条信息msecs毫秒。但这个函数怎么看都像一个get函数。Qt4中,我们曾考虑改名为setMessage()以达到一致性。但是setMessage有两个参数,不太像set函数,最终我们决定改名为showMessage(),以便区分。

再看event那个例子。同步的时候,可以把event对象当作参数传递,因为马上就会返回,函数可以直接删除局部变量。但是异步时,就要创建一个新对象,完成以后删掉,否则就会有内存泄露。所以我们应该把两个API分别设计成:

sendEventNow(Event event); sendEventLater(Event* event);

以避免用户乱用。很不幸地,Qt在这里犯了脑残的错误,sendEvent和postEvent都是接受Event*的参数,这就很容易造成内存泄露。当然,为了一致,你可以定义两个都接受Event*,然后自己管理event对象的生存期,这样效率很低下但是很安全。有时候我们就是要在平衡之间作出选择。

别用缩写

尽量避免缩写。当然有一些常见的例外,如min,max,dir,rect,prev。但是要注意有一致性,不能有的用有的不用。Qt本身在这方面做得其实相当不好。对于参数命名来说,可以适当放宽限制,但是也要保证意思清楚明晰。

名字要专不要通

API的名字空间是很宝贵的。尽量用专用名,否则一旦通用的名字被用了,以后就很难有机会收回来。QRegExp其实被叫做QStringPattern也很恰当,但是这个名字太通用了,所以最后还是选择了QRegExp。

比如你要给SQL添加一个错误报告类,最好叫做SqlErrorHandler而不是ErrorHandler,否则将来很难与XmlErrorHandler作出区分。将来扩展库的时候,如果要用到ErrorHandler作为基类也不会头疼。

Qt在某些方面做得也很不好。比如QDom系列类,就没有区分SAX和DOM的分支,这造成了一定的混乱。

不要太过迁就下层API

如果你要包装一系列API,不要被它的命名方式所支配。按照你自己的命名规则统一命名方式。你设计的目的是让用户使用方便高效,而不是迁就下层的库。

选择合适的缺省值

在Qt中设计一个按钮很容易:

QPushButton * button=new QPushButton(text,paret);

如果你编过Cocoa程序,你就知道,要生成一个按钮要设置9个参数,而99%的时间你选择的初始参数都是一样的。为什么不用缺省值呢?这就是Qt聪明的地方。尽量让你的客户省事,猜测他们需要什么缺省值,不要让他们费劲,隐藏不必要的细节,这就是API的设计之道。

通过选择合适的缺省值,不仅可以减少代码量,还可以让API简单可预测。尤其当你有bool类型参数的时候,尽量让缺省值为false。不要以为参数越多API就越强大,你需要的是易用的API。

不要自作聪明

API应该简单清楚,尽量少让用户产生惊讶的感觉。如果过于自作聪明把API弄得不易用,就远离了API的本来目的。尽量贴近你用户的习惯而不是试图教他们怎么做,否则你就等着写文档去吧。

Qt3 的QLabel就是一个自作聪明的例子。QLabel::setText()集成了显示普通文本和html文本的功能。貌似节省了一个API,但是这样很容易被客户误用。如果客户想显示一些html的源代码,还必须调用setTextFormat(),大部分人并不知道这个从而变得无所适从。避免自作聪明的方法是分开两个setText()和setHtml()。

注意边界值

对于类库来说,边界值的处理相当重要,认为边界值发生概率很小就不加注意是很幼稚的。边界值造成的问题会在使用这个类的其他类中得到扩散和放大。比如字符串查找函数有边界值问题,在正则表达式中,这个问题很可能就会被放大。

处理边界值的一个常见错误是在函数开始的时候就加入边界检查。这样做大多数时候并不是必要的。建议你先按照正常的情况进行处理,最后才对边界值进行处理,这样可以提高API的效率。另外就是要记得在unit test中加入边界值的测试。

小心定义虚API

虚API一般更难定义,并且很容易在新版本发布时出错。这个问题叫做“fragile base class problem”。设计虚API时,要注意以下两个问题:

第一是定义的虚API太少,以后发现不够用。一开始很难知道将来要用什么样的API,要用多少。万一定义的API不够用,会限制用户的扩展功能。

第二个就是滥用virtual。在C++中,虚函数效率是很低的。如果你的类并不需要扩展这个功能,就不要定义成虚函数,否则不仅效率低下,还会误导使用者。

设计API时,你必须全盘考虑,逐个过滤来决定哪些API应该是虚的,哪些不应该是,在文档里应该详细说明你的类如何使用这些虚方法。

在C++里,大部分虚函数应该被声明为保护的,以保证不被错误修改调用影响其他类的访问。

Qt4 的QIODevice就是一个很好的例子。公用API为read(),write(),而虚函数为readData()和writeData()。这样就避免了访问混乱的情况发生。QWidget也类似,公用API为show(),resize(),repaint(),而虚函数为 showEvent(),resizeEvent(),paintEvent()。

C++的一个很操蛋的地方就是加入虚函数肯定会破坏二进制兼容。有一个很恶心的办法可以避免这个问题,那就是定义一个通用的虚函数占位:

virtual void virtual_hook(int id, void * data);

结构性

很多API在创建对象的时候要求用户指定一大堆的属性,比如Win32编程:

m_hWindow = ::CreateWindow("AppWindow", /* class name */ m_pszTitle, /* title to window */ WS_OVERLAPPEDWINDOW, /* style */ CW_USEDEFAULT, /* start pos x */ CW_USEDEFAULT, /* start pos y */ m_nWidth, /* width */ m_nHeight, /* height */ NULL, /* parent HWND */ NULL, /* menu HANDLE */ hInstance, /* */ NULL); /* creatstruct param */

这么多参数对于用户来说是个噩梦。一般现代API会采用另外一种方式,就是基于属性的设计。这样用户就可以用很多行代码慢慢设计一个类实例,不需要干预的非必须属性完全可以不管。

window = new Window; window->setClassName("AppWindow"); window->setWindowTitle(winTitle); window->setStyle(Window::Overlapped); window->setSize(width, height); window->setModuleHandle(moduleHandle);

这样做有多个优点:
* 看起来比较简单
* 不用记住参数的顺序
* 可读性强,不需要特别说明注释
* 属性可以有缺省值,不是必须指定所有的属性
* 随时可以更改属性
* 可以随时取得属性,便于除错
* 方便进行可视化图形化设计

对于开发库的牛人来说,对此要多多考虑一层。因为属性设置的顺序不确定,一般要进行”lazy initialization”来避免每一个属性变化的时候都重新初始化整个对象。

比如 QRegExp,用户可以这样初始化:

QRegExp regExp("*.wk?", Qt::CaseInsensitive, QRegExp::Wildcard);

也可以这样初始化:

QRegExp regExp; regExp.setPattern("*.wk?"); regExp.setCaseSensitivity(Qt::CaseInsensitive); regExp.setPatternSyntax(QRegExp::Wildcard);

在实现中,QRegExp把编译表达式的过程延后到第一次使用时,避免了多次编译。

最高境界是手中无剑

剑客的最高境界是手中无剑,心中有剑。最好的API是让用户完全不觉得在用你的API,而是在用他们最熟悉的工具,完全没有障碍和隔阂。

在Qt3中,QWidget的最大限制是32768×32768。在Qt4中,已经没有了这种限制。Qt4还增加了pdf格式的支持,StyleSheet支持,OpenGL支持。虽然这些功能很强大,但是API接口并没有太大的变化,用户体验并没有太多变化,也不用花费太多时间重新学习。在用户不知不觉之间,新的功能,新的API已经进入了用户的视野。用户虽然浑然不知却已不知不觉获得了更加强大的工具而进入了编程的自由王国。什么时候,你能让用户忘记API而快乐自然地使用你提供的功能时,你会发现自己已然是个API设计大师了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本文档是QT中文版本 内容详尽,下面是片段 信号 void activated ( int id ) 静态公有成员 QKeySequence shortcutKey ( const QString & str ) QString keyToString ( QKeySequence k ) (obsolete) QKeySequence stringToKey ( const QString & s ) (obsolete) 保护成员 virtual bool eventFilter ( QObject * o, QEvent * e ) -------------------------------------------------------------------------------- 详细描述 QAccel类用来处理键盘的加速键和快捷键。 键盘加速键是在某个组合键按下的时候出发一个动作,加速键可以处理窗口部件和它子部件里所有的键盘动作所以它不会被键盘焦点所影响。 在大多数情况下,你不需要直接使用这个类。使用QAction类建立的具有加速键的动作可以同时在菜单和工具栏里使用。如果你的兴趣只是在菜单里使用QMenuData::insertItem()或者QMenuData::setAccel()建立只作用在菜单里的加速键。那么许多窗口部件可以自动的生成加速键,比如QButton、QGroupBox、QLabel(使用QLabel::setBuddy())、QMenuBar和QTabBar。实例: QPushButton p( "&Exit", parent ); // 自动使用快捷键ALT+Key_E QPopupMenu *fileMenu = new fileMenu( parent ); fileMenu->insertItem( "Undo", parent, SLOT(undo()), CTRL+Key_Z ); QAccel包括一个加速键的列表,这个列表里的项目可以使用insertItem()、removeItem()、clear()、key()和findKey()。 每一个加速键项目是由一个标示符和 QKeySequence组成。一个单独的键组是由一个键盘码组合上改变符形成的(SHIFT,CTRL,ALT 或者 UNICODE_ACCEL)。例如,CTRL + Key_p可以作为文本打印的快捷键。这个键的键盘码在qnamespace.h里列出。还有,使用UNICODE_ACCEL可以使字符以统一码(unicode)的形式表现出来。例如 UNICODE_ACCEL + 'A' 所给出的加速键和Key_A是一样的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值