设计心得——隔离隐藏的初步实践

一、隔离隐藏

人们总是说"细节决定成败",但不是每个人对细节都把握的妥帖的。如是放任程序的设计把细节都暴露给每个开发者,那么细节就大概率决定失败了。所以在设计者的眼中,应该假定每个开发者都是不可信任的。

既然是是这样,设计就应该把细节的控制达到自我消除的地步。当然,这是一种理想状态,实际的情况下,出于各种原因和目的,设计者需要作出妥协和退让,但在原则的问题上则应该坚守立场,除非不可抵抗力量。所以,设计要尽量做到细节对外的屏蔽以及接口的最小化。也就是说,设计要隔离隐藏与对外接口无用的内容,这种对外,既指对第三方和应用者,也包括自己各个开发单元之间的通信。

在C++中,头文件的包含始终是让设计者和开发者头疼的问题,它不仅导致了头文件依赖的重编译问题,甚至顺序的不同都可能导致程序的编译错误。但最让开发者恼火的是,如果头文件的设计不妥,则有可能产生ABI的不兼容甚至暴露实现细节的内容。这对很多软件公司是不可能接受的。

二、实现的方法

既然明白了设计的目的和初衷,那么是不是可以找到一个一劳永逸的设计方法或设计模式来解决这类问题呢?每个人都希望如此,但实际不可能。世界上的各种环境都有所不同而且有可能在随时变化。如果一种方式能够满足所有的需求,那么设计师也就没有存在的必要了。

其实对设计师来说,最重要的一点就是根据不同的情况来进行不同的设计。也就是说,在实际的设计开发过程中,会有多种的设计方式来进行选择甚至是组合使用。它们主要有:

  1. 软件基础隔离方式
    这种方式是最简单、最基础的方式,直接暴露相关的接口,不考虑过多的风险内容。仅在实现中使用传统的模块和名空间等进行隔离控制。它适合于很小的且不怎么重要的项目,比如几千行左右的小项目,也没有什么不可控的风险等等。使用用过多的方式反而会增加项目整体的成本,得不偿失。
  2. 接口隔离
    接口隔离又分为:
    1)抽象接口隔离
    这种方式为大多数的开发者和设计者广泛使用,即抽象一个接口基类,供外部应用。但继承和多态的引入,导致了编译和运行开销的增加。同时,抽象的设计本身对设计者来说就是一个较为复杂的问题。
    2)抽象与实现完全隔离
    这种方式虽然有着抽象接口的问题,但由于完全隔离实现,实现了初步的ABI接口的稳定性。如果再引入前向声明,则可以较好解决编译依赖的问题。而在前面分析过的类型擦除,也可以认为是一种接口的抽象。
    接口隔离适合于诸如数据库开发、数据传输以及游戏开发等。可以说这是一种非常广泛的隔离隐藏的实现方式,其它的应用都可以建立在它实现的基础上进行。
c 复制代码
// Graphics.h - 接口类头文件
class Graphics {
public:
    virtual ~Graphics() = default;
    virtual void Draw(const std::vector<Point>& vp) = 0;
    virtual void resize(int width, int height) = 0;
    
protected:
    Graphics() = default;
};

// Rectangle.h - 实现头文件
class Rectangle final : public Graphics {
public:
    Rectangle();
    ~Rectangle() override;
    
    void Draw(const std::vector<Point>& vp) override;
    void resize(int width, int height) override;
    
private:
    // 使用Pimpl调用第三方库实现
    struct MySelfImpl;
    std::unique_ptr<MySelfImpl> impl_;
};
  1. 设计原则和设计模式隔离
    1)工厂和单例模式隔离
    通过单例、工厂模式,来进行技术实现细节的封装,并对外只提供相关的实现后的接口。同时在编译时也会尽量的减少重编译的单元。但在多线程应用及单例实现上需要注意一些具体的问题,如饱汉饿汉模式,并发问题以及智能指针的问题等等。
c 复制代码
// JsonManager.h
class JsonManager {
public:
    static JsonManager& getInstance();
    std::string toJSON(const std::string& key,const std::string& str) ;
    void getJSON(const std::string& key, std::string& ret)const;
    
    JsonManager(const JsonManager&) = delete;
    JsonManager& operator=(const JsonManager&) = delete;
    //三/五法则,移动相关函数=delete,略
private:
// 私有构造函数,单实例控制,但C++11后推荐使用静态局部变量
    JsonManager();  
    //私有化析构函数,用来禁止在栈上创建对象并只可以显示控制析构(参看前面的私有化析构函数的文章)
    ~JsonManager(); 
    
    //隔离隐藏细节的类实现:Impl类由其它相关实现
    class DoWithImpl;
    std::unique_ptr<DoWithImpl> pImpl_;
    
    // 单实例
    static std::unique_ptr<JsonManager> instance_;
};
//实现,略

2)桥接和策略模式隔离

这种方式可以通过接口的依赖注入,动态的实现接口与相关实现的完全隔离。与接口抽象隔离有着交叉的部分。这种方式的问题在于如果引入了模板编程,则可能导致扩展后的代码膨胀问题。

使用这种隔离方式,基本上就和相关的设计模式和原则绑定在了一起,其应用场景也与相关模式雷同。在中大型的软件开发中,设计模式的引入才更具有实用价值。

c 复制代码
// MsgBridging.h 
class MsgBridging {
public:
    virtual ~MsgBridging() = default;
    virtual void transfer(const std::string& msg) = 0;
    
protected:
    MsgBridging() = default;
};

// MsgImpl.h 
class MsgImpl {
public:
    virtual ~MsgImpl() = default;
    virtual void notify(const std::string& msg) = 0;
    
protected:
    MsgImpl() = default;
};

// 桥接类
class CompressMsgBridging : public MsgBridging {
public:
    explicit CompressMsgBridging(std::unique_ptr<MsgImpl> impl)
        : msgImpl_(std::move(impl)) {}
    
    void transfer(const std::string& msg) override {
        std::string cMsg = compress(msg);
        msgImpl_->notify(cMsg);
    }
    
private:
    std::unique_ptr<MsgImpl> msgImpl_;
    
    std::string compress(const std::string& msg) {
        //压缩算法,略
        return msg; 
    }
};
  1. pimpl的隔离
    pimpl与前向声明很容易混为一谈,也可以混为一谈。前提是要掌握pimpl的核心是如何应用前向声明。pimpl强调的是通过前向声明,以指针的方式访问实现封装类并暴露指针对外的接口,实现接口与实现的解耦,从而达到减少编译依赖,提高ABI的兼容性。
    pimpl的实现有很多种方法,可以多重隔离也可以简单隔离,这就看设计者面对的应用环境了。如果需要全面的ABI兼容,并且软件规模非常大,经常需要对外部接口重复编译,则可以多重隔离尽量实现ABI的兼容性。如果只是一种轻量级的应用 ,则可以简单隔离即可。不管哪一种,都会减少头文件的依赖引起的编译污染。
c 复制代码
//对外接口
class Widget {
public:
    Widget();
    ~Widget();
    void run();
    
private:
    // 前向声明
    class Impl;
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp
#include "Widget.h"

// 实现类
class Widget::Impl {
public:
    void startWork() { 
        content_ = "abc";
    }
    
    std::string getContent() const { return content_; }
    
private:
    std::string content_{""};
    std::vector<int> workerID_;
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}

Widget::~Widget() = default; // 必须显式定义(Impl是不完整类型)

void Widget::run() {
    pImpl->startWork();
}

三、对比分析

对于大多数的设计者和开发者来说,使用基础的软件隔离和初步的接口抽象已经能够完成大多数的设计。甚至在不少的项目上,单独使用一种设计都可以满足设计需求。但在一些大型软件设计上,单独使用一种设计方法往往无法达到ABI兼容、编译期控制以及对测试的较好支持等特定的需求。

使用Pimpl可以减少编译依赖即对头文件的依赖,提高ABI的兼容性并隐藏相关的实现细节,从而在某种程度上可以减少跨平台开发的复杂度。但如果在使用智能指针的情况下需要注意析构函数的处理(见前面PIMPL相关的文章分析),避免编译器的非完整性定义错误出现。另外对指针指向的对象的生命周期进行控制防止出现内存泄露。还有一个重要的问题,Pimpl引入的间接访问,有可能会增加开销以及让程序的可读性有所下降。它特别适合于中大规模的项目和库的开发,这可以体现在开源的大项目中,经常可以看到类似的代码。

而如果想使用运行时多态来处理接口抽象,则考虑使用接口隔离的方法。通过接口类的继承通过多态来进行目标的处理。但这种情况下,ABI的兼容性就需要考虑使用前向声明或Pimpl来进行控制。抽象本身就是一个复杂的问题,搞不好就反而是画虎不成,而接口设计也不是简单的拿过来就成,所以抽象接口看似简单,其实需要考虑的东西非常多。既要有接口最小化的原则,又要考虑扩展性和兼容性。万其是面对实现接口的不断的扩展,如何在保证API的稳定性的前提下保证ABI的稳定性,本身就是一个难题。

在抽象接口的基础上,可以考虑引入相关的设计模式和设计原则的隔离隐藏,它们的优势在于这种方式一般经过了反复的实践考验,一般来说不会出现大问题(除非设计者自身用错)。但设计模式只是考虑了代码的重用和模块化的处理,而对并行编程及ABI等完全没有考虑,这就需要开发者在使用这种方式时,除了要考虑对象的生命周期外,也需要考虑并行访问时资源的管理问题。特别是如果在单实例的情况下,使用智能指针的各种特殊情况。

上面提到的几种实现隔离隐藏的方式,并不是说各自都是完全独立互相排斥的。其内部往往可能互相应用互相交融。比如在策略模式中可以使用抽象接口,在抽象接口中可以使用Pimpl等等。更不要提最基础的模块和名空间的隔离了,它是所有隐藏隔离方法的基础。

在实际工程的应用上,要分级分层的控制使用相关的方法,不要一味的强调哪种方法更有优势而忽略其缺点。设计者和开发者可以灵活的组合使用不同的方式来达到隔离隐藏的细节目的。也可以在不同的模块层级上使用不同的实现方式进行隔离设计。如在业务层可以抽象接口为主,在中间层可以使用设计模式,而在底层使用Pimpl。一定要保持一种观念:最合适的选择就是最优的选择!

四、总结

关于动态的发展的眼光去看待和解决问题,才是一个优秀的设计者必备的基础素养。从思想整体上始终不断的保持着灵活多变、实事求是的审时度势的态度,才能够在复杂多变的实际应用场景中以不变应万变,真正的从根源上避免设计上的缺陷。

相关推荐
C++ 老炮儿的技术栈2 小时前
不调用C++/C的字符串库函数,编写函数strcpy
c语言·开发语言·c++·windows·git·postman·visual studio
fyzy2 小时前
C++写后端实现,实现前后端分离
开发语言·c++
CSDN_RTKLIB3 小时前
C++谓词
c++·stl
汉克老师4 小时前
GESP2025年9月认证C++五级真题与解析(单选题9-15)
c++·算法·贪心算法·排序算法·归并排序·gesp5级·gesp五级
飞鹰514 小时前
CUDA高级优化实战:Stream、特殊内存与卷积优化—Week3学习总结
c++·gpt·chatgpt·gpu算力
txinyu的博客6 小时前
std::function
服务器·开发语言·c++
学嵌入式的小杨同学6 小时前
【嵌入式 C 语言实战】交互式栈管理系统:从功能实现到用户交互全解析
c语言·开发语言·arm开发·数据结构·c++·算法·链表
txinyu的博客6 小时前
static_cast、const_cast、dynamic_cast、reinterpret_cast
linux·c++
“αβ”7 小时前
TCP相关实验
运维·服务器·网络·c++·网络协议·tcp/ip·udp