读《C++ API设计》

时间:2020-09-17 21:23:12   收藏:0   阅读:38

读《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

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;
};

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

适配器

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

优点如下:

外观

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

常见用途:

观察者

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

设计

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

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

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

架构约束可以细分为:

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

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

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

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

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

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

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

组合优先于继承。

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

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

常见的互补的术语:

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

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

风格

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

C++用法

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

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

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

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

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

性能

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

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

// head.h
#ifndef _HEAD_
#define _HEAD_
#endif

#ifndef _HEAD_
#include "head.h"
#endif

版本控制 (TODO: Read Again)

主.次.补丁

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

文档

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

doxygen常用命令:

测试

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

非功能测试:

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

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

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

google mock

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

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

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

脚本化 (TODO: read again)

可扩展性

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

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

为API设计插件时的决策:

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

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!