zoukankan      html  css  js  c++  java
  • 读《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 <类名>[<头文件>][<头文件名>]
    • rief 简要说明
    • author
    • date
    • param
    • param[in]
    • param[out]
    • param[in,out]
    • eturn
    • code endcode
    • verbatim <字面文本块> endverbatim
    • exception 异常对象 描述
    • deprecated 解释及替代品
    • attention 需要注意的消息
    • warning 警告消息
    • version
    • ug
    • see
    • ame 组名

    测试

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

    非功能测试:

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

    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 {};
    
  • 相关阅读:
    计算机综合面试题总结
    MySQL入门很简单: 13 数据备份和还原
    MySQL入门很简单: 12 MYSQL 用户管理
    MySQL入门很简单: 11 mysql函数
    页面即使加过了移除监听事件,但是到新页面后事件任然存在
    git命令大全
    document.documentElement.scrollTop指定位置失效解决办法
    vue做商品选择如何保持样式
    vue父组件向子组件传对象,不实时更新解决
    vue 遮罩层阻止默认滚动事件
  • 原文地址:https://www.cnblogs.com/grass-and-moon/p/13687369.html
Copyright © 2011-2022 走看看