工程实践:c++接口设计指北(代码片段)

CodeBowl CodeBowl     2023-01-23     367

关键词:

最近在工作中,需要将代码封装成库,供其他方调用。在其中涉及到如何设计接口类,第一次接触,将总结和经验记录下来。

导读

为什么本文叫做《工程实践:C++的接口设计》,是因为,我们大部分人入门的时候,都是调用别人封装好的库函数,却没有尝试过自己封装库给别人用。但是在正常工作中,也就是工程化中,我们会经常封装库给其他人应用,这里面会涉及到怎么封装一个函数,提供一个优秀的接口。
接口:类暴露出来的部分,是类所提供的功能。

接口设计准则

我们在工程化的过程中,设计接口一般遵守以下几个准则:

  1. 单一功能原则
    一个class就其整体应该只提供单一的服务。如果一个class提供多样的服务,那么就应该把它拆分,反之,如果一个在概念上单一的功能却由几个class负责,这几个class应该合并。
  2. 开放/封闭原则
    一个设计并实现好的class,应该对扩充的动作开放,而对修改的动作封闭。也就是说,这个class应该是允许扩充的,但不允许修改。如果需要功能上的扩充,一般来说应该通过添加新类实现,而不是修改原类的代码。添加新类不单可以通过直接继承,也可以通过组合。
  3. 最小惊讶原理
    在重载函数,或者子类实现父类虚函数时,应该基本维持函数原来所期望的功能。

接口设计注意事项

  1. 过度封装
    很多人喜欢这样封装,把接口局限在仅仅解决一个特定的问题上面,失去了代码的灵活性。而且,也容易出现面条代码,让人不知所云。一个接口被写的仅仅用于解决当前问题,当试图增加其扩展性时,发现为时已晚。
    为了防止过度封装,在设计接口的时候,我们应该考虑以下几个问题:

1.我们需要解决什么问题
2.问题的核心是什么
3.应该怎样设计,可以方便客户程序员扩展

  1. 起名要见名知意
    一个好的的接口方法名,是接口设计中成功的一半。

  2. 不要让使用者进行过多工作
    如果使用者使用我们的接口时,进行了过多的准备,那么对我们来说就是失败的。
    所以,设计的时候要记得尽可能简化客户程序员逻辑,使接口设计能够看起来简洁、漂亮,而不至于被接口的复杂性所吓倒。

  3. 简洁
    这个比较好理解,举个例子,你一定见过一个函数使用,需要传进去五六个参数,但是对我们有用的往往只有那么一俩个。
    所以,简洁设计是接口设计的一个重要原则。可以在接口的内部实现中,使用复杂冗余的参数,而在暴露给客户程序员的接口中,一定要尽可能简洁。

  4. 清晰的文档表述
    这也是我在工作中,最头疼的问题,头疼的不是我不会写,而是在使用其他人提供的库时,没有使用文档,使用起来是痛苦的,所以为了不让这种痛苦发生在其他人身上,我现在从维护一份良好的文档开始。

接口设计想达到的效果

隔离用户操作与底层逻辑

接口的俩种方法

一般来说,有两种方法设计接口类。
第一种是PIMP方法,即Pointer to Implementation,在接口类成员中包含一个指向实现类的指针,这样可以最大限度的做到接口和实现分离的原则。
第二种方法叫Object-Interface方法,它的思想是采用C++的动态功能,实现类继承接口类,功能接口函数定义成虚函数。

先说结论,我们处于自身习惯的原因,选择了Object-Interface方法。

PIMP方法

所谓PImp是非常常见的隐藏真实数据成员的技巧,核心思路就是用另一个类包装了所要隐藏的真实成员,在接口类中保存这个类的指针。

//header complex.h
class ComplexImpl;
class Complex
public:
    Complex& operator+(const Complex& com );
    Complex& operator-(const Complex& com );
    Complex& operator*(const Complex& com );
    Complex& operator/(const Complex& com );

private:
    ComplexImpl* pimpl_;
;

在接口文件中声明一个ComplexImpl*,然后在另一个头文件compleximpl.h中定义这个类

//header compleximpl.h
class ComplexImpl
public:
    ComplexImpl& operator+(const ComplexImpl& com );
    ComplexImpl& operator-(const ComplexImpl& com );
    ComplexImpl& operator*(const ComplexImpl& com );
    ComplexImpl& operator/(const ComplexImpl& com );

private:
    double real_;
    double imaginary_;
;

可以发现,这个ComplexImpl的接口基本没有什么变化(其实只是因为这个类功能太简单,在复杂的类里面,是需要很多private的内部函数去抽象出更多实现细节),然后在complex.cpp中,只要

#include "complex.h"
#include "compleximpl.h"

包含了ComplexImpl的实现,那么所有对于Complex的实现都可以通过ComplexImpl这个中介去操作。详细做法百度还有一大堆,就不细说了。

Object-Interface 抽象基类法

一般来说,如果一个接口类对应有若干个实现类,可以采用这种方法。

上面我们讲了plmp方法,我们隐藏掉俩个数据成员,但同时也多出了一个新的数据成员,也就是接口指针,那么有没有方法,连这个指针也不要呢?

这时候就是抽象基类发挥作用的时候了。看代码:

class Complex
public:
    static std::unique_ptr<Complex> Create();

    virtual Complex& operator+(const Complex& com ) = 0;//纯虚函数,接口成员函数
    virtual Complex& operator-(const Complex& com ) = 0;
    virtual Complex& operator*(const Complex& com ) = 0;
    virtual Complex& operator/(const Complex& com ) = 0;
;

将要暴露出去的接口都设置为纯虚函数,通过 工厂方法Create来获取Complex指针,Create返回的是继承实现了集体功能的内部类;

//Complex类功能的内部实现类
class ComplexImpl : public Complex
public:
    virtual Complex& operator+(const Complex& com ) override;
    virtual Complex& operator-(const Complex& com ) override;
    virtual Complex& operator*(const Complex& com ) override;
    virtual Complex& operator/(const Complex& com ) override;
private:
    double real_;
    double imaginary_;

至于Create函数也很简单:

std::unique_ptr<Complex> Complex::Create()

    return std::make_unique<ComplexImpl>();

这样,我们完完全全将Complex类的实现细节全部封装隐藏起来了,用户一点都不知道里面的数据结构是什么;

当然,对于Complex这样的类来说,用户是有获取他的实部虚部这样的需求的,也很简单,再加上两个Get方法就可以达到目的。

Object_interface 抽象基类示例代码

  1. 首先,声明一个接口
// circle.h
// 圆的接口类
class Circle 
public:
   virtual ~Circle() ;

   // 接口方法:面积
   virtual double area() = 0;
;
  1. 通过继承的方式实现这个接口
// circle_impl.h
#include "circle.h"
 
// 圆的具体实现类
class CircleImpl : public Circle 
 
private:
	double radius;
public:
	CircleImpl(double radius);
	double area() override;
;
// circle_impl.cpp
#include <cmath>
#include "circle_impl.h"
 
inline double pi() 
	return std::atan(1) * 4;
;
 
CircleImpl::CircleImpl(double _radius) : radius(_radius) 
;
 
double CircleImpl::area() 
	return pi() * radius * radius;
;
  1. 最后,通过管理类创建接口派生类的实例,或者销毁接口派生类的实例:
// circle_manager.h
#include "circle.h"
 
// 圆的创建工厂类
class CircleManager 
public:
    static Circle* create(double radius);     // 创建circle实例
    static void destroy(Circle* circlePtr);   // 销毁circle实例
;
// circle_manager.cpp
#include "circle_manager.h"
#include "circle_impl.h"
 
Circle* CircleManager::create(double radius) 
    Circle* circlePtr = new CircleImpl(radius);
 
    return circlePtr;
;
 
void CircleManager::destroy(Circle* circlePtr) 
    delete circlePtr;
; 

现在我们接口已经实现完毕了,我们可以把它封装成库,给其他人使用了,这里封装库我们就不多言了。

最后,来看一下使用效果:

// main.cpp
#include <iostream>
#include "circle_manager.h"
#include "circle.h"
 
int main() 

    Circle* circlePtr = CircleManager::create(3);
    cout << circlePtr->area() <<endl;
    CircleManager::destroy(circlePtr);
    
    system("pause");
 
    return 0;

以上代码只提供给外部circle的接口,circle的实现完全被隐藏了起来,外部将无从知晓,外部使用者只能通过circle管理类生成circle的派生类的实例。外部使用者得到circle派生类的实例后,除了能调用接口暴露的方法area()外,其它什么也做不了,这样就完全达到了使用接口的最终目标。

参考资料

C++中的接口设计准则
C++ 头文件接口设计浅谈
一款优秀的 SDK 接口设计十大原则
C++:如何正确的使用接口类

总结

本篇文章抛砖引玉,自己也是刚刚接触,写完收工,干饭去!

前端codereview指北(代码片段)

作者:magentaqin,腾讯CSIG前端开发工程师说到CodeReview,经常有同学会问,究竟从哪些方面下手?除了一些抽象的Review原则,有没有更细化的实施准则来指导实践?PCG代码委员会曾推出过通道晋级代码检... 查看详情

软件工程第二次实践(代码片段)

实现一个能够对文本文件中的单词的词频进行统计的控制台程序1.Github项目地址:xinz2.PSP表格3.解题思路描述:拿到题目之后,先总的浏览了两遍,大致上明白了题意和需要用的知识。首先得先通过学习学会如何使用c++读取和写... 查看详情

django快速开发实践:drf框架和xadmin配置指北(代码片段)

步骤既然是快速开发,那废话不多说,直接说步骤:安装Djagno安装DjangoRestFramework定义models定义Restframework的serializers定义Restframework的viewsets配置Restframework的router配置管理后台admin根据需要写template和对应的view经过这些步骤就能得... 查看详情

08-微服务版单点登陆系统(sso)实践(2107)(代码片段)

...点登陆系统解决方案设计单点登陆系统初步设计服务设计工程结构设计SSO父工程创建及初始化创建父工程父工程pom文件初始配置系统基础服务工程设计及实现业务描述表结构设计工程数据初始化创建系统服务工程并初始化Pojo对... 查看详情

海量node.js网关的架构设计与工程实践!(代码片段)

expressrequestUpstream,resolveUpstreamapp=express()upstream=resolveUpstream(req.method,req.path,req.headers)response=requestUpstream(upstream,req.body)res.send(response)port==>console.log(`Appliste 查看详情

go应用性能优化指北(代码片段)

...流程依然在持续向线上转移,转移过程中,作为工程师,我们会碰到各种各样的性能问题。互联网公司本质是将用户共通 查看详情

c++类设计和实现的十大最佳实践

C++代码提供了足够的灵活性,因此对于大部分工程师来说都很难把握。本文介绍了写好C++代码需要遵循的10个最佳实践,并在最后提供了一个工具可以帮助我们分析C++代码的健壮度。原文:10Bestpractic... 查看详情

c++类设计和实现的十大最佳实践

C++代码提供了足够的灵活性,因此对于大部分工程师来说都很难把握。本文介绍了写好C++代码需要遵循的10个最佳实践,并在最后提供了一个工具可以帮助我们分析C++代码的健壮度。原文:10Bestpractic... 查看详情

c++类设计和实现的十大最佳实践

...1;C++代码提供了足够的灵活性,因此对于大部分工程师来说都很难把握。本文介绍了写好C++代码需要遵循的10个最佳实践,并在最后提供了一个工具可以帮助我们分析C++代码的健壮度。原文:10Bestpract... 查看详情

腾讯云十亿级node.js网关的架构设计与工程实践(代码片段)

作者|王伟嘉 编辑|孙瑞瑞 expressrequestUpstream,resolveUpstreamapp=express()upstream=resolveUpstream(req.method,req.path,req.headers)response=requestUpstream(upstream,req.body)res.send(response)port==>这样的机制 查看详情

c++接口设计的心得体会(代码片段)

今天在模块接口设计的时候,对如何设计一个好的接口,产生了疑惑。遂请教了俩位师傅,得到了俩条建议,私以为十分有用!1.接口中的参数最好不要用基础类型如果不考虑维护的话,我们提供一个接口&... 查看详情

实践|sentinel扩展性设计(代码片段)

摘要:Sentinel提供多样的SPI接口用于提供扩展的能力。用户可以在用同一个sentinel-core的基础上自行扩展接口实现,从而可以方便地给Sentinel添加自定义的逻辑。初始化逻辑扩展机制为了统一初始化的流程,我们抽象出了InitFunc接... 查看详情

设计模式之适配器模式(c++)(代码片段)

...十甚至更多的功能类,各个类组合起来就能实现一些工程上的技术,若不对其进行组合封装,则调用算法库的软件工程师(不具备专业技术背景)就不知道如何使用了。而通过适配器模式可以将复杂的功能封... 查看详情

打通源码,高效定位代码问题|云效工程师指北

简介:为了帮助企业和团队挖掘更多源代码价值以赋能日常代码研发、运维等工作,云效代码团队在大数据和智能化方向进行了一系列的探索和实践(例如代码搜索与推荐),本文主要介绍我们如何通过直接... 查看详情

c++工程实践必备技能(代码片段)

文章目录单元测试框架如何引入如何使用测试相关SUBCASETEST_SUITETEST_CASE_FIXTURETEST_CASE_TEMPLATE断言相关常用断言宏常用工具函数benchmark框架如何引入如何使用防止被优化优化不稳定比较测试结果计算BigO输出结果到其他格式CLion中查... 查看详情

工程实践:到底要不要使用智能指针(代码片段)

工程实践:到底要不要使用智能指针前言从需求开始探讨问题智能指针现状unique_ptr示例shard_ptr接口该不该使用智能指针智能指针作为函数参数智能指针作为函数返回值如何选择智能指针注意不要踩的“坑”总结参考资料前言... 查看详情

msfstagers开发不完全指北(代码片段)

...现。本系列将从之前这篇文章中获取到的原理性知识进行实践,一步步记录我在这个过程中踩到的坑与收获,这个系列可能会更新得比较慢,也可能会不定期鸽,希望大家能够一同学习,有什么错误的地方也欢迎 查看详情

自动化接口测试实践经验(代码片段)

作者:faithchen,腾讯PCG测试开发工程师一、背景自动化测试对于我们提升研发效能、CI/CD(持续集成/持续交付)是不可或缺的部分。在后台自动化测试中,接口测试尤为重要,它能够保证被测后台服务的质量,以... 查看详情