一、接口
接口,在前面已经分析了几篇相关的文章了。但具体到c++如何实践开发,则需要在本文再唠叨几句。接口的定义和作用,对于绝大数的开发者来说,应该已经门儿清,这里就不再重复,不清楚的可以查看前面的相关文章。
在c++的开发实践中,只要程序稍微大一些,就不可避免的要有接口的处理。它对于模块的安全性、复用性非常重要。同时,对于大规模的软件开发来说,分层和跨域的接口设计更是重中之重。
在设计中,接口是分层和模块间解耦的重要实现手段。相同的接口,因为应用的场景不同,可能有不同的实现。它们有可能依赖于版本的控制也有可能基于自动反射的控制等等。这也解决了对外封闭,对内灵活的的处理机制。
二、种类
接口的形式上的本质是什么?其实就是函数。无论是专门接口函数,还是暴露出来的抽象的接口类型(包括语言专有的接口定义以及类似c++这种以抽象类或其它形式提供接口),其内部真正操作的仍然是函数。
即从形式上的表现来看,主要分为两大类,即函数和抽象类型。
对于c++来说,实现接口的细分方式有很多种方式:
1.函数
这是接口最常见的形式,它不但包括普通的函数,也包括模板函数、抽象函数、回调函数、函数指针以及lambda表达式等
using cbfunc = void (*)(int);
2.类
这在于内部接口的实现中较为常见,一般是通过抽象类来实现类似于其它语言的interface。当然,有的也可以提供普通类或结构体来实现接口。比如前面提到的不同通信协议下的数据协议的实现
class iaction {
public:
virtual double play() const = 0;
virtual ~iaction() = default;
};
struct data1
{
int d;
};
struct data2:public data1{};
3.模板
这种实现和类、函数一致,只是抽象的形式改为了模板泛型。更加容易适配较为广泛的开发和应用。这里就不再给出具体的例子
4.新标准中的concepts等
这种属于一种设计型的接口控制,包括前面的sfinae等一些特定的用法等
template<class t>
concept checkdemo = requires(t t) {
t.must();
};
三、数据传输协议和格式
之所以把这节专门列出,主要是面对库、框架或分布式的设计者。
普通的接口应用往往用到的较少或者说不建议盲目的引入:
- c++的对象
在这种接口的数据处理中,往往使用各种对象如容器、tuple以及继承对象等等 - 常见的数据格式
常见的数据格式有xml、yaml、json以及二进制的框架格式如protobuf、messagepack及avro等 - 常见的数据协议
常见的协议包括restful、grpc、websocket、soap和graphql等等
这个不是重点,所以就不再一一展开说明,有兴趣可以自行查看,都是比较成熟的技术。
三、c++中设计实践
在前面的分析中,更强调的是接口的规范性和实现的方式,但缺少了对其整体的考虑:
1.接口
对于接口的设计,场景有很多种情况,但需要注意的千万不能教条或僵化的套用相关的接口设计方法。常见的接口应用场景有以下几种情况:
自己应用:这种相对灵活,哪种方式就看实现的方便、快捷程度
对内应用:这就需要考虑一定的适配性和升级的扩展,但仍然看重快捷方便
对外应用:重点考虑的是适用性,特别是作为底层支持时,更应该考虑各种场景下的可用性
强制接口应用:这种就是调用或指定对某些应用提供接口服务,需要严格对齐需求。其次才能考虑方便快捷以及扩展性等
跨平台(语言)应用:这种更应该考虑的是抽象平台和语言的兼容性。不要引入对平台或语言耦合性的技术点和数据类型
2.参数和返回值
这是接口中重要的一环,看上去简单,但如果对外开放或跨平台支持时,就需要仔细斟酌。
比如参数的类型、数量。返回值的类型。参数的顺序有没有依赖等等
3.数据协议或格式
在传统接口设计中,对数据传输是十分敏感的。如字节序、对齐等等。但随着互联网应用的发展,如json,xml以及其它各种数据格式或封装都涌现出来。这时就考验设计者如何在效率、成本、安全等方面上的综合考虑了。比如为了一个简单的字符串传递引入了json,又需要引入相关的解析库,是不是值得?未来是不是需要更多的使用json等等
可以这样理解,接口的设计要根据场景和实际需求的变化,不断的进行调整。而不是简单的认为因袭传统或书本上怎么教就怎么做。灵活是c++的灵魂!接口设计亦是如此。
四、分析
在明白了接口及其设计的复杂性之后,就可以有针对性的学习别人设计思想来完善自己的设计思想。
一般来说,对开发者经常遇到的接口有三种情况:
- 简单的函数接口
这种简单的函数接口,更强调的是一种显式的约束。让应用者能够以类似“防呆”的调用进行应用 - 开发框架和库的接口
这种对框架和库的接口设计,除了上面的约束外,还需要考虑对支持的平台、语言等进行兼容性设计以及异常情况的反馈。它的接口抽象可能更高级,适配性更强。可以引入更多的设计方式如注入等。在框架中往往还会引入异步接口这种形式。 - 分布式接口
它更强调的是“契约”。往往这种接口是建立在某种广泛的协议或通信框架基础上。对异构平台、不同系统等进行管理。接口形式上更贴近于常见的协议类型如restful等
从不同的角度看接口的设计和应用,必然呈现出不同的感官情况。这也是一个正常的应用形态。这也意味着,即使相同的设计方式,达到的目的不同,可能引入的侧重点也有所不同。
接口的适配
在实际的开发中,往往会遇到接口是较为抽象的(低级的),而c++中使用的相对要高级一些。这就需要对不同的应用进行接口的适配。隔离变化并适配使用。
一般接口的适配有以下几种方式:
- 使用仿函数
这是一种最简单的方式,通过重载operator(),将对象操作转为函数操作。它适合逻辑性强、比较简单的场景下 - 使用设计模式
经典的就是适配器模式,也可以使用策略模式的装饰器模式等等。看具体的情况。 - 使用模板
模板提供了各种特化、偏特化和全实例化。从而实现不同情况下的类型适配
其实到这里,大家是不是发现,设计本身就是要对开发有相当深入的理解,否则设计一定是一个“受限”的实现。说直白一些,猪都没见过,怎么可能画出猪来。
五、关注性能
回到c++的特性之一,性能。接口设计或适配很多情况下是为了双方的通信或数据处理,所以接口设计的好坏,很有可能影响要通信的效率和数据处理的速度。甚至可能影响到接口应用方的设计架构。
举一个简单的例子,接口中有一个大的对象或字符串,此时不使用引用,可能就会影响到效率。
所以在接口设计时需要考虑:
- 拷贝的消除
这个很好理解,不要进行无谓的大对象或大内存的拷贝 - 消除动态内存的分配
有些接口设计或适配可能需要进行动态内存的分配,比如使用了多态。这 - 消除中间环节调用
不要在设计或适配过程中加深函数的调用栈,虽然大多数情况下可能影响很小 - 允许的情况下使用内联
使用内联应该都明白,强调的还是速度 - 编译期预处理
把一些能够在编译期进行展开或计算的代码转移到编译期,在模板编译或元编程中是一种重要的优化手段
六、总结
接口非常常见,也很容易设计。可面临的场景复杂、应用各异时,对设计者的要求就相当高了。
大家可以看一下linux内核中相同接口的发展变化,从不同的角度来看为什么这个接口会如此变化?可能是采用了新技术,也可能是精简是功能…凡此种种。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论