API设计浅析
来源 https://www.cnblogs.com/grass-and-moon/p/13687369.html
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 {};
============ End