精华内容
下载资源
问答
  • API 设计规范

    千次阅读 2020-09-12 18:15:55
    文章目录目录API 设计规范Design-First(设计优先)Code-First(编码优先) API 设计规范 通常,设计 API 规范有两个方向,Design-First(设计优先) 或 Code-First(编码优先)。 Design-First(设计优先) 即优先...

    目录

    API 实现方式

    通常,设计 API 规范有两个方向,Design-First(设计优先) 或 Code-First(编码优先)。

    Design-First(设计优先)

    即优先设计 API 规范,设计完成后再着手进行代码开发工作。推荐使用 OpenAPI-GUI v3 来设计 API 描述文件。

    采用 Design-First 就意味着,将设计 API 路由、参数等工作提前,后续整个软件开发的流程都需要围绕着 API 规范为核心,当然这需要有一定的设计经验的开发人员才能胜任。

    Design-First 有很多好处:

    • 提高开发效率。开发团队将根据 API 规范进行并行开发和对接工作,而无需等待接口逻辑开发完毕。
    • 降低接口开发的成本,无需修改代码逻辑即可轻松地修改 API 规范,因为 API 描述语言(如:OpenAPI)与编码语言无关。
    • 开发人员更加专注于设计 API 规范,对比 Code-First 可以描写更多 API 的细节,如:校验规则、范例数据等,同时开发人员对 API 的全局一致性、扩展性、规范性也有更好的把控。
    • 在联调开发的过程中可以提前发现和解决问题,避免问题在开发完毕后修改的成本过高。
    • 由于 API 描述更加标准化,可以方便做更多的 API 生态延伸,比如基于 API 规范生成 Mock API Server,开发 API 自动化测试代码,接入 API 网关等。

    Code-First(编码优先)

    即通过代码中关于 API 描述特性、注解或注释自动生成 API 描述文件的设计方式,如:JAVA 生态的 SpringFox。

    适合倾向于在代码上编写 API 规范,通过自动化设施自动生成文档的团队。

    Code-First 的优点:

    • 节省时间。对于 API 开发者,编码的同时可以获得一份满足基本要求的 API 文档。
    • 方便接入自动化 CI/CD 流程中。

    虽然 Code-First 省去了开发者设计 API 描述文件的阶段,提高了 API 开发者的效率,但是从整个团队的角度来看,效率并不一定提升了,反而有可能降低了效率。

    不同 API 开发者的经验和习惯的不同,极有可能在编码的过程中对 API 的限制条件考虑不全,又或者框架生成 API 文档的程序完善度不够,种种因素导致最终生成的 API 的描述无法达到理想标准。

    而很多 API 开发者习惯开发完成后才推送代码,并生成 API 文档,也导致了团队的进程阻塞于此,拖后了整个团队的开发进程。另一方面,API 在开发完成如果没有测试,很有可能导致 API 对接者在对接的过程中遇到重重阻碍。

    如果使用 Code-First 设计方向,建议:

    • 选用完善程度比较高的生成组件。
    • 对 API 的描述尽可能的细致、完整。
    • 优先设计路由、请求、响应等规则,详细的逻辑代码在 API 设计完成后再着手开发。

    API 设计规范

    为用户设计 API

    API 并不是用来盲目的暴露一些数据或业务处理能力的,它就像我们每天使用的任何形式的接口一样(例如:微波炉的操作按钮),用来帮助用户完成他们的目标。所以,需要从用户的视角来决定一个 API 的设计是否友好,在设计 API 的时候要充分考虑 UI/UE 的操作流程。

    反之,如果以开发者的视角去设计 API,那么通常的后果是开发出的 API 往往都偏重与于功能的实现过程和原理,而不是用户如何能简单平滑的使用这个 API 来达到他们的目的。

    所以,一个好的 API 设计一定是面向用户的,充分隐藏底层复杂原理的。要设计出让用户容易理解和容易使用的 API。

    识别 API 的目标

    识别 API 的目标,最基本的需要对以下方面有深刻且精准的认识:

    • Who,谁使用这个 API?
    • What,用户拿这个 API 能做什么事?
    • How,用户如何做这件事?
    • What Need,用户想要做这件事的话还需要什么?
    • What Return,用户会得到什么?

    Mock API Server

    Mock API Server 基于 API 描述文件自动生成 Mock API,通过提供真实 API 响应的范例数据来模拟真实的 API 服务,并且支持路由及参数校验。

    使用场景

    1. API 对接/调试:通常在公司项目中,API 使用者(如:前端、App、自动化 API 测试开发人员)的开发进度会比后端 API 开发人员提前开始开发实现。而使用基于 API 文档的 Mock API 可提供模拟真实 API 响应的沙盒环境,以便 API 使用者提前开始调试工作。另一方面 API 使用者可以及时反馈 API 设计问题,在完成 API 实现之前提早完善 API 设计,使得 API 开发工作更加高效和趋于完美。
    2. API 测试:当需要对部分 API 进行统一测试时,可以替换其他 API 为 Mock API,而无需关心其他依赖 API 是否满足测试条件。
    3. 外部 API 服务:通常外部 API 服务可能会有不可靠、收费、访问限制等情况,所以可以替换外部 API 服务为 Mock API,通过 Mock 外部 API 服务的真实数据来调试程序逻辑。

    使用步骤

    1. API 规范设计:OpenAPI 规范。
    2. 生成范例数据 :Swagger Editor 可自动通过参数生成 Example。
    3. 生成 Mock API:Swagger CodeGen 可以自定生成 Mock API Server。
    展开全文
  • API设计原则

    千次阅读 2017-09-22 23:11:01
    原文链接:API Design Principles – Qt Wiki 基于Gary的影响力上 Gary Gao ...此文既是Qt官网上的API设计指导准则,也是Qt在API设计上的实践总结。虽然Qt用的是C++,但其中设计原则和思考是具有普适性的(如果

    原文链接:API Design Principles – Qt Wiki
    基于Gary的影响力上 Gary Gao 的译文稿:C++的API设计指导

    译序

    api design

    Qt的设计水准在业界很有口碑,一致、易于掌握和强大的API是Qt最著名的优点之一。此文既是Qt官网上的API设计指导准则,也是Qt在API设计上的实践总结。虽然Qt用的是C++,但其中设计原则和思考是具有普适性的(如果你对C++还不精通,可以忽略与C++强相关或是过于细节的部分,仍然可以学习或梳理关于API设计最有价值的内容)。整个篇幅中有很多示例,是关于API设计一篇难得的好文章。

    需要注意的是,这篇Wiki有一些内容并不完整,所以,可能会有一些阅读上的问题,我们对此做了一些相关的注释。

    PS:翻译中肯定会有不足和不对之处,欢迎评论&交流;另译文源码在GitHub的这个仓库中,可以提交Issue/Fork后提交代码来建议/指正。

    API设计原则

    一致、易于掌握和强大的API是Qt最著名的优点之一。此文总结了我们在设计Qt风格API的过程中所积累的诀窍(know-how)。其中许多是通用准则;而其他的则更偏向于约定,遵循这些约定主要是为了与已有的API保持一致。

    虽然这些准则主要用于对外的API(public API),但在设计对内的API(private API)时也推荐遵循相同的技巧(techniques),作为开发者之间协作的礼仪(courtesy)。

    如有兴趣也可以读一下 Jasmin Blanchette 的Little Manual of API Design (PDF) 或是本文的前身 Matthias Ettrich 的Designing Qt-Style C++ APIs

    1. 好API的6个特质

    API之于程序员就如同图形界面之于普通用户(end-user)。API中的『P』实际上指的是『程序员』(Programmer),而不是『程序』(Program),强调的是API是给程序员使用的这一事实。

    在第13期Qt季刊Matthias 的关于API设计的文章中提出了观点:API应该极简(minimal)且完备(complete)、语义清晰简单(have clear and simple semantics)、符合直觉(be intuitive)、易于记忆(be easy to memorize)和引导API使用者写出可读代码(lead to readable code)。

    1.1 极简

    极简的API是指每个class的public成员尽可能少,public的class也尽可能少。这样的API更易理解、记忆、调试和变更。

    1.2 完备

    完备的API是指期望有的功能都包含了。这点会和保持API极简有些冲突。如果一个成员函数放在错误的类中,那么这个函数的潜在用户就会找不到,这也是违反完备性的。

    1.3 语义清晰简单

    就像其他的设计一样,我们应该遵守最少意外原则(the principle of least surprise)。好的API应该可以让常见的事完成的更简单,并有可以完成不常见的事的可能性,但是却不会关注于那些不常见的事。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在Qt 3中,QMimeSourceFactory不应命名成QImageLoader并有不一样的API。)

    1.4 符合直觉

    就像计算机里的其他事物一样,API应该符合直觉。对于什么是符合直觉的什么不符合,不同经验和背景的人会有不同的看法。API符合直觉的测试方法:经验不很丰富的用户不用阅读API文档就能搞懂API,而且程序员不用了解API就能看明白使用API的代码。

    1.5 易于记忆

    为使API易于记忆,API的命名约定应该具有一致性和精确性。使用易于识别的模式和概念,并且避免用缩写。

    1.6 引导API使用者写出可读代码

    代码只写一次,却要多次的阅读(还有调试和修改)。写出可读性好的代码有时候要花费更多的时间,但对于产品的整个生命周期来说是节省了时间的。

    最后,要记住的是,不同的用户会使用API的不同部分。尽管简单使用单个Qt类的实例应该符合直觉,但如果是要继承一个类,让用户事先看好文档是个合理的要求。

    2. 静态多态

    相似的类应该有相似的API。在继承(inheritance)合适时可以用继承达到这个效果,即运行时多态。然而多态也发生在设计阶段。例如,如果你用QProgressBar替换QSlider,或是用QString替换QByteArray,你会发现API的相似性使的替换很容易。这即是所谓的『静态多态』(static polymorphism)。

    静态多态也使记忆API和编程模式更加容易。因此,一组相关的类有相似的API有时候比每个类都有各自的一套API更好。

    一般来说,在Qt中,如果没有足够的理由要使用继承,我们更倾向于用静态多态。这样可以减少Qt public类的个数,也使刚学习Qt的用户在翻看文档时更有方向感。

    2.1 好的案例

    QDialogButtonBoxQMessageBox,在处理按钮(addButton()setStandardButtons()等等)上有相似的API,不需要继承某个QAbstractButtonBox类。

    2.2 差的案例

    QTcpSocketQUdpSocket都继承了QAbstractSocket,这两个类的交互行为的模式(mode of interaction)非常不同。似乎没有什么人以通用和有意义的方式用过QAbstractSocket指针(或者  以通用和有意义的方式使用QAbstractSocket指针)。

    2.3 值得斟酌的案例

    QBoxLayoutQHBoxLayoutQVBoxLayout的父类。好处:可以在工具栏上使用QBoxLayout,调用setOrientation()使其变为水平/垂直。坏处:要多一个类,并且有可能导致用户写出这样没什么意义的代码,((QBoxLayout *)hbox)->setOrientation(Qt::Vertical)

    3. 基于属性的API

    新的Qt类倾向于用『基于属性(property)的API』,例如:

    1
    2
    3
    4
    QTimer timer;
    timer.setInterval(1000);
    timer.setSingleShot(true);
    timer.start();

    这里的 属性 是指任何的概念特征(conceptual attribute),是对象状态的一部分 —— 无论它是不是Q_PROPERTY。在说得通的情况下,用户应该可以以任何顺序设置属性,也就是说,属性之间应该是正交的(orthogonal)。例如,上面的代码可以写成:

    1
    2
    3
    4
    QTimer timer;
    timer.setSingleShot(true);
    timer.setInterval(1000);
    timer.start();

    【译注】:正交性是指改变某个特性而不会影响到其他的特性。《程序员修炼之道》中讲了关于正交性的一个直升飞机坠毁的例子,讲得深入浅出很有画面感。

    为了方便,也写成:

    1
    timer.start(1000);

    类似地,对于QRegExp会是这样的代码:

    1
    2
    3
    4
    QRegExp regExp;
    regExp.setCaseSensitive(Qt::CaseInsensitive);
    regExp.setPattern(".");
    regExp.setPatternSyntax(Qt::WildcardSyntax);

    为实现这种类型的API,需要借助底层对象的懒创建。例如,对于QRegExp的例子,在不知道模式语法(pattern syntax)的情况下,在setPattern()中去解释"."就为时过早了。

    属性之间常常有关联的;在这种情况下,我们必须小心处理。思考下面的问题:当前的风格(style)提供了『默认的图标尺寸』属性 vs. QToolButton的『iconSize』属性:

    1
    2
    3
    4
    5
    6
    toolButton->setStyle(otherStyle);
    toolButton->iconSize();    // returns the default for otherStyle
    toolButton->setIconSize(QSize(52, 52));
    toolButton->iconSize();    // returns (52, 52)
    toolButton->setStyle(yetAnotherStyle);
    toolButton->iconSize();    // returns (52, 52)

    提醒一下,一旦设置了iconSize,设置就会一直保持,即使改变当前的风格。这 很好。但有的时候需要能重置属性。有两种方法:

    1. 传入一个特殊值(如QSize()-1或者Qt::Alignment(0))来表示『重置』
    2. 提供一个明确的重置方法,如resetFoo()unsetFoo()

    对于iconSize,使用QSize()(比如 QSize(–1, -1))来表示『重置』就够用了。

    在某些情况下,getter方法返回的结果与所设置的值不同。例如,虽然调用了widget->setEnabled(true),但如果它的父widget处于disabled状态,那么widget->isEnabled()仍然返回的是false。这样是OK的,因为一般来说就是我们想要的检查结果(父widget处于disabled状态,里面的子widget也应该变为灰的不响应用户操作,就好像子widget自身处于disabled状态一样;与此同时,因为子widget记得在自己的内心深处是enabled状态的,只是一直等待着它的父widget变为enabled)。当然诸如这些都必须在文档中妥善地说明清楚。

    4. C++相关

    4.1 值 vs. 对象

    4.1.1 指针 vs. 引用

    指针(pointer)还是引用(reference)哪个是最好的输出参数(out-parameters)?

    1
    2
    void getHsv(int *h, int *s, int *v) const;
    void getHsv(int &h, int &s, int &v) const;

    大多数C++书籍推荐尽可能使用引用,基于一个普遍的观点:引用比指针『更加安全和优雅』。与此相反,我们在开发Qt时倾向于指针,因为指针让用户代码可读性更好。比较下面例子:

    1
    2
    color.getHsv(&h, &s, &v);
    color.getHsv(h, s, v);

    只有第一行代码清楚表达出hsv参数在函数调用中非常有可能会被修改。

    这也就是说,编译器并不喜欢『出参』,所你应该在新的API中避免使用『出参』,而是返回一个结构体,如下所示:

    1
    2
    struct Hsv { int hue, saturation, value };
    Hsv getHsv() const;

    【译注】:函数的『入参』和『出参』的混用会导致 API 接口语义的混乱,所以,使用指针,在调用的时候,实参需要加上“&”,这样在代码阅读的时候,可以看到是一个『出参』,有利于代码阅读。(但是这样做,在函数内就需要判断指针是否为空的情况,因为引用是不需要判断的,所以,这是一种 trade-off)

    另外,如果这样的参数过多的话,最好使用一个结构体来把数据打包,一方面,为一组返回值取个名字,另一方面,这样有利用接口的简单。

    4.1.2 按常量引用传参 vs. 按值传参

    如果类型大于16字节,按常量引用传参。

    如果类型有重型的(non-trivial)拷贝构造函数(copy-constructor)或是重型的析构函数(destructor),按常量引用传参以避免执行这些函数。

    对于其它的类型通常应该按值传参。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void setAge(int age);
    void setCategory(QChar cat);
    void setName(QLatin1String name);
     
    // const-ref is much faster than running copy-constructor and destructor
    void setAlarm(const QSharedPointer<Alarm> &alarm);
     
    // QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect
    // are good examples of other classes you should pass by value.

    【译注】:这是传引用和传值的差别了,因为传值会有对像拷贝,传引用则不会。所以,如果对像的构造比较重的话(换句话说,就是对像里的成员变量需要的内存比较大),这就会影响很多性能。所以,为了提高性能,最好是传引用。但是如果传入引用的话,会导致这个对象可能会被改变。所以传入const reference。

    4.2 虚函数

    在C++中,当类的成员函数声明为virtual,主要是为了通过在子类重载此函数能够定制函数的行为。将函数声明为virtual的目的是为了让对这个函数已有的调用变成执行实际实例的代码路径。对于没有在类外部调用的函数声明成virtual,你应该事先非常慎重地思考过。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // QTextEdit in Qt 3: member functions that have no reason for being virtual
    virtual void resetFormat();
    virtual void setUndoDepth( int d );
    virtual void setFormat( QTextFormat *f, int flags );
    virtual void ensureCursorVisible();
    virtual void placeCursor( const QPoint &pos;, QTextCursor **c = 0 );
    virtual void moveCursor( CursorAction action, bool select );
    virtual void doKeyboardAction( KeyboardAction action );
    virtual void removeSelectedText( int selNum = 0 );
    virtual void removeSelection( int selNum = 0 );
    virtual void setCurrentFont( const QFont &f );
    virtual void setOverwriteMode( bool b ) { overWrite = b; }

    QTextEdit从Qt 3移植到Qt 4的时候,几乎所有的虚函数都被移除了。有趣的是(但在预料之中),并没有人对此有大的抱怨,为什么?因为Qt 3没用到QTextEdit的多态行为 —— 只有你会;简单地说,没有理由去继承QTextEdit并重写这些函数,除非你自己调用了这些方法。如果在Qt在外部你的应用程序你需要多态,你可以自己添加多态。

    【译注】:『多态』的目的只不过是为了实践 —— 『依赖于接口而不是实现』,也就是说,接口是代码抽像的一个非常重要的方式(在Java/Go中都有专门的接口声明语法)。所以,如果没有接口抽像,使用『多态』的意义也就不大了,因为也就没有必要使用『虚函数』了。

    4.2.1 避免虚函数

    在Qt中,我们有很多理由尽量减少虚函数的数量。每一次对虚函数的调用会在函数调用链路中插入一个未掌控的节点(某种程度上使结果更无法预测),使得bug修复变得更复杂。用户在重写的虚函数中可以做很多疯狂的事:

    • 发送事件
    • 发送信号
    • 重新进入事件循环(例如,通过打开一个模态文件对话框)
    • 删除对象(即触发『delete this』)

    还有其他很多原因要避免过度使用虚函数:

    • 添加、移动或是删除虚函数都带来二进制兼容问题(binary compatibility/BC)
    • 重载虚函数并不容易
    • 编译器几乎不能优化或内联(inline)对虚函数的调用
    • 虚函数调用需要查找虚函数表(v-table),这比普通函数调用慢了2到3倍
    • 虚函数使得类很难按值拷贝(尽管也可以按值拷贝,但是非常混乱并且不建议这样做)

    经验告诉我们,没有虚函数的类一般bug更少、维护成本也更低。

    一般的经验法则是,除非我们以这个类作为工具集提供而且有很多用户来调用某个类的虚函数,否则这个函数九成不应该设计成虚函数。

    【译注】:

    1. 使用虚函数时,你需要对编译器的内部行为非常清楚,否则,你会在使用虚函数时,觉得有好些『古怪』的问题发生。比如在创建数组对象的时候。
    2. 在C++中,会有一个基础类,这个基础类中已经实现好了很多功能,然后把其中的一些函数放给子类去修改和实现。这种方法在父类和子类都是一组开发人员维护时没有什么问题,但是如果这是两组开发人员,这就会带来很多问题了,就像Qt这样,子类完全无法控制,全世界的开发人员想干什么就干什么。所以,子类的代码和父类的代码在兼容上就会出现很多很多问题。所以,还是上面所说,其实,虚函数应该声明在接口的语义里(这就是设计模式的两个宗旨——依赖于接口,而不是实现;钟爱于组合,而不是继承。也是为什么Java和Go语言使用interface关键字的原因,C++在多态的语义上非常容易滥用)

    4.2.2 虚函数 vs. 拷贝

    多态对象(polymorphic objects)和值类型的类(value-type classes)两者很难协作好。

    包含虚函数的类必须把析构函数声明为虚函数,以防止父类析构时没有清理子类的数据,导致内存泄漏。

    如果要使一个类能够拷贝、赋值或按值比较,往往需要拷贝构造函数、赋值操作符(operator =)和相等操作符(operator ==)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class CopyClass {
    public:
        CopyClass();
        CopyClass(const CopyClass &other);
        ~CopyClass();
        CopyClass &operator =(const CopyClass &other);
        bool operator ==(const CopyClass &other) const;
        bool operator !=(const CopyClass &other) const;
     
        virtual void setValue(int v);
    };

    如果继承CopyClass这个类,预料之外的事就已经在代码时酝酿了。一般情况下,如果没有虚成员函数和虚析构函数,就不能创建出可以多态的子类。然而,如果存在虚成员函数和虚析构函数,这突然变成了要有子类去继承的理由,而且开始变得复杂了。起初认为只要简单声明上虚操作符重载函数(virtual operators)。 但其实是走上了一条混乱和毁灭之路(破坏了代码的可读性)。看看下面的这个例子:

    1
    2
    3
    4
    class OtherClass {
    public:
        const CopyClass &instance() const; // 这个方法返回的是什么?可以赋值什么?
    };

    (这部份还未完成)

    【译注】:因为原文上说,这部份并没有完成,所以,我也没有搞懂原文具体也是想表达什么。不过,就标题而言,原文是想说,在多态的情况下拷贝对象所带来的问题??

    4.3 关于const

    C++的关键词const表明了内容不会改变或是没有副作用。可以应用于简单的值、指针及指针所指的内容,也可以作为一个特别的属性应用于类的成员函数上,表示成员函数不能修改对象的状态。

    然而,const本身并没有提供太大的价值 —— 很多编程语言甚至没有类似const的关键词,但是却并没有因此产生问题。实际上,如果你不用函数重载,并在C++源代码用搜索并删除所有的const,几乎总能编译通过并且正常运行。尽量让使用的const保持实用有效,这点很重要。

    让我们看一下在Qt的API设计中与const相关的场景。

    4.3.1 输入参数:const指针

    有输入指针参数的const成员函数,几乎总是const指针参数。

    如果函数声明为const,意味着既没有副作用,也不会改变对象的可见状态。那为什么它需要一个没有const限定的输入参数呢?记住const类型的函数通常被其他const类型的函数调用,接收到的一般都是const指针(只要不主动const_cast,我们推荐尽量避免使用const_cast)

    以前:

    1
    2
    3
    bool QWidget::isVisibleTo(QWidget *ancestor) const;
    bool QWidget::isEnabledTo(QWidget *ancestor) const;
    QPoint QWidget::mapFrom(QWidget *ancestor, const QPoint &pos) const;

    QWidget声明了许多非const指针输入参数的const成员函数。注意,这些函数可以修改传入的参数,不能修改对象自己。使用这样的函数常常要借助const_cast转换。如果是const指针输入参数,就可以避免这样的转换了。

    之后:

    1
    2
    3
    bool QWidget::isVisibleTo(const QWidget *ancestor) const;
    bool QWidget::isEnabledTo(const QWidget *ancestor) const;
    QPoint QWidget::mapFrom(const QWidget *ancestor, const QPoint &pos) const;

    注意,我们在QGraphicsItem中对此做了修正,但是QWidget要等到Qt 5:

    1
    2
    bool isVisibleTo(const QGraphicsItem *parent) const;
    QPointF mapFromItem (const QGraphicsItem *item, const QPointF &point) const;

    4.3.2 返回值:const值

    调用函数返回的非引用类型的结果,称之为右值(R-value)。

    非类(non-class)的右值总是无cv限定类型(cv-unqualified type)。虽然从语法上讲,加上const也可以,但是没什么意义,因为鉴于访问权限这些值是不能改变的。多数现代编译器在编译这样的代码时会提示警告信息。

    【译注】:cv-qualified的类型(与cv-unqualified相反)是由const或者volatile或者volatile const限定的类型。详见cv (const and volatile) type qualifiers – C++语言参考

    当在类类型(class type)右值上添加const关键字,则禁止访问非const成员函数以及对成员的直接操作。

    不加const则没有以上的限制,但几乎没有必要加上const,因为右值对象生存时间(life time)的结束一般在C++清理的时候(通俗的说,下一个分号地方),而对右值对象的修改随着右值对象的生存时间也一起结束了(也就是本条语句的执行完成的时候)。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    struct Foo {
        void setValue(int v) { value = v; }
        int value;
    };
     
    Foo foo() {
        return Foo();
    }
     
    const Foo cfoo() {
        return Foo();
    }
     
    int main() {
        // The following does compile, foo() is non-const R-value which
        // can't be assigned to (this generally requires an L-value)
        // but member access leads to a L-value:
        foo().value = 1; // Ok, but temporary will be thrown away at the end of the full-expression.
     
        // The following does compile, foo() is non-const R-value which
        // can't be assigned to, but calling (even non-const) member
        // function is fine:
        foo().setValue(1); // Ok, but temporary will be thrown away at the end of the full-expression.
     
        // The following does _not_compile, foo() is ''const'' R-value
        // with const member which member access can't be assigned to:
        cfoo().value = 1; // Not ok.
     
        // The following does _not_compile, foo() is ''const'' R-value,
        // one cannot call non-const member functions:
        cfoo().setValue(1); // Not ok
    }

    【译注】:上述的代码说明,如果返回值不是const的,代码可以顺利编译通过,然而并没有什么卵用,因为那个临时对像马上就被抛弃了。所以,这样的无用的代码最好还是在编译时报个错,以免当时头脑发热想错了,写了一段没用但还以为有用的代码。

    4.3.3 返回值:非const的指针还是有const的指针

    谈到const函数应该返回非const的指针还是const指针这个话题时,多数人发现在C++中关于『const正确性』(const correctness)在概念上产生了分歧。 问题起源是:const函数本身不能修改对象自身的状态,却可以返回成员的非const指针。返回指针这个简单动作本身既不会影响整个对象的可见状态,当然也不会改变这个函数职责范围内涉及的状态。但是,这却使得程序员可以间接访问并修改对象的状态。

    下面的例子演示了通过返回非const指针的const函数绕开const约定(constness)的诸多方式中的一种:

    1
    2
    3
    4
    QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const {
        moveBy(10, 10); // doesn't compile!
        window()->childAt(mapTo(window(), rect().center()))->moveBy(10, 10); // compiles!
    }

    返回const指针的函数正是保护以避免这些(可能是不期望的/没有预料到的)副作用,至少是在一定程度上。但哪个函数你会觉得更想返回const指针,或是不止一个函数?

    若采用const正确(const-correct)的方法,每个返回某个成员的指针(或多个指向成员的指针)的const函数必须返回const指针。在实践中,很不幸这样的做法将导致无法使用的API:

    1
    2
    3
    4
    5
    6
    QGraphicsScene scene;
    // … populate scene
     
    foreach (const QGraphicsItem *item, scene.items()) {
        item->setPos(qrand() % 500, qrand() % 500); // doesn't compile! item is a const pointer
    }

    QGraphicsScene::items()是一个const函数,顺着思考看起来这个函数只应该返回const指针。

    在Qt中,我们几乎只有非const的使用模式。我们选择的是实用路子: 相比滥用非const指针返回类型带来的问题,返回const指针更可能招致过分使用const_cast的问题。

    4.3.4 返回值:按值返回 还是 按const引用返回?

    若返回的是对象的拷贝,那么返回const引用是更直接的方案; 然而,这样的做法限制了后面想要对这个类的重构(refactor)。 (以d-point的典型做法(idiom)为例,我们可以在任何时候改变Qt类在内存表示(memory representation);但却不能在不破坏二进制兼容性的情况下把改变函数的签名,返回值从const QFoo &变为QFoo。) 基于这个原因,除去对运行速度敏感(speed is critical)而重构不是问题的个别情形(例如,QList::at()),我们一般返回QFoo而不是const QFoo &

    【译注】:参看《Effective C++》中条款23:Don’t try to return a reference when you must return an object

    4.4.5 const vs. 对象的状态

    const正确性(Const correctness)的问题就像C圈子中vi与emacs的讨论,因为这个话题在很多地方都存在分歧(比如基于指针的函数)。

    但通用准则是const函数不能改变类的可见状态。『状态』的意思是『自身以及涉及的职责』。这并不是指非const函数能够改变自身的私有成员,也不是指const函数改变不了。而是指函数是活跃的并存在可见的副作用(visible side effects)。const函数一般没有任何可见的副作用,比如:

    1
    2
    QSize size = widget->sizeHint(); // const
    widget->move(10, 10); // not const

    代理(delegate)负责在其它对象上绘制内容。 它的状态包括它的职责,因此包括在哪个对象做绘制这样的状态。 调用它的绘画行为必然会有副作用; 它改变了它绘制所在设备的外观(及其所关联的状态)。鉴于这些,paint()作为const函数并不合理。 进一步说,任何paint()QIconpaint()的视图函数是const函数也不合理。 没有人会从内部的const函数去调用QIcon::paint(),除非他想显式地绕开const这个特性。 如果是这种情况,使用const_cast会更好。

    1
    2
    3
    4
    5
    // QAbstractItemDelegate::paint is const
    void QAbstractItemDelegate::paint(QPainter **painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
     
    // QGraphicsItem::paint is not const
    void QGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem option, QWidget *widget)

    const关键字并不能按你期望的样子起作用。应该考虑将其移除而不是去重载const/非const函数。

    5. API的语义和文档

    当传值为-1的参数给函数,函数会是什么行为?有很多类似的问题……

    是警告、致命错误还是其它?

    API需要的是质量保证。API第一个版本一定是不对的;必须对其进行测试。 以阅读使用API的代码的方式编写用例,且验证这样代码是可读的。

    还有其他的验证方法,比如

    • 让别人使用API(看了文档或是先不看文档都可以)
    • 给类写文档(包含类的概述和每个函数)

    6. 命名的艺术

    命名很可能是API设计中最重要的一个问题。类应该叫什么名字?成员函数应该叫什么名字?

    6.1 通用的命名规则

    有几个规则对于所有类型的命名都等同适用。第一个,之前已经提到过,不要使用缩写。即使是明显的缩写,比如把previous缩写成prev,从长远来看是回报是负的,因为用户必须要记住缩写词的含义。

    如果API本身没有一致性,之后事情自然就会越来越糟;例如,Qt 3 中同时存在activatePreviousWindow()fetchPrev()。恪守『不缩写』规则更容易地创建一致性的API。

    另一个时重要但更微妙的准则是在设计类时应该保持子类名称空间的干净。在Qt 3中,此项准则并没有一直遵循。以QToolButton为例对此进行说明。如果调用QToolButton的 name()caption()text()或者textLabel(),你觉得会返回什么?用Qt设计器在QToolButton上自己先试试吧:

    • name属性是继承自QObject,返回内部的对象名称,用于调试和测试。
    • caption属性继承自QWidget,返回窗口标题,对QToolButton来说毫无意义,因为它在创建的时候parent就存在了。
    • text函数继承自QButton,一般用于按钮。当useTextLabel不为true,才用这个属性。
    • textLabel属性在QToolButton内声明,当useTextLabeltrue时显示在按钮上。

    为了可读性,在Qt 4中QToolButtonname属性改成了objectNamecaption改成了windowTitle,删除了textLabel属性因为和text属性相同。

    当你找不到好的命名时,写文档也是个很好方法:要做的就是尝试为各个条目(item)(如类、方法、枚举值等等)写文档,并用写下的第一句话作为启发。如果找不到一个确切的命名,往往说明这个条目是不该有的。如果所有尝试都失败了,并且你坚信这个概念是合理的,那么就发明一个新名字。像widget、event、focus和buddy这些命名就是在这一步诞生的。

    【译注】:写文档是一个非常好的习惯。写文档的过程其实就是在帮你梳理你的编程思路。很多时候,文档写着写着你就会发现要去改代码去了。除了上述的好处多,写文档还有更多的好处。比如,在写文档的过程中,你发现文字描述过于复杂了,这表明着你的代码或逻辑是复杂的,这就倒逼你去重构你的代码。所以 —— 写文档其实就是写代码

    6.2 类的命名

    识别出类所在的分组,而不是为每个类都去找个完美的命名。例如,所有Qt 4的能感知模型(model-aware)的item view,类后缀都是ViewQListViewQTableViewQTreeView),而相应的基于item(item-based)的类后缀是WidgetQListWidgetQTableWidgetQTreeWidget)。

    6.3 枚举类型及其值的命名

    声明枚举类型时,需要记住在C++中枚举值在使用时不会带上类型(与Java、C#不同)。下面的例子演示了枚举值命名得过于通用的危害:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace Qt
    {
        enum Corner { TopLeft, BottomRight, ... };
        enum CaseSensitivity { Insensitive, Sensitive };
        ...
    };
     
    tabWidget->setCornerWidget(widget, Qt::TopLeft);
    str.indexOf("$(QTDIR)", Qt::Insensitive);

    在最后一行,Insensitive是什么意思?命名枚举类型的一个准则是在枚举值中至少重复此枚举类型名中的一个元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace Qt
    {
        enum Corner { TopLeftCorner, BottomRightCorner, ... };
        enum CaseSensitivity { CaseInsensitive, CaseSensitive };
        ...
    };
     
    tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
    str.indexOf("$(QTDIR)", Qt::CaseInsensitive);

    当对枚举值进行或运算并作为某种标志(flag)时,传统的做法是把或运算的结果保存在int型的值中,但这不是类型安全的。Qt 4提供了一个模板类QFlags,其中的T是枚举类型。为了方便使用,Qt用typedef重新定义了QFlag类型,所以可以用Qt::Alignment代替QFlags

    习惯上,枚举类型命名用单数形式(因为它一次只能『持有』一个flag),而持有多个『flag』的类型用复数形式,例如:

    1
    2
    enum RectangleEdge { LeftEdge, RightEdge, ... };
    typedef QFlags<RectangleEdge> RectangleEdges;

    在某些情形下,持有多个『flag』的类型命名用单数形式。对于这种情况,持有的枚举类型名称要求是以Flag为后缀:

    1
    2
    enum AlignmentFlag { AlignLeft, AlignTop, ... };
    typedef QFlags<AlignmentFlag> Alignment;

    6.4 函数和参数的命名

    函数命名的第一准则是可以从函数名看出来此函数是否有副作用。在Qt 3中,const函数QString::simplifyWhiteSpace()违反了此准则,因为它返回了一个QString而不是按名称暗示的那样,改变调用它的QString对象。在Qt 4中,此函数重命名为QString::simplified()

    虽然参数名不会出现在使用API的代码中,但是它们给程序员提供了重要信息。因为现代的IDE都会在写代码时显示参数名称,所以值得在头文件中给参数起一个恰当的名字并在文档中使用相同的名字。

    6.5 布尔类型的getter与setter方法的命名

    bool属性的getter和setter方法命名总是很痛苦。getter应该叫做checked()还是isChecked()scrollBarsEnabled()还是areScrollBarEnabled()

    Qt 4中,我们套用以下准则为getter命名:

    • 形容词以is为前缀,例子:
      • isChecked()
      • isDown()
      • isEmpty()
      • isMovingEnabled()
    • 然而,修饰名词的形容词没有前缀:
      • scrollBarsEnabled(),而不是areScrollBarsEnabled()
    • 动词没有前缀,也不使用第三人称(-s):
      • acceptDrops(),而不是acceptsDrops()
      • allColumnsShowFocus()
    • 名词一般没有前缀:
      • autoCompletion(),而不是isAutoCompletion()
      • boundaryChecking()
    • 有的时候,没有前缀容易产生误导,这种情况下会加上is前缀:
      • isOpenGLAvailable(),而不是openGL()
      • isDialog(),而不是dialog()
        (一个叫做dialog()的函数,一般会被认为是返回QDialog。)

    setter的名字由getter衍生,去掉了前缀后在前面加上了set;例如,setDown()setScrollBarsEnabled()

    7. 避免常见陷阱

    7.1 简化的陷阱

    一个常见的误解是:实现需要写的代码越少,API就设计得越好。应该记住:代码只会写上几次,却要被反复阅读并理解。例如:

    1
    QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");

    这段代码比下面的读起来要难得多(甚至写起来也更难):

    1
    2
    3
    4
    5
    QSlider *slider = new QSlider(Qt::Vertical);
    slider->setRange(12, 18);
    slider->setPageStep(3);
    slider->setValue(13);
    slider->setObjectName("volume");

    【译注】:在有IDE的自动提示的支持下,后者写起来非常方便,而前者还需要看相应的文档。

    7.2 布尔参数的陷阱

    布尔类型的参数总是带来无法阅读的代码。给现有的函数增加一个bool型的参数几乎永远是一种错误的行为。仍以Qt为例,repaint()有一个bool类型的可选参数用于指定背景是否被擦除。可以写出这样的代码:

    1
    widget->repaint(false);

    初学者很可能是这样理解的,『不要重新绘制!』,能有多少Qt用户真心知道下面3行是什么意思:

    1
    2
    3
    widget->repaint();
    widget->repaint(true);
    widget->repaint(false);

    更好的API设计应该是这样的:

    1
    2
    widget->repaint();
    widget->repaintWithoutErasing();

    在Qt 4中,我们通过移除了重新绘制(repaint)而不擦除widget的能力来解决了此问题。Qt 4的双缓冲使这种特性被废弃。

    还有更多的例子:

    1
    2
    3
    widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding, true);
    textEdit->insert("Where's Waldo?", true, true, false);
    QRegExp rx("moc_***.c??", false, true);

    一个明显的解决方案是bool类型改成枚举类型。我们在Qt 4的QString中就是这么做的。对比效果如下:

    1
    2
    str.replace("%USER%", user, false);               // Qt 3
    str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4

    8. 案例研究

    8.1 QProgressBar

    为了展示上文各种准则的实际应用。我们来研究一下Qt 3中QProgressBar的API,并与Qt 4中对应的API作比较。在Qt 3中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class QProgressBar : public QWidget
    {
        ...
    public:
        int totalSteps() const;
        int progress() const;
     
        const QString &progressString() const;
        bool percentageVisible() const;
        void setPercentageVisible(bool);
     
        void setCenterIndicator(bool on);
        bool centerIndicator() const;
     
        void setIndicatorFollowsStyle(bool);
        bool indicatorFollowsStyle() const;
     
    public slots:
        void reset();
        virtual void setTotalSteps(int totalSteps);
        virtual void setProgress(int progress);
        void setProgress(int progress, int totalSteps);
     
    protected:
        virtual bool setIndicator(QString &progressStr,
                                  int progress,
                                  int totalSteps);
        ...
    };

    该API相当的复杂和不一致;例如,reset()setTotalSteps()setProgress()是紧密联系的,但方法的命名并没明确地表达出来。

    改善此API的关键是抓住QProgressBar与Qt 4的QAbstractSpinBox及其子类QSpinBoxQSliderQDail之间的相似性。怎么做?把progresstotalSteps替换为minimummaximumvalue。增加一个valueChanged()消息,再增加一个setRange()函数。

    进一步可以观察到progressStringpercentageindicator其实是一回事,即是显示在进度条上的文本。通常这个文本是个百分比,但是可通过setIndicator()设置为任何内容。以下是新的API:

    1
    2
    3
    virtual QString text() const;
    void setTextVisible(bool visible);
    bool isTextVisible() const;

    默认情况下,显示文本是百分比指示器(percentage indicator),通过重写text()方法来定制行为。

    Qt 3的setCenterIndicator()setIndicatorFollowsStyle()是两个影响对齐方式的函数。他们可被一个setAlignment()函数代替:

    1
    void setAlignment(Qt::Alignment alignment);

    如果开发者未调用setAlignment(),那么对齐方式由风格决定。对于基于Motif的风格,文字内容在中间显示;对于其他风格,在右侧显示。

    下面是改善后的QProgressBar API:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class QProgressBar : public QWidget
    {
        ...
    public:
        void setMinimum(int minimum);
        int minimum() const;
        void setMaximum(int maximum);
        int maximum() const;
        void setRange(int minimum, int maximum);
        int value() const;
     
        virtual QString text() const;
        void setTextVisible(bool visible);
        bool isTextVisible() const;
        Qt::Alignment alignment() const;
        void setAlignment(Qt::Alignment alignment);
     
    public slots:
        void reset();
        void setValue(int value);
     
    signals:
        void valueChanged(int value);
        ...
    };

    8.2 QAbstractPrintDialog & QAbstractPageSizeDialog

    Qt 4.0有2个幽灵类QAbstractPrintDialogQAbstractPageSizeDialog,作为 QPrintDialogQPageSizeDialog类的父类。这2个类完全没有用,因为Qt的API没有是QAbstractPrint-或是-PageSizeDialog指针作为参数并执行操作。通过篡改qdoc(Qt文档),我们虽然把这2个类隐藏起来了,却成了无用抽象类的典型案例。

    这不是说, 的抽象是错的,QPrintDialog应该是需要有个工厂或是其它改变的机制 —— 证据就是它声明中的#ifdef QTOPIA_PRINTDIALOG

    8.3 QAbstractItemModel

    关于模型/视图(model/view)问题的细节在相应的文档中已经说明得很好了,但作为一个重要的总结这里还需要强调一下:抽象类不应该仅是所有可能子类的并集(union)。这样『合并所有』的父类几乎不可能是一个好的方案。QAbstractItemModel就犯了这个错误 —— 它实际上就是个QTreeOfTablesModel,结果导致了错综复杂(complicated)的API,而这样的API要让 所有本来设计还不错的子类 去继承。

    仅仅增加抽象是不会自动就把API变得更好的。

    8.4 QLayoutIterator & QGLayoutIterator

    在Qt 3,创建自定义的布局类需要同时继承QLayoutQGLayoutIterator(命名中的G是指Generic(通用))。QGLayoutIterator子类的实例指针会包装成QLayoutIterator,这样用户可以像和其它的迭代器(iterator)类一样的方式来使用。通过QLayoutIterator可以写出下面这样的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    QLayoutIterator it = layout()->iterator();
    QLayoutItem **child;
    while ((child = it.current()) != 0) {
        if (child->widget() == myWidget) {
            it.takeCurrent();
            return;
        }
        ++it;
    }

    在Qt 4,我们干掉了QGLayoutIterator类(以及用于盒子布局和格子布局的内部子类),转而是让QLayout的子类重写itemAt()takeAt()count()

    8.5 QImageSink

    Qt 3有一整套类用来把完成增量加载的图片传递给一个动画 —— QImageSource/Sink/QASyncIO/QASyncImageIO。由于这些类之前只是用于启用动画的QLabel,完全过度设计了(overkill)。

    从中得到的教训就是:对于那些未来可能的还不明朗的需求,不要过早地增加抽象设计。当需求真的出现时,比起一个复杂的系统,在简单的系统新增需求要容易得多。

    from: https://coolshell.cn/articles/18024.html

    展开全文
  • 网络基础10 Restful API设计规范

    万次阅读 2017-11-03 19:58:30
    Restful API设计规范总结

    概念

    Restful API用来规范应用如何在HTTP层与API提供方进行数据交互 。

    Restful API描述了HTTP层里客户端和服务器端的数据交互规则:客户端通过向服务器端发送HTTP(S)请求,接收服务器的响应,完成一次HTTP交互。这个交互过程中,REST架构约定两个重要方面就是HTTP请求的所采用方法,以及请求的链接。

    在请求层面,Restful API规范可以简单粗暴抽象成以下两个规则:

    1. 请求API的URL表示用来定位资源
    2. 请求的METHOD表示对这个资源进行的操作

    API的URL

    版本号

    在Restful API中,API应当尽量兼容之前的版本。Web端很容易为了适配服务端的新的API接口进行版本升级,而Android、IOS等客户端必须通过用户主动升级产品到新版本,才能适配新接。

    为了解决这个问题,在设计Restful API时一般情况下会在URL中保留版本号,并同时兼容多个版本:

    【GET】  /v1/users/{user_id}  // 版本 v1 的查询用户列表的 API 接口
    【GET】  /v2/users/{user_id}  // 版本 v2 的查询用户列表的 API 接口
    

    现在可以在不改变V1版本的接口情况下,新增V2版本的接口满足新的业务需求。服务端会同时兼容多个版本,但是同时维护版本过多也会成为不小的负担。

    常见的做法是,不维护全部的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,大部分的用户升级到新的版本后,废弃一些使用量较少的服务端老版本的API接口,并要求使用产品老旧版本的用户墙纸升级。

    不合理的URL

    URL用来定位资源,跟要进行的操作区分开,这就意味这URL不该有任何动词

    下面示例中的getcreatesearch等动词,都不应该出现在REST架构的后端接口路径中。在以前,这些接口中的动名词通常对应后台的某个函数。比如:

    /api/getUser
    /api/createApp
    /api/searchResult
    /api/deleteAllUsers
    

    当我们需要对单个用户进行操作时,根据操作的方式不同可能需要下面的这些接口:

    /api/getUser 
    //  用来获取某个用户的信息,还需要以参数方式传入用户 id 信息)
    
    /api/updateUser 
    // 用来更新用户信息
    
    /api/deleteUser
    // 用来删除单个用户
    
    /api/resetUser 
    // 重置用户的信息
    

    这样的弊端在于:

    1. URL更长了
    2. 对一个资源实体进行不同的操作就是一个不同URL,造成URL过多难以管理。

    其实当你回过头看「URL」这个术语的定义时,更能理解这一点。URL的意思是统一资源定位符,这个术语已经清晰的表明,一个URL应该用来定位资源,而不应该掺入对操作行为的描述。

    Restful的URL

    在REST架构的URL应该是这个样子:

    1. URL中不应该出现任何表示操作的动词,链接只用于对应资源
    2. URL中应该单复数区分,推荐的实践是永远只用复数;比如GET /api/users表示获取用户的列表;如果获取单个资源,传入ID,比如/api/users/123表示获取单个用户的信息;
    3. 按照资源的逻辑层级,对URL进行嵌套,比如一个用户属于某个团队,而这个团队也是众多团队之一;那么获取这个用户的接口可能是这样:
    GET /api/teams/123/members/234
    // 表示获取 id 为 123 的小组下,id 为234 的成员信息
    

    按照类似的规则,可以写出如下的接口:

    /api/teams 
    // 对应团队列表
    
    /api/teams/123 
    // 对应 ID 为 123 的团队
    
    /api/teams/123/members 
    // 对应 ID 为 123 的团队下的成员列表
    
    /api/teams/123/members/456 
    // 对应 ID 为 123 的团队下 ID 为 456 的成员
    

    特殊情况

    有的时候一个资源变化难以使用标准的Restful API来命名,可以考虑使用一些特殊的Actions命名。比如,“密码修改”这个接口的命名很难完全使用名词来构建路径,此时可以引入Action:

    PUT  /v1/users/{user_id}/password/actions/modify
    // 密码修改
    

    大小写

    根据RFC3986定义,URL是大小写明暗的,所以为了避免歧义,尽量使用小写字母。

    API的请求方法

    在很多系统中,几乎只用GET和POST方法来完成了所有的接口操作。这个行为类似于全用<div>来布局。实际上,我们不只有GET和POST可用,在REST架构中,有以下几个重要的请求方法:GET,POST,PUT,PATCH,DELETE。这几个方法都可以与对数据的 CRUD 操作对应起来。

    CRUD 是指在做计算处理时的增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写。即增删改查

    简单来说,GET用于查询资源,POST用于创建资源,PUT用于更新服务端的资源的全部信息,PATCH 用于更新服务端的资源的部分信息,DELETE 用于删除服务端的资源。

    GET          /users                # 查询用户信息列表
    GET          /users/1001           # 查看某个用户信息
    POST         /users                # 新建用户信息
    PUT          /users/1001           # 更新用户信息(全部字段)
    PATCH        /users/1001           # 更新用户信息(部分字段)
    DELETE       /users/1001           # 删除用户信息
    

    GET

    资源的读取,用GET请求,比如:

    GET /api/users
    // 表示读取用户列表
    

    GET应当实现为一个安全幂等的方法。用于获取数据而不应该产生副作用。

    POST

    资源的创建,用POST方法;

    POST 是一个非幂等的方法,多次调用会造成不同效果;

    幂等(Idempotent):如果对服务器资源的多次请求与一次请求造成的副作用是一样的的话,那这个请求方法可以被认为是幂等。

    比如下面的请求会在服务器上创建一个name属性为John的用户,多次请求就会创建多个这样的用户。

    POST /api/users
    
    {
      "name": "John"
    }
    

    PUT和PATCH

    用于更新的HTTP方法有两个,PUT和PATCH。

    他们都应当被实现为幂等方法,即多次同样的更新请求应当对服务器产生同样的副作用。

    PUT和PATCH有各自不同的使用场景:

    • PUT用于更新资源的全部信息,在请求的body中需要传入修改后的全部资源主体;
    • PATCH用于局部更新,在body中只需要传入需要改动的资源字段。

    设想服务器中有以下用户资源/api/users/123

    {
     "id": 123,
     "name": "Original",
     "age": 20
    }
    

    当我们往后台发送更新请求时,PATCH 和 PUT 造成的效果是不一样。

    PUT /api/users/123
    
    {
     "name": "PUT Update"
    }
    

    上述 PUT 请求操作后的内容是:

    {
     "id": 123,
     "name": "PUT Update"
    }
    

    可以观察到,资源原有的 age 字段被清除掉了。

    而如果改用 PATCH 的话,

    PATCH /api/users/123
    
    {
     "name": "PATCH Update"
    }
    

    更新后的内容是:

    {
     "id": 123,
     "name": "PATCH Update",
     "age": 20
    }
    

    请求中指定的name属性被更新了,而原有的age属性则保持不变。

    PATCH的作用在于如果一个资源有很多字段,在进行局部更新时,只需要传入需要修改的字段即可。否则在用PUT的情况下,你不得不将整个资源模型全都发送回服务器,造成网络资源的极大浪费。

    DELETE

    资源的删除,相应的请求HTTP方法就是DELETE。这个也应当被实现为一个幂等的方法。如:

    DELETE /api/users/123
    

    用于删除服务器上ID123的资源,多次请求产生副作用都是,是服务器上ID123的资源不存在。

    HEAD和OPTIONS

    HEAD和OPTIONS不太常用

    • HEAD:获取资源的头部信息,比如只想了解某个文件的大小、某个资源的修改日期等
    • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。针对非简单请求的CORS请求,会在正式通信之前增加一次HTTP查询请求,称为“预检”请求,对应的请求方法就是OPTION

    不符合CRUD的情况

    在实际资源操作中,总会有一些不符合CRUD的情况,一般会添加控制参数或者把动作转换成资源,Github采用的后者,比如『喜欢』一个gist,就增加一个/gists/:id/star子资源,然后对齐进行操作,『喜欢』使用PUT /gists/:id/star,『取消喜欢』使用DELETE /gists/:id/star

    查询参数

    REST风格的接口地址,表示的可能是单个资源,也可能是资源的集合;当我们需要访问资源集合时,设计良好的接口应当接受参数,允许只返回满足某些特定条件的资源列表。

    公共参数

    常规的公共查询参数有:

    参数名 作用
    offset 返回记录的开始位置
    limit 返回记录的数量
    keyword 提供关键词进行搜索
    sort 指定排序的字段
    orderby 指定排序方式

    具体来看:

    (1)以offsetlimit参数来进行分页:

    GET /api/users?offset=0&limit=20
    

    (2)使用keyword提供关键词进行搜索:

    GET /api/users?keyword=john
    

    (3)使用sort参数和orderby参数进行排序

    GET /api/users?sort=age&orderby=asc    // 按年龄升序
    GET /api/users?sort=age&orderby=desc   // 按年龄降序
    

    有的时候也可以只用orderby来进行排序:

    GET /api/users?se&orderby=age_asc      // 按年龄升序
    GET /api/users?se&orderby=age_desc     // 按年龄降序
    

    个性参数

    上面介绍的offsetlimitorderby是一些公共参数。此外,业务场景中还存在许多个性化的参数:

    【GET】  /v1/categorys/{category_id}/enable=[1|0]&os_type={field}&device_ids={field,field,…}
    

    注意不要过度设计,只返回用户需要的查询参数,此外,需要考虑是否对查询参数创建数据库索引以提高查询性能。

    语义化

    设计合适的API URL,以及选择合适的请求方法,可以语义化的描述一个HTTP请求的操作。

    当我们都熟悉且遵循这样的规范后,基本可以看到一个REST风格的接口就知道如何使用这个接口进行CRUD操作了。

    比如下面这面这个接口就表示搜索ID123的图书馆的书,并且书的信息里包含关键字game,返回前十条满足条件的结果。

    GET /api/libraries/123/books?keyword=game&sort=price&limit=10&offset=0
    

    同样,下面这个请求的意思也就很明显了吧。

    PATCH /api/companies/123/employees/234
    
    {
        "salary": 2300
    }
    

    状态码

    服务端会在响应头的status code中向用户返回的状态码,它说明了请求的大致情况,是否正常完成、需要进一步处理、出现的错误。大改分为几个区间:

    • 2xx,请求正常处理并返回
    • 3xx,重定向,请求的资源位置发生变化
    • 4xx,客户端发送请求错误
    • 5xx,服务端错误

    常见的有以下一些:

    状态码 状态信息 说明
    200 OK 请求成功
    201 Created 创建成功
    204 No Content 删除数据成功
    301 Moved Permanently 请求的资源已经永久性地移动到另外一个地方,
    后续所有的请求都应该直接访问新地址。
    服务端会把新地址写在Location头部字段,方便客户端使用
    304 Not Modified 请求的资源和之前的版本一样,没有发生改变。用来缓存资源
    400 Bad Request 请求语法错误,body数据格式有误,body缺少必须的字段等,
    导致服务端无法处理
    401 Unauthorized 未授权
    403 Forbidden 有授权(与401相对),但是被拒绝
    404 Not Found 客户端要访问的资源不存在
    405 Method Not Allowed 服务端接收到了请求,资源存在,但是不支持对应的方法。
    服务端必须返回Allow头部,告诉客户端哪些方法是允许的
    406 Not Acceptable 用户请求的格式不可得
    (比如用户请求JSON格式,但是只有XML格式)
    500 Internal Server Error 服务器发生错误
    503 Service Unavailable 服务器因为负载过高或者维护,暂时无法提供服务。

    错误处理

    当RESTful API接口出现非2xx的HTTP错误码响应时,采用全局的异常结构响应信息。

    一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

    {
      error: "Invalid API key"
    }
    

    也可以采取下面的结构:

    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    
    {
        "code": "INVALID_ARGUMENT",
        "message": "{error message}",
        "cause": "{cause message}",
        "request_id": "01234567-89ab-cdef-0123-456789abcdef",
        "host_id": "{server identity}",
        "server_time": "2014-01-01T12:00:00Z"
    }
    

    返回结果

    返回内容

    最好采用JSON作为返回内容的格式。如果用户需要其他格式,比如XML,应该在请求头的Accept字段中指定。

    对于不支持的格式,服务端需要返回正确的状态码并给出详细说明。

    返回规范

    针对不同操作,服务器向用户返回的结果应该符合以下规范。

    【GET】     /{version}/{resources}                    // 返回资源对象的列表(数组)
    【GET】     /{version}/{resources}/{resource_id}      // 返回单个资源对象
    【POST】    /{version}/{resources}                    // 返回新生成的资源对象
    【PUT】     /{version}/{resources}/{resource_id}      // 返回完整的资源对象
    【PATCH】   /{version}/{resources}/{resource_id}      // 返回完整的资源对象
    【DELETE】  /{version}/{resources}/{resource_id}      // 状态码 200,返回完整的资源对象。
    【DELETE】  /{version}/{resources}/{resource_id}      // 状态码 204,返回一个空文档
    

    Hypermedia API

    RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

    比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

    {
      "link": {
        "rel": "collection https://www.example.com/zoos",
        "href": "https://api.example.com/zoos",
        "title": "List of zoos",
        "type": "application/vnd.yourformat+json"
      }
    }
    

    上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

    Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

    {
      "current_user_url": "https://api.github.com/user",
      "authorizations_url": "https://api.github.com/authorizations",
      // ...
    }
    

    从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user, 然后就得到了下面结果。

    {
      "message": "Requires authentication",
      "documentation_url": "https://developer.github.com/v3"
    }
    

    其他

    1. API的身份认证应该使用OAuth 2.0框架。
    2. 服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

    关于REST的更多详细规范,可以参考这个仓库

    参考

    展开全文
  • C++ API设计笔记

    千次阅读 2020-03-29 15:00:06
    《C++ API设计》原英文版由Martin Reddy著,中文版出版于2013年,这里是中文版的笔记。 1. API简介 1.1 什么是API:API(Application Programming Interface)提供了对某个问题的抽象,以及客户与解决该问题的软件...

    《C++ API设计》原英文版由Martin Reddy著,中文版出版于2013年,这里是中文版的笔记。

    1. API简介

    1.1 什么是API:API(Application Programming Interface)提供了对某个问题的抽象,以及客户与解决该问题的软件组件之间进行交互的方式。组件本身通常以软件类库形式分发,它们可以在多个应用程序中使用。概况地说,API定义了一些可复用的模块,使得各个模块化功能块可以嵌入到最终用户的应用程序中去。API是一个明确定义的接口,可以为其它软件提供特定服务。

    在C++中,API一般包括一个或多个头文件(.h)以及辅助文档。某个特定API的具体实现通常是可以被链接到最终用户程序中的库文件,它也可以是静态库,又或者是动态库。

    C++ API通常会包含如下的元素:头文件;类库;文档

    API是软件组件的逻辑接口,隐藏了实现这个接口所需的内部细节。

    1.2 API设计上有什么不同:接口是开发者所编写的最重要的代码。因为比起相关的实现代码出现问题,修复接口出现的问题代价要大得多。

    API开发中的一些关键因素:

    (1).API是为开发者设计的接口。

    (2).多个应用程序可以共享同一个API。

    (3).修改API时,必须尽可能保证向后兼容。

    (4).出于向后兼容的需求,一定要具有变更控制流程。

    (5).API的生存周期一般都比较长。

    (6).在编写API时,良好的文档必不可少,特别是当不提供实现的源代码时。

    (7).自动化测试同样也很重要。

    API描述了其他工程师构建他们的应用软件所使用的软件。因此,API必须拥有良好的设计、文档、回归测试,并且保证发布之间的稳定性。

    1.3 为什么要使用API:

    (1).更健壮的代码:隐藏实现、延长寿命、促进模块化、减少代码重复、消除硬编码假设、易于改变实现、易于优化。

    (2).代码复用:就是使用已有的软件去构建新的软件。API提供了一种代码复用的机制。

    (3).并行开发。

    1.4 何时应当避免使用API:

    (1).许可证限制。

    (2).功能不匹配。

    (3).缺少源代码:不能访问API的源代码就丧失了通过修改源代码修复错误的能力。

    (4).缺乏文档。

    1.5 API示例:在现代软件开发中API无处不在,从OS、语言层面的API,到图像、声音、图形、并发、网络、XML、数学、Web浏览器以及GUI API。

    术语SDK(软件开发工具包)和术语API是密切相关的。本质上讲,SDK是安装在计算机上的特定平台的包,其目的是使用一个或多个API构建应用。SDK至少要包含编译程序所需的头文件(.h)以及提供API实现的库文件(.dylib、.so、.dll),用以链接到应用程序之中。然而,SDK还可能包含其它帮助使用API的资源,如文档、示例代码以及支持工具。

    1.6 文件格式和网络协议:

    在计算机应用中存在几个其它形式的常用通信”协议”,其中最常见的一个就是文件格式。它是使用众所周知的数据组织层次将内存中的数据存储到磁盘文件上的方法。比如,JPEG文件交换格式(JFIF)是用来交换JPEG编码图像的图像文件格式,通常使用.jpg或.jpeg文件扩展名。

    客户端/服务端应用、点对点应用以及中间件服务,使用建立好的且通常基于网络套接字的协议发送和接收数据。

    每当你创建一个文件格式或者客户端/服务器协议时,同时也要为其创建API。这样,规范的细节以及未来的任何变更都将是集中且隐藏的。

    2. 特征

    2.1 问题域建模:编写API的目的是解决特定的问题或完成具体的任务。因此,API应该首先为问题提供一个清晰的解决方案,同时能对实际的问题域进行准确的建模。

    API应该对它所解决的问题提供逻辑抽象。也就是说,在设计API时,应该阐述在选定问题域内有意义的深层概念,而不是公开低层实现细节。当你把API文档提供给一位非程序员时,他应当能够理解接口中的概念并且知道它的工作机制。

    API同样需要对问题域的关键对象建模。该过程旨在描述特定问题域中对象的层次结构,因此经常被称作”面向对象设计”或者”对象建模”。对象建模的目的是确定主要对象的集合,这些对象提供的操作以及对象之间的关系。

    2.2 隐藏实现细节:创建API的主要原因是隐藏所有的实现细节,以免将来修改API对已有客户造成影响。任何内部实现细节(那些很可能变更的部分)必须对该API的客户隐藏。主要有两种技巧可以达到此目标:物理隐藏和逻辑隐藏。物理隐藏表示只是不让用户获得私有源代码。逻辑隐藏则需要使用语言特性限制用户访问API的某些元素。

    物理隐藏:在C和C++中,声明和定义是有特定含义的精确术语。声明只是告诉编译器一个名字以及它的类型,并不为其分配任何内存。与之相对,定义提供了类型结构体的细节,如果是变量则为其分配内存。声明告诉编译器某个标识符的名称及类型。定义提供该标识符的完整细节,即它是一个函数体还是一块内存区域。物理隐藏表示将内部细节(.cpp)与公有接口(.h)分离,存储在不同的文件中

    逻辑隐藏:封装提供了限制访问对象成员的机制:public、protected、private。封装是将API的公有接口与其底层实现分离的过程。逻辑隐藏指的是使用C++语言中受保护的和私有的访问控制特性从而限制访问内部细节。类的数据成员应该始终声明为私有的,而不是公有的或受保护的。

    永远不要返回私有数据成员的非const指针或引用,这会破坏封装性。

    强烈建议在API中采用Pimpl惯用法,这样就可以将所有实现细节完全和公有头文件分开。Pimpl惯用法:它将所有的私有数据成员隔离到一个.cpp文件中独立实现的类或结构体中。之后,.h文件仅需要包含指向该类实例的不透明指针(opaque pointer)即可

    将私有功能声明为.cpp文件中的静态函数,而不要将其作为私有方法暴露在公开的头文件中。(更好的做法是使用Pimpl惯用法。)

    隐藏实现类:除了隐藏类的内部方法和变量之外,还应该尽力隐藏那些纯粹是实现细节的类。实际上,一些类仅用于实现,因此应该将其从API的公有接口中移除。

    2.3最小完备性:

    (1).优秀的API设计应该是最小完备的。即它应该尽量简洁,但不要过分简洁。

    (2).谨记奥卡姆(Occam)剃刀原理:若无必要,勿增实体。

    (3).疑惑之时,果断弃之,精简API中公有的类和函数。

    谨慎添加虚函数:虚函数的调用必须在运行时查询虚函数表决定,而非虚函数的调用在编译时就能确定。使用虚函数一般需要维护指向虚函数表的指针,进而增加了对象的大小。不是所有的虚函数都能内联,因而将虚函数声明为内联是没有任何意义的。因为虚函数是运行时确定的,而内联是在编译时进行优化。谨记C++中的inline关键字仅仅是给编译器的一个提示。如果类包含任一虚函数,那么必须将析构函数声明为虚函数,这样子类就可以释放其可能申请的额外资源。绝不在构造函数或析构函数中调用虚函数,这些调用不会指向子类

    避免将函数声明为可以重写的函数(虚的),除非你有合理且迫切的需求。

    2.4易用性:优秀的API设计应该使简单的任务更简单,使人一目了然。例如,好的API可以让客户仅通过方法签名就能知晓使用方法,而不需要另写文档:

    (1).使用枚举类型代替布尔类型,提高代码的可读性。

    (2).避免编写拥有多个相同类型参数的函数。

    (3).使用一致的函数命名和参数顺序。

    (4).正交的API意味着函数没有副作用。设计正交API时需要铭记两个重要因素:减少冗余:确保只有一种方式表示相同的信息;增加独立性:确保暴露的概念没有重叠。

    (5).当需要客户销毁指针时,使用智能指针返回动态申请的对象。

    (6).将资源的申请与释放当作对象的构造和析构。

    (7).不要将平台相关的#if或#ifdef语句放在公共的API中,因为这些语句暴露了实现细节,并使API因平台而异。

    2.5 松耦合:耦合:软件组件之间相互连接的强度的度量,即系统中每个组件对其它组件的依赖程度。内聚:单个软件组件内的各种方法相互关联或聚合强度的度量。

    (1).优秀的API表现为松耦合和高内聚。

    (2).除非确实需要#include类的完整定义,否则应该为类使用前置声明。如果类A仅需要知道类B的名字,即它不需要知道类B的大小或调用类B的任何方法,那么类A就不需要依赖类B的完整声明。在这种情况下,可以为类B使用前置声明,而非包含整个接口,这样就降低了这两个类之间的耦合。

    (3).与成员函数相比,使用非成员、非有元的方法能降低耦合度。如果情况允许,那么优先声明非成员、非友元的函数,而非成员函数,这么做在促进封装的同时还降低了这些函数和类的耦合度。

    (4).有时,使用数据冗余降低类之间的耦合是合理的。尽管如此,即使是有意的冗余,也是冗余,应该小心谨慎地使用,同时添加良好的注释。

    (5).管理器类可以通过封装几个低层次类降低耦合。管理器类拥有并协调几个低层次的类。可以用它打破基于一组低层次的类的一个或多个类的依赖关系。

    (6).回调:在C和C++中,回调是模块A中的一个函数指针,该指针被传递给模块B,这样B就能在合适的时候调用A中的函数。模块B对模块A一无所知,并且对模块A不存在”包含”(include)或者”链接”(link)依赖。回调的这种特性使得低层代码能够执行与其不能有依赖关系的高层代码。因此,在大型项目中,回调是一种用于打破循环依赖的常用技术。

    3. 模式

    设计模式是针对软件设计问题的通用解决方案。该术语源于《设计模式:可复用面向对象软件的基础》一书。书中介绍了下列通用的设计模式,它们可以分成三大类

    (1).创建型模式:

    抽象工厂模式:创建一组相关的工厂。

    建造者模式:将复杂对象的构建与表示分离。

    工厂方法模式:将类的实例化推迟到子类中。

    原型模式:指定类的原型实例,克隆该实例可以生成新的对象。

    单例模式:确保类只有一个实例。

    (2).结构型模式:

    适配器模式:将类的接口转换为另一种接口。

    桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。

    组合模式:将对象组合成树型结构,表示”部分----整体”的层次结构。

    装饰模式:动态地给一个对象添加一些额外的行为。

    外观模式:为子系统中的一组接口提供统一的高层次接口。

    享元模式:利用共享技术高效地支持大量细粒度的对象。

    代理模式:提供另一个对象的替代物或占位符,以便控制对该对象的访问。

    (3).行为模式:

    职责链模式:使多个接收者对象有机会处理来自发送者对象的请求。

    命令模式:将请求或操作封装成对象,并支持可撤销的操作。

    解释器模式:指定如何对某种语言的语句进行表示和判断。

    迭代器模式:提供一种顺序访问某种聚合对象元素的途径。

    中介者模式:定义一个中介对象,用于封装一组对象的交互。

    备忘录模式:捕获对象的内部状态,以便将来可将该对象恢复到保存时的状态。

    观察者模式:定义对象间一种一对多的依赖关系,当对象的状态发生改变时,所有依赖于它的对象都将得到通知。

    状态模式:当对象内部状态改变时,对象看起来好像修改了它所属的类。

    策略模式:定义一组算法并封装每个算法,使它们在运行时可以相互替换。

    模板方法模式:定义某操作中算法的框架,将其中一些步骤推迟到子类中。

    访问者模式:表述对某对象结构的元素所执行的操作。

    设计模式更多介绍参考:https://blog.csdn.net/fengbingchun/category_2134223.html

    测试代码cplusplus_api_design.hpp内容如下:

    // Pimpl惯用法:"自动定时器",当被销毁时打印出其生存时间
    class AutoTimer {
    public:
    	explicit AutoTimer(const std::string& name);
    	~AutoTimer();
    
    	AutoTimer(const AutoTimer&) = delete;
    	AutoTimer& operator=(const AutoTimer&) = delete;
    
    private:
    	class Impl; // 私有内嵌类
    	std::unique_ptr<Impl> impl_;
    };
    
    // 单例模式
    class Singleton {
    public:
    	static Singleton& GetInstance() // 既可以返回单例类的指针也可以返回引用,当返回指针时,客户可以删除该对象,因此最好返回引用
    	{
    		static Singleton instance;
    		return instance;
    	}
    
    	Singleton(const Singleton&) = delete;
    	Singleton& operator=(const Singleton&) = delete;
    
    private:
    	Singleton() { fprintf(stdout, "constructor\n"); }
    	~Singleton() { fprintf(stdout, "destructor\n"); }
    };
    
    // 单一状态设计模式
    class Monostate {
    public:
    	int GetTheAnswer() const { return answer_; }
    
    private:
    	static int answer_; // 也可以将该静态变量声明为.cpp文件作用域的静态变量,而不是私有的类静态变量
    };
    
    // 工厂模式
    class IRenderer {
    public:
    	virtual ~IRenderer() {}
    	virtual void Render() = 0;
    };
    
    class RendererFactory {
    public:
    	IRenderer* CreateRenderer(const std::string& type);
    };
    
    // 扩展工厂模式:将派生类与工厂方法解耦并支持在运行时添加新的派生类:工厂类维护一个映射,此映射将类型名与创建对象的回调关联起来。
    // 然后就可以允许新的派生类通过一对新的方法调用来实现注册和注销。
    // 需要注意的问题是,工厂对象必须保持其状态信息。因此,最好强制要求任一时刻都只能创建一个工厂对象,这也是为何多数工厂对象也是单例的原因
    class RendererFactory2 {
    public:
    	typedef IRenderer* (*CreateCallback)();
    	static void RegisterRenderer(const std::string& type, CreateCallback cb);
    	static void UnRegisterRenderer(const std::string& type);
    	static IRenderer* CreateRenderer(const std::string& type);
    
    private:
    	typedef std::map<std::string, CreateCallback> callback_map_;
    	static callback_map_ renderers_;
    };
    
    // 代理模式:尤其适用于Original类是第三方类库
    class IOriginal {
    public:
    	virtual void DoSomething(int value) = 0;
    };
    
    class Original : public IOriginal {
    public:
    	void DoSomething(int value) override { fprintf(stdout, "Original::DoSomething\n"); }
    };
    
    class Proxy : public IOriginal {
    public:
    	Proxy() : orig_(new Original()) {}
    	~Proxy() { delete orig_; }
    
    	void DoSomething(int value) override { return orig_->DoSomething(value); }
    
    	Proxy(const Proxy&) = delete;
    	Proxy& operator=(const Proxy&) = delete;
    
    private:
    	Original* orig_;
    };
    
    // 适配器模式
    class Rectangle {
    public:
    	Rectangle() = default;
    	~Rectangle() {}
    
    	void setDimensions(float cx, float cy, float w, float h) { fprintf(stdout, "width: %f, height: %f\n", w, h); }
    };
    
    class RectangleAdapter {
    public:
    	RectangleAdapter() : rect_(new Rectangle()) {}
    	~RectangleAdapter() { delete rect_; }
    
    	void Set(float x1, float y1, float x2, float y2)
    	{
    		float w = x2 - x1;
    		float h = y2 - y1;
    		float cx = w / 2.f + x1;
    		float cy = h / 2.f + y1;
    		rect_->setDimensions(cx, cy, w, h);
    	}
    
    	RectangleAdapter(const RectangleAdapter&) = delete;
    	RectangleAdapter& operator=(const RectangleAdapter&) = delete;
    
    private:
    	Rectangle* rect_;
    };
    
    // 外观模式
    class Subsystem1 {
    public:
    	Subsystem1() = default;
    	~Subsystem1() {}
    	void Operation() { fprintf(stdout, "subsystem1 operation\n"); }
    };
    
    class Subsystem2 {
    public:
    	Subsystem2() = default;
    	~Subsystem2() {}
    	void Operation() { fprintf(stdout, "subsystem2 operation\n"); }
    };
    
    class Facade {
    public:
    	Facade() : subs1_(new Subsystem1()), subs2_(new Subsystem2()) {}
    	~Facade()
    	{
    		delete subs1_;
    		delete subs2_;
    	}
    
    	void OperationWrapper()
    	{
    		subs1_->Operation();
    		subs2_->Operation();
    	}
    
    	Facade(const Facade&) = delete;
    	Facade& operator=(const Facade&) = delete;
    
    private:
    	Subsystem1* subs1_;
    	Subsystem2* subs2_;
    };
    
    // 观察者模式
    class SubjectBase; // 抽象主题
    class ObserverBase { // 抽象观察者
    public:
    	ObserverBase(std::string name, SubjectBase* sub) : name_(name), sub_(sub) {}
    	virtual void Update() = 0;
    
    protected:
    	std::string name_;
    	SubjectBase* sub_;
    };
    
    class StockObserver: public ObserverBase { // 具体观察者,看股票的
    public:
    	StockObserver(std::string name, SubjectBase* sub) : ObserverBase(name, sub) {}
    	void Update() override;
    };
    
    class NBAObserver : public ObserverBase { // 具体观察者,看NBA的
    public:
    	NBAObserver(std::string name, SubjectBase* sub) : ObserverBase(name, sub) {}
    	void Update() override;
    };
    
    class SubjectBase { // 抽象主题
    public:
    	virtual void Attach(ObserverBase* observer) = 0;
    	virtual void Notify() = 0;
    
    public:
    	std::string action_;
    	std::vector<ObserverBase*> observers_;
    };
    
    
    class SecretarySubject : public SubjectBase { // 具体主题
    public:
    	void Attach(ObserverBase* ob) { observers_.push_back(ob); }
    
    	void Notify()
    	{
    		for (auto it = observers_.cbegin(); it != observers_.cend(); ++it) {
    			(*it)->Update();
    		}
    	}
    };
    
    int test_api_design_3();
    int test_api_design_3_pimpl();
    int test_api_design_3_singleton();
    int test_api_design_3_monostate();
    int test_api_design_3_factory();
    int test_api_design_3_factory_expand();
    int test_api_design_3_proxy();
    int test_api_design_3_adapter();
    int test_api_design_3_facade();
    int test_api_design_3_observer();

    测试代码cplusplus_api_design.cpp内容如下:

    class AutoTimer::Impl {
    public:
    	double GetElapsed() const
    	{
    #ifdef _WIN32
    		return (GetTickCount() - start_time_) / 1e3;
    #else
    		struct timeval end_time;
    		gettimeofday(&end_time, nullptr);
    		double t1 = start_time_.tv_usec / 1e6 + start_time_.tv_sec;
    		double t2 = end_time.tv_usec / 1e6 + end_time.tv_sec;
    		return t2 - t1;
    #endif
    	}
    
    	std::string name_;
    #ifdef _WIN32
    	DWORD start_time_;
    #else
    	struct timeval start_time_;
    #endif
    };
    
    AutoTimer::AutoTimer(const std::string& name) : impl_(std::make_unique<Impl>())
    {
    	impl_->name_ = name;
    #ifdef _WIN32
    	impl_->start_time_ = GetTickCount();
    #else
    	gettimeofday(&impl_->start_time_, nullptr);
    #endif
    }
    
    AutoTimer::~AutoTimer()
    {
    	fprintf(stdout, "%s : took %f secs\n", impl_->name_.c_str(), impl_->GetElapsed());
    }
    
    int test_api_design_3_pimpl()
    {
    	AutoTimer auto_timer("Take");
    
    	return 0;
    }
    
    int test_api_design_3_singleton()
    {
    	Singleton& singleton1 = Singleton::GetInstance();
    	Singleton& singleton2 = Singleton::GetInstance();
    
    	return 0;
    }
    
    int Monostate::answer_ = 42;
    int test_api_design_3_monostate()
    {
    	Monostate m1, m2;
    	fprintf(stdout, "answer1: %d, answer2: %d\n", m1.GetTheAnswer(), m2.GetTheAnswer());
    
    	return 0;
    }
    
    class OpenGLRenderer : public IRenderer {
    public:
    	OpenGLRenderer() { fprintf(stdout, "constructor OpenGLRenderer\n"); }
    	void Render() override { fprintf(stdout, "OpenGLRenderer::Render\n"); }
    	~OpenGLRenderer() { fprintf(stdout, "destructor OpenGLRenderer\n"); }
    };
    
    class DirectXRenderer : public IRenderer {
    public:
    	DirectXRenderer() { fprintf(stdout, "constructor DirectXRenderer\n"); }
    	void Render() override { fprintf(stdout, "DirectXRenderer::Render\n"); }
    	~DirectXRenderer() { fprintf(stdout, "destructor DirectXRenderer\n"); }
    };
    
    IRenderer* RendererFactory::CreateRenderer(const std::string& type)
    {
    	if (type == "opengl")
    		return new OpenGLRenderer();
    	if (type == "directx")
    		return new DirectXRenderer();
    
    	return nullptr;
    }
    
    int test_api_design_3_factory()
    {
    	RendererFactory factory;
    
    	IRenderer* renderer1 = factory.CreateRenderer("opengl");
    	IRenderer* renderer2 = factory.CreateRenderer("directx");
    
    	if (renderer1) {
    		renderer1->Render();
    		delete renderer1;
    	}
    
    	if (renderer2) {
    		renderer2->Render();
    		delete renderer2;
    	}
    
    	return 0;
    }
    
    RendererFactory2::callback_map_ RendererFactory2::renderers_;
    
    void RendererFactory2::RegisterRenderer(const std::string& type, CreateCallback cb)
    {
    	renderers_[type] = cb;
    }
    
    void RendererFactory2::UnRegisterRenderer(const std::string& type)
    {
    	renderers_.erase(type);
    }
    
    IRenderer* RendererFactory2::CreateRenderer(const std::string& type)
    {
    	callback_map_::iterator it = renderers_.find(type);
    	if (it != renderers_.end()) {
    		// 调用回调以构造此派生类的对象
    		return (it->second)();
    	}
    
    	return nullptr;
    }
    
    // API用户现在可以在系统中注册(以及注销)新的渲染器
    class UserRenderer : public IRenderer {
    public:
    	UserRenderer() { fprintf(stdout, "constructor UserRenderer\n"); }
    	void Render() override { fprintf(stdout, "UserRenderer::Render\n"); }
    	~UserRenderer() { fprintf(stdout, "destructor UserRenderer\n"); }
    	static IRenderer* Create() { return new UserRenderer(); }
    };
    
    int test_api_design_3_factory_expand()
    {
    	// 注册一个新的渲染器
    	RendererFactory2::RegisterRenderer("user", UserRenderer::Create);
    	// 为新的渲染器创建一个实例
    	IRenderer* r = RendererFactory2::CreateRenderer("user");
    	if (r) {
    		r->Render();
    		delete r;
    	}
    
    	return 0;
    }
    
    int test_api_design_3_proxy()
    {
    	Proxy proxy;
    	proxy.DoSomething(3);
    
    	return 0;
    }
    
    int test_api_design_3_adapter()
    {
    	RectangleAdapter rect;
    	rect.Set(10.f, 5.f, 20.f, 25.f);
    
    	return 0;
    }
    
    int test_api_design_3_facade()
    {
    	Facade facade;
    	facade.OperationWrapper();
    
    	return 0;
    }
    
    void StockObserver::Update()
    {
    	fprintf(stdout, "%s: %s, can't play stock\n", name_.c_str(), sub_->action_.c_str());
    }
    
    void NBAObserver::Update()
    {
    	fprintf(stdout, "%s: %s, can't watch NBA\n", name_.c_str(), sub_->action_.c_str());
    }
    
    int test_api_design_3_observer()
    {
    	SubjectBase* subject = new SecretarySubject();
    
    	ObserverBase* observer1 = new NBAObserver("Jack", subject);
    	ObserverBase* observer2 = new StockObserver("Tom", subject);
    
    	subject->Attach(observer1);
    	subject->Attach(observer2);
    
    	subject->action_ = "boss comes";
    	subject->Notify();
    
    	delete subject;
    	delete observer1;
    	delete observer2;
    
    	return 0;
    }
    
    
    int test_api_design_3()
    {
    	//return test_api_design_3_pimpl();
    	//return test_api_design_3_singleton();
    	//return test_api_design_3_monostate();
    	//return test_api_design_3_factory();
    	//return test_api_design_3_factory_expand();
    	//return test_api_design_3_proxy();
    	//return test_api_design_3_adapter();
    	//return test_api_design_3_facade();
    	return test_api_design_3_observer();
    }

    3.1 Pimpl惯用法:”pointer to implementation”(指向实现的指针)的缩写。该技巧可以避免在头文件中暴露私有细节。因此它是促进API接口和实现保持完全分离的重要机制。但是Pimpl并不是严格意义上的设计模式(它是受制于C++特定限制的变通方案),这种惯用法可以看作是桥接设计模式的一种特例。

    使用Pimpl惯用法将实现细节从公有头文件中分离出来。Pimpl利用了C++的一个特点,即可以将类的数据成员定义为指向某个已经声明过的类型的指针。这里的类型仅仅作为名字引入,并没有被完整地定义,因此我们就可以将该类型的定义隐藏在.cpp文件中。这通常称为不透明指针,因为用户无法看到它所指向的对象细节。本质上,Pimpl是一种同时在逻辑上和物理上隐藏私有数据成员与函数的办法。Pimpl惯用法将所有私有成员放置在一个类(或结构体)中,这个类在头文件中前置声明,在.cpp文件中定义

    使用Pimpl惯用法时,应采用私有内嵌实现类。只有在.cpp文件中其它类或自由函数必须访问Impl成员时,才应采用公有内嵌类(或公有非内嵌类)

    如果没有为类显式定义复制构造函数和赋值操作符,C++编译器会默认创建。但是这种默认的构造函数只能指向对象的浅复制。这不利于采用Pimpl的类,因为这意味着如果客户复制了对象,则这两个对象将指向同一个Impl实现对象。这样一来,两个对象可能在析构函数中尝试删除同一个Impl对象,这很可能导致崩溃。处理这个问题有以下两种可选方案:禁止复制类或显式定义复制语义。

    注意Pimpl类的复制语义,可以使用智能指针管理指向Impl对象的指针的初始化和销毁。

    Pimpl惯用法的主要缺点是,必须为你创建的每个对象分配并释放实现对象。这使对象增加了一个指针,同时因为必须通过指针间接访问所有成员变量,这种额外的调用层次与新增的new和delete开销类似,可能引入性能冲击。如果关注内存分配器的性能,那么可以考虑使用”快速Pimpl”(Fast Pimpl)惯用法,该方法为Impl类重载了new和delete操作符,以便使用更加高效的小内存定长分配器。

    3.2 单例:单例设计模式用来确保一个类仅存在一个实例。该模式亦提供对此唯一实例的全局访问点。可以认为单例是一种更加优雅的全局变量。单例模式要求创建一个类,它包含一个静态方法,每次调用该方法时返回该类的同一个实例。

    单例是一种更加优雅地维护全局状态的方式,但始终应该考虑清楚是否需要全局状态。

    将构造函数、析构函数、复制构造函数以及赋值操作符声明为私有(或受保护的),可以实现单例模式。

    不同编译单元中的非局部静态对象的初始化顺序是未定义的。因此,初始化单例的途径之一是在类的方法中创建静态变量。但是该方式不是线程安全的。

    使用C++创建线程安全的单例是困难的。可以考虑使用静态构造函数或API初始化函数对其进行初始化。

    依赖注入是一种将对象传入到某个类中(注入),而不是让这个类自己创建并存储该对象的技巧。依赖注入使采用了单例的代码更易于测试。

    单一状态(Monostate)设计模式允许创建类的多个实例,这些实例使用相同的静态数据。如果不需要惰性初始化全局数据,或者想让类的唯一性透明化,可以考虑使用单一状态代替单例。单例仅允许创建一个实例,因此强制了唯一性的结构,而单一状态使得所有实例共享同一份数据,因此强制了唯一性的行为。

    单例模式有一些替代方案,包括依赖注入、单一状态模式以及使用会话上下文。

    3.3 工厂模式:它是一个创建型的设计模式。它允许创造对象时不指定要创建的对象的具体类型。本质上,工厂方法是构造函数的一般化。从基本层面来看,工厂方法仅是一个普通的方法调用,该调用返回类的实例。但是,它们经常和继承一起使用,即派生类能够重写工厂方法并返回派生类的实例。常见的做法是使用抽象基类实现工厂模式

    在C++中,构造函数没有运行时动态绑定的概念,不能声明虚构造函数,没有返回值,构造函数的名字与它所在的类的名字相同。

    使用工厂方法提供更强大的类构造语义并隐藏子类的细节。

    3.4 API包装器模式:潜在副作用是影响性能,这主要因为额外增加的一级间接寻址以及存储包装层次状态带来的开销。结构化设计模式可以处理接口包装任务。

    (1).代理模式:代理设计模式为另一个类提供了一对一的转发接口。代理类和原始类有相同的接口。它可以被认为是一个单一组件包装器。此模式通常的实现是,代理类中存储原始类的副本,但更可能是指向原始类的指针,然后代理类中的方法将重定向到原始类对象中的同名方法。代理提供了一个接口,此接口将函数调用转发到具有同样形式的另一个接口。

    (2).适配器模式:将一个类的接口转换为一个兼容的但不相同的接口。与代理模式的相似之处是,适配器设计模式也是一个单一组件包装器,但适配器类和原始类的接口可以不相同

    适配器将一个接口转换为一个兼容的但不相同的接口。

    适配器可以用”组合”或者”继承”来实现。这两种类型分别称为对象适配器和类适配器。

    (3).外观模式:能够为一组类提供简化的接口。它实际上定义了一个更高层次的接口,以使得底层子系统更易于使用。外观模式和适配器模式的区别是,外观模式简化了类的结构,而适配器模式仍然保持相同的类结构

    外观模式为一组类提供了简化的接口。在封装外观模式中,底层类不再可访问。

    3.5 观察者模式:支持组件解耦且避免了循环依赖

    模型--视图--控制器(Model-View-Controller, MVC)架构:此模式要求业务逻辑(Model)独立于用户界面(View),控制器(Controller)接收用户输入并协调另外两者。MVC架构模式促使核心业务逻辑(或者说模型)与用户界面(或者说视图)分离。它还隔离了控制器逻辑,控制器逻辑会引起模型的改变并更新视图。

    观察者模式是”发布/订阅”范式(Publish/Subscribe,简写做pub/sub)的一个具体实例。这些技术定义了对象之间一对多的依赖,使得发布者对象能够将它的状态变化通知给所有的订阅对象,而又不直接依赖于订阅者对象。实现观察者模式的典型做法是引入两个概念:主题(Subject)和观察者(Observer),也称作发布者和订阅者。一个或多个观察者注册主题中感兴趣的事件,之后主题会在自身状态发生变化时通知所有注册的观察者。

    4. 设计

    4.1 良好设计的例子:

    4.2 收集功能性需求:软件产业中的需求可以分为不同的类型,包括如下几种:(1).业务需求:即软件如何满足组织的需求。(2).功能性需求:即软件应该完成什么功能。(3).非功能性需求:描述软件必须达到的质量标准。

    功能性需求规定了API如何表现。

    功能性需求一般用需求文档来管理,其中每个需求都有一个唯一的标识符和一个描述信息。好的功能性需求应该是简单、易读、清晰、可验证的,而且没有开发术语。

    4.3 创建用例:用例从用户的角度描述API的需求。

    用例可以是面向目标的简短描述信息的简要列表,也可以是更为正式的模板定义的结构化说明。

    用户故事是敏捷开发过程中一种从用户那获得最小需求的方法。用户故事是一个高层的需求概念,它仅包含了足够的信息,开发者可以利用这些信息对实现用户故事所需要付出的努力给出一个合理的评估。

    4.4 API设计的元素:产生好的API设计的秘诀是:对问题领域进行合理抽象,然后设计相应的对象与类的层次结构来表达该抽象。抽象是关于一些事务的简单描述,不需要对编程实现有任何了解即可理解。

    API设计包括开发顶层架构和详细的类层次结构。

    4.5 架构设计:软件架构描述了一个完整系统的很粗糙的结构:API中顶层对象的集合以及它们彼此之间的关系。

    架构设计被众多独特的组织、环境和运行等因素约束。有些约束是可以协商的。总是为变化而设计。变化是不可避免的。

    API的关键对象并不容易识别,请尝试从不同的角度看待问题,并不断迭代和完善你的模型。

    要避免API各个组件间的循环依赖。

    在API的附属文档中要描述其高层架构并阐述其原理。

    4.6 类的设计:要集中精力设计定义了API 80%功能的20%的类。

    避免深度继承层次结构。

    除非使用接口和mixin类,否则要避免多重继承。组合优先于继承。

    Liskov替换原则(Liskov Substitution Principle, LSP)指出,在不修改任何行为的情况下用派生类替换基类,这应该总是可行的。

    开放--封闭原则(Open/Closed Principle, OCP):API应该为不兼容的接口变化而关闭,为功能扩展而开放。

    迪米特法则(Law of Demeter, LoD)指出,你应该只调用自己类或直接相关对象的函数。

    4.7 函数设计:避免过长的参数列表。对于有很多可选参数的函数,你可以考虑通过传递结构体(struct)或映射(map)来代替。

    API中处理错误条件的三种主要方式是:返回错误码;抛出异常;中止程序。使用一致的、充分文档化的错误处理机制。

    如果你选择在代码中使用异常来通知意外情况,从std::exception派生自己的异常,并定义what()方法来描述失效信息。

    在出现故障时,让API快速干净地退出,并给出完整精确的诊断细节。

    5. 风格

    5.1 纯C API:

    为实现更严格的类型检查,并确保C++程序可以使用你的API,请尝试用C++编译器来编译C API。相对于C编译器而言,C++编译器的类型检查更为严格

    C函数的链接方式与C++函数不同。也就是说,同样的函数,如果由C和C++编译器分别生成目标文件,该函数的表示是不同的。将C API包装在一个extern “C”构造块中,这会告诉C++编译器,构造块中的函数应该使用C风格的链接方式。C编译器不能解析该语句,所以最好使用条件编译,使之仅支持C++编译器。

    在C API的头文件中使用extern “C”限制,以便于C++程序能正确地编译和链接C API

    5.2 面向对象的C++ API:面向对象API允许使用对象而非动作来建模软件,同时也带来了继承和封装等优点。

    5.3 基于模板的API:模板是C++的一个特性,它支持使用泛化的、尚未具体指定的类型编写函数或类。然后,你可以用具体类型实例化这些泛化的类型,以此实现模板的特化。模块可以在编译时执行一些工作,进而改进运行时性能。

    5.4 数据驱动型API:数据驱动型程序指的是:通过每次运行时提供不同的输入数据,它可以指向不同的操作。程序的业务逻辑能够抽象到可以由人来编辑的数据文件中去。利用这种方法,可以在不重新编译可执行文件的情况下改变程序的行为。

    数据驱动型API可以很好地映射到Web服务和其他客户端/服务器形式的API,它们也支持数据驱动型测试技术。

    6. C++用法

    6.1 命名空间:是若干唯一符号的逻辑分组。它提供了一种避免命名冲突的方法,以防两个API使用相同的名字定义符号。任何时候都不应该在公用API头文件的全局作用域内使用using关键字。

    应当始终使用一致的前缀或C++的namespace关键字为API符号提供命名空间。

    6.2 构造函数和赋值:如果类分配了资源,则应该遵循”三大件”(Big Tree)规则,同时定义析构函数、复制构造函数和赋值操作符。

    考虑在只带有一个参数的构造函数的声明前使用explicit关键字,用于阻止构造对象时特定的构造函数被隐式调用。

    6.3 const正确性:是指使用C++的const关键字将变量或者方法声明为不可变的。

    尽可能早地将函数和参数声明为const。过后修正API中的const正确性会既耗时又麻烦。

    当向const函数传入引用或指针时,