读《C++ API设计》

读《C++ API设计》

API简介

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

+-----------------------------------------------------+
|                                                     |
|            Second Life Viewer                       | 应 用 程 序 代 码
|                                                     |
+-----------------------------------------------------+

+-----------+ +-------------+ +-------------+
|           | |             | |             |
|  IICommon | | IIMessage   | | IIAudio     |   ...    内 部 API
|           | |             | |             |
+-----------+ +-------------+ +-------------+

+----------+ +-----+ +---------+ +---------+
|OpenGL    | | ARP | | Boost   | |OpenSSL  |           第 三 方 API
+----------+ +-----+ +---------+ +---------+

+-------------+ +--------------------------+
|标 准 C 库    | |  标 准 模 板 库          |           语 言 API
+-------------+ +--------------------------+

特征

本章主要用来回答下面这个问题:优质的API应该具有哪些基本特征。

getter,setter的优点:

  • 有效性验证
  • 惰性求值
  • 缓存
  • 额外的计算
  • 通知
  • 调试
  • 同步
  • 更精细的访问控制
  • 维护不变式关系

将私有功能声明为.cpp文件中的静态函数,而不要将其作为私有方法暴露在公开的头文件中。

疑惑之时,果断弃之!精简API中共有的类和函数。

避免将函数声明为虚函数,除非有合理且迫切的需求。使用时,需要谨记一下几点原则:

  • 如果类包含任一虚函数,那么必须将析构函数声明为虚函数。
  • 一定要编写文档,说明类的方法是如何相互调用的。
  • 绝不在构造函数或析构函数中调用虚函数,这些调用不会指向子类。

基于最小化核心API,以独立的模块或库的形式构建便捷API。

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

将资源的申请与释放当做对象的构造和析构。

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

优秀的API表现为松耦合高内聚。

模式

主要涉及的模式有:

  • Pimpl惯用法:支持在共有接口中完全隐藏内部细节。
  • 单例和工厂方法
  • 代理、适配器和外观:在现有的不兼容接口或遗留接口上封装API的各种途径
  • 观察者:该行为模式可以用来减少类之间的直接依赖。

Pimpl

Pimpl使用示例:

// with out pimpl
// autotimer.h
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif
#include <string>

class AutoTimer
{
public:
    explicit AutoTimer(const std::string &name);
    ~AutoTimer();

private:
    double GetElapsed() const;

    std::string mName;
#ifdef _WIN32
    DWORD mStartTime;
#else
    struct timeeval mStartTime;
#endif
};

// with pimpl
// autotimer.h
#include <string>

class AutoTimer
{
public:
    explicit AutoTimer(const std::string &name);
    ~AutoTimer();
private:
    class Impl;
    Impl *mImpl;
};

// autotimer.cpp
#include "autotimer.h"

#include <iostream>
#if _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif

class AutoTimer::Impl
{
public:
    double GetElapsed() const
    {
        ...
    }

    std::string mName;
#ifdef _WIN32
    DWORD mStartTime;
#else
    struct timeval mStartTime;
#endif
};

AutoTimer::AutoTimer(const std::string &name) :
    mImpl(new AutoTimer::Impl())
{
    mImpl->mName = name;
#ifdef _WIN32
    mImpl->mStartTime = GetTickCount();
#else
    gettimeofday(&mImpl->mStartTime,NULL);
#endif
}

AutoTimer::~AutoTimer()
{
    std::cout << mImpl->mName << ": took " << mImpl->GetElapsed() << " secs" << std::endl;
    delete mImpl;
    mImpl = NULL;
}

单例

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

依赖注入,实现:

/// 此处传入的Database是一个单例,这样,在该类的内部不用反复调用GetInstance,
/// 同时,这样的操作方式使得接口更加易于测试,因为对象的依赖项可以被
/// 替换为桩对象(stub)或模拟对象(mock),以便执行单元测试
class MyClass
{
public:
    MyClass(Database *db) : mDatabase(db) {}
private:
    Database *mDatabase;
};

初版《设计模式》的作者指出,他们计划从原列表中移除的一个模式就是单例模式

工厂

让工厂类维护一个映射,此映射将类型名和创建对象的回调关联起来。示例:

#include "renderer.h"
#include <string>
#include <map>

class RendererFactory
{
public:
    typedef IRenderer* (*CreateCallback)(); // 此处的CreateCallback可以是具体类的对应的Create函数
    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> CallbackMap;
    static CallbackMap mRenderers;
}

代理

代理提供了一个接口,此接口将函数调用转发到具有相同形式的另一个接口。

class Proxy
{
public:
    Proxy() : mOrig(new Original()) {}
    ~Proxy() { delete mOrig; }

    bool DoSomething(int value) { return mOrig->DoSomething(value); }
private:
    Proxy(const Proxy&);
    const Proxy &operator=(const Proxy&);
    Original *mOrig;
};

使用代理模式的一些案例:

  • 实现原始对象的惰性实例,当需要的时候才创建原始对象
  • 实现对原始对象的访问控制
  • 支持调试模式,实现不调用原始对象的方法,用来调试接口
  • 支持资源共享,多个proxy对象,共享相同的原始基础类。
  • 应对original类将来被修改

适配器

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

优点如下:

  • 强制API始终保持一致性。
  • 包装API的依赖库
  • 转换数据类型
  • 为API暴露一个不同调用约定

外观

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

常见用途:

  • 隐藏遗留代码
  • 创建便捷API
  • 支持简化功能或者替代功能的API

观察者

观察者支持组件解耦且避免了循环依赖。

设计

+----------------------+         +------------------------+      +---------------+
|     Analyze          |         |       Design           |      |     Implement |
|                      |         |                        |      |               |
|    Requirement       |         |     Architecture       |      |     Coding    |
|                      +--------->                        +------>               |
|    User Case         |         |    Class Design        |      |     Testing   |
|                      |         |                        |      |               |
|    User's Story      |         |     Method Design      |      |     Document  |
|                      |         |                        |      |               |
|                      |         |                        |      |               |
+----------^-----------+         +------------^-----------+      +--------^------+
        |                                  |                           |
        |                                  |                           |
        |                                  |                           |
        +----------------------------------+---------------------------+

演进式实现一个不错的选择是,将丑陋的旧代码隐藏在精心设计的新的API之后,然后利用这些整洁的API逐步更新所有客户端代码,并将代码自动化测试下。

创建API的架构过程可以分解为4个基本步骤:

  • 分析影响架构的功能性需求;
  • 识别架构的约束并加以说明;
  • 创造系统中的主要对象,并确认它们之间的关系;
  • 架构的交流与文档

架构约束可以细分为:

  • 组织因素:预算,时间表,团队大小与专业知识,软件开发过程,决定子系统是自己构建还是购买,管理焦点等;
  • 环境因素:硬件,平台,软件约束、客户端/服务器约束,协议约束,文件格式约束,数据库依赖,开发工具等
  • 运行因素:性能,内存利用率,可靠性,可用性,并发性,可定制型,可扩展性,脚本功能,安全性,国际化,网络带宽

识别主要抽象,openscenegraph api顶层架构:

                                +-----------+
                    视 图         |           |
                    ^         | 节 点 工 具 包 |
遍 历 器 +                |         |           |
    +---------->     |         |           |
                    +         |   仿 真     |
                场 景 图 渲 染 <---+           |
数 据 库 +---------->               |   地 形     |
^                              |           |
|                              |   动 画     |
+                              |           |
插 件                             +-----------+

一些比较流行架构模式的一个分类:

  • 结构化模式:分层模式,管道与过滤器模式和黑板模式
  • 交互式系统:MVC,MVP,表示-抽象-控制模式
  • 分布式系统:客户端/服务器模式,三层架构,点对点模式以及代理模式。
  • 自适应系统:微内核模式与反射模式

循环依赖意味着无法对每个组件进行单独测试,也不能在不牵扯组件的情况下复用另一个组件。基本上要理解任何一个组件都必须理解全部组件。

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

要集中精力设计定义了API80%功能的20%的类。

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

组合优先于继承。

开闭原则:类的目标应该是为扩展而开放,为修改而关闭。它关注的焦点是创建可以长期使用的稳定性接口。

迪米特法则,一个函数可以做的事情只包括:

  • 调用同一个类的其它函数
  • 在同一个类的数据成员上调用函数
  • 在它接受的任何参数上调用函数
  • 在它创建的任何局部对象上调用函数
  • 在全局对象上调用函数

常见的互补的术语:

  • Add/Remove
  • Begin/End
  • Create/Destroy
  • Enable/Disable
  • Insert/Delete
  • Lock/Unlock
  • Next/Previous
  • Open/Close
  • Push/Pop
  • Send/Receive
  • Show/Hide
  • Source/Target

使用一致的、充分文档化的错误处理机制,返回错误码,抛出异常,中止程序。

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

风格

本章会介绍四种风格迥异的API

  • 纯C API:func(obj,a,b,c)
  • 面向对象的C++ API: obj.func(a,b,c)
  • 基于模板的API
  • 数据驱动型API:send("func",a,b,c); 这类接口特定是,将参数通过灵活的数据结构打包,连通命名的命令一起发送给数据程序,而不是调用特定的方法或自由函数。

C++用法

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

考虑在只带有一个参数的构造函数的声明前使用explicit关键字。

避免使用友元。它往往预示着糟糕的设计,这就等于赋予用户访问API所有受保护成员和私有成员的权限。

使用内部链接以便隐藏.cpp文件内部的、具有文件作用域的自由函数和变量。也就是说,使用static关键字或匿名命名空间。

应该显示导出共有API的符号,以便维持对动态库中类、函数和变量访问性的直接控制。对于GNU C++,可以使用__fvisibility_hidden选项。

性能

不要以扭曲API的设计为代价换取高性能。

为优化API,应使用工具收集代码在真实运行示例中的性能数据,然后把优化精力集中在实际的瓶颈上。不要猜测性能瓶颈的位置。

  • const引用
  • 前置声明
  • 冗余的include警戒语句
// head.h
#ifndef _HEAD_
#define _HEAD_
#endif

#ifndef _HEAD_
#include "head.h"
#endif
  • 应该使用extern声明全局作用域的常量,或者在类中以静态const 方式声明常量,然后再.cpp中定义常量
  • 初始化列表
  • Vector.h detail/Vector.h
  • 写时复制
  • 时效分析
    • 内嵌测量,代码内嵌计时器
    • 二进制测量
    • 采样
    • 监控计数器
  • 基于内存的分析:IBM Rational Purify,Valgrind,Parasoft Insure++,Coverity
  • 多线程分析:Intel Thread Checker,Helgrind,DRD

版本控制 (TODO: Read Again)

主.次.补丁

只在必要时再分支,尽量延迟创建分支的时机。尽量使用分支代码线路而非冻结代码线路。尽早且频繁的合并分支。

文档

复用做起来远不如说起来那么简单,它同时需要良好的设计和优秀的文档。即使我们发现了难得一见的良好设计,如果没有优秀的文档,这个组件就很难得以复用。

doxygen常用命令:

  • \file [<文件名>]
  • \class <类名>[<头文件>][<头文件名>]
  • \brief 简要说明
  • \author
  • \date
  • \param
  • \param[in]
  • \param[out]
  • \param[in,out]
  • \return
  • \code \endcode
  • \verbatim <字面文本块> \endverbatim
  • \exception 异常对象 描述
  • \deprecated 解释及替代品
  • \attention 需要注意的消息
  • \warning 警告消息
  • \version
  • \bug
  • \see
  • \name 组名

测试

为了确保不破坏用户程序,编写自动化测试所能采取的措施中最重要的一项。

非功能测试:

  • 性能测试
  • 负载测试
  • 可扩展性测试
  • 浸泡测试:尝试长期持续地运行软件
  • 安全性测试
  • 并发测试

API测试应组合使用单元测试,和集成测试,也可以适当运用非功能性测试,如性能、并发、安全。

单元测试是一种白盒测试技术,用于独立验证函数和类的行为。

如果代码依赖于不可靠的资源,比如数据库、文件系统或网络,那么可以使用桩对象或模拟对象创建个更健壮的单元测试。

google mock

使用SelfTest()成员函数测试类的私有成员。

使用断言记录和验证那些绝不应该发生的程序设计错误。

#ifdef DEBUG
#include <assert.h>
#else
#define assert(func)
#endif

脚本化 (TODO: read again)

可扩展性

Qt工具包可以通过QPluginLoader来扩展。

一般如果要创建插件系统,有两个主要特性是必须要设计的。

  • 插件API:要创建插件,用户必须编译并连接插件API
  • 插件管理器:核心API的一个对象,负责管理所有插件的声明周期,插件的加载、注册、卸载等各个阶段。该对象也叫做插件注册表。

为API设计插件时的决策:

  • C还是C++:c可以跨平台跨编译器
  • 内部元数据还是外部元数据
  • 插件管理器是通用还是专用
  • 安全性
  • 静态库还是动态库

C++实现插件

开源库DynObj。

插件API

插件应该提供两个最基本的回调函数,初始化和清理函数。

// defines.h
#ifdef _WIN32
#ifdef BUILDING_CORE
#define CORE_API __declspec(dllexport)
#define PLUGIN_API __declspec(dllimport)
#else
#define CORE_API __declspec(dllimport)
#define PLUGIN_API __declspec(dllexport)
#endif
#else
#define CORE_API
#define PLUGIN_API
#endif

// renderer.h
class IRenderer
{
public:
    virtual ~IRenderer() {}
    virtual bool LoadScene(const char* filename) = 0;
    virtual void SetViewportSize(int w, int h) = 0;
    ...
};

// pluginapi.h
#include "defines.h"
#include "renderer.h"

#define CORE_FUNC extern "C" CORE_API
#define PLUGIN_FUNC extern "C" PLUGIN_API

#define PLUGIN_INIT() PLUGIN_FUNC int PluginInit()
#define PLUGIN_FREE() PLUGIN_FUNC int PluginFree()
typedef IRenderer *(*RendererInitFunc)();
typedef void (*RendererFreeFunc)(IRenderer*);

CORE_FUNC void RegisterRenderer(const char* type, RendererInitFunc init_cb, RendererFreeFunc free_cb);

插件示例:

// plugin1.cpp
#include "pluginapi.h"
#include <iostream>

class OpenGLRenderer : public IRenderer
{
public:
    ~OpenGLRenderer() {}
    ...
};

PLUGIN_FUNC IRenderer *CreateRenderer() { return new OpenGLRenderer(); }
PLUGIN_FUNC void DestroyRenderer(IRenderer* r) { delete r; }
PLUGIN_INIT()
{
    RegisterRenderer("opengl", CreateRenderer, DestroyRenderer);
    return 0;
}

插件管理器:

  • 加载所有插件的元数据
  • 将动态库加载到内存中,提供对库中符号的访问能力,并在必要时卸载
  • 初始化,清理
// pluginmanager.cpp
#include "defines.h"
#include <string>
#include <vector>

class CORE_API PluginInstance
{
public:
    explicit PluginInstance(const std::string& name);
    ~PluginInstance();
    bool Load();
    bool Unload();
    bool IsLoaded();
    std::string GetFileName();
    std::string GetDisplayName();
private:
    PluginInstance(const PluginInstance&);
    const PluginInstance &operator = (const PluginInstance&);
    class Impl;
    Impl *mImpl;
};

class CORE_API PluginManager
{
public:
    static PluginManager &GetInstance();
    bool LoadAll();
    bool Load(const std::string& name);
    bool UnloadAll();
    bool Unload(const std::string& name);
    std::vector<PluginInstance*> GetAllPlugins();
private:
    PluginManager();
    ~PluginManager();
    std::vector<PluginInstance*> mPlugins;
};

访问者模式

访问者模式的核心目标是,允许客户遍历一个数据结构中的所有对象,并在每个对象上执行给定的操作。

// 场景图层次结构的例子

                     +----------------+
                     |                |
      +--------------+   Transform0   +--------+
      |              |                |        |
      |              +------+---------+        |
      |                     |                  |
      |                     |                  |
      |                     |                  |
      |                     |                  |
      |                     |                  |
+-----v----+      +---------v------+   +-------v--------+
|          |      |                |   |                |
|  Light0  |      |  Transform1    |   |  Transform2    |
|          |      |                |   |                |
+----------+      +-+------------+-+   +-----------+----+
                    |            |                 |
                    |            |                 |
                    |            |                 |
                    |            |                 |
                    |            |                 |
             +------v-----+   +--v-------+    +----v--------+
             |            |   |          |    |             |
             |  Shape0    |   |  Shape1  |    |   Shape2    |
             |            |   |          |    |             |
             +------------+   +----------+    +-------------+

// nodevisitor.h
class ShapeNode;
class TransformNode;
class LightNode;

class INodeVisitor
{
public:
    virtual ~INodeVisitor() {}
    virtual void Visit(ShapeNode &node) = 0;
    virtual void Visit(TransformNode &node) = 0;
    virtual void Visit(LightNode &node) = 0;
};

// scenegraph.h
#include <string>
class INodeVisitor;
class BaseNode
{
public:
    explicit BaseNode(const std::string &name);
    virtual ~BaseNode() {}
    virtual void Accept(INodeVisitor &visitor) = 0;
private:
    std::string mName;
};

class ShapeNode : public BaseNode {};
class TransformNode : public BaseNode {};
class LightNode : public BaseNode {};

原文链接: https://www.cnblogs.com/grass-and-moon/p/13687369.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    读《C++ API设计》

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/202879

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午9:18
下一篇 2023年2月12日 下午9:18

相关推荐