热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

C++中的依赖性反转(来自SOLID原则)

如何解决《C++中的依赖性反转(来自SOLID原则)》经验,为你挑选了1个好方法。

在阅读并观察了很多关于SOLID原理之后,我非常希望在我的工作中使用这些原则(主要是C++开发),因为我认为它们是很好的原则,并且它们确实会为我的代码质量,可读性,可测试性带来很多好处. ,重用和可维护性.但我真的很难用'D'(依赖倒置).该主要说明:
A.高级模块不应该依赖于低级模块.两者都应该取决于抽象.
B.抽象不应该依赖于细节.细节应取决于抽象.

让我举例说明:
让我说我写的是以下界面:

    class SOLIDInterface {
      //usual stuff with constructor, destructor, don't copy etc
      public:
      virtual void setSomeString(const std::string &someString) = 0;
    };

(为了简单起见,请忽略"正确接口"所需的其他内容,例如非虚拟公共,私有虚拟等,这不是问题的一部分.)

注意,setSomeString()采用std :: string.
但是这打破了上面的原则,因为std :: string是一个实现.
Java和C#没有这个问题,因为该语言提供了所有复杂常见类型(如字符串和容器)的接口.
C++不提供这一点.
现在,C++提供了编写这个接口的可能性,我可以编写一个'IString'接口,它将采用任何支持使用类型擦除的std :: string接口的实现
(非常好的文章:http:// www.artima.com/cppsource/type_erasure.html)

所以实现可以使用STL(std :: string)或Qt(QString),或者我自己的字符串实现或其他东西.
喜欢它应该.

但这意味着,如果我(不仅是我而是所有C++开发人员)想要编写遵循SOLID设计原则(包括'D')的C++ API,我将不得不实现大量代码以适应所有常见的非自然类型.
除了在努力方面不现实之外,这个解决方案还有其他一些问题,例如 - 如果STL发生了变化?(对于这个例子)
并且它不是真正的解决方案,因为STL没有实现IString,而IString正在抽象STL,所以即使我是创建这样一个接口,主要问题仍然存在.
(我甚至没有遇到这样的问题,这会增加多态开销,对某些系统而言,取决于大小和硬件要求可能是不可接受的)

所以可能会问:
我在这里遗漏了什么(我猜这是真正的答案,但是什么?),是否有一种方法可以在C++中使用依赖性反转,而无需以实际方式为常见类型编写全新的接口层- 或者我们注定要编写始终依赖于某些实现的API?

谢谢你的时间!

编辑:从我到目前为止收到的前几条评论中我认为需要澄清:std :: string的选择只是一个例子.它可能是QString - 我只是采用STL,因为它是标准.它的字符串类型甚至不重要,它可以是任何常见类型.

EDIT2:我选择Corristo的答案不是因为他明确地回答了我的问题,而是因为广泛的帖子(加上其他答案)让我能够隐含地从中提取答案,意识到讨论倾向于偏离实际问题是:当你使用基本的复杂类型(如字符串和容器)以及任何有意义的STL时,你能用C++实现依赖性反转吗?(最后一部分是问题的一个非常重要的元素).也许我应该明确地指出我在运行时多态性之后没有编译时间.明确的答案是否定的,这是不可能的.如果STL将抽象接口暴露给它们的实现(如果确实存在阻止STL实现从这些接口派生的原因(比如性能)),那么它可能是可能的,那么它仍然可以简单地维护这些抽象接口以匹配实现).

对于我完全可以控制的类型,是的,实现DIP没有技术问题.但很可能任何这样的接口(我自己的)仍将使用字符串或容器,迫使它使用STL实现或其他.以下所有建议的解决方案要么在运行时不是多态的,要么/并且在界面周围强制安静一些编码 - 当你认为你必须为所有这些常见类型执行此操作时,实际情况就不存在了.

如果你认为你知道的更好,并且你说我可以拥有上面描述的内容,那么只需发布证明它的代码即可.我赌你!:-)



1> Corristo..:

请注意,C++不是面向对象的编程语言,而是让程序员在许多不同的范例之间进行选择.C++的一个关键原则是零成本抽象,其中特别需要以这样的方式构建抽象,即用户不为他们不使用的东西付费.

然后,使用派生类实现的虚拟方法定义接口的C#/ Java风格不属于该类别,因为即使您不需要多态行为,也要std::string实现虚拟接口,每次调用其中一个它的方法会产生vtable查找.对于应该在各种设置中使用的C++标准库中的类,这是不可接受的.

定义接口而不继承抽象接口类

C#/ Java方法的另一个问题是,在大多数情况下,您实际上并不关心从某个特定抽象接口类继承的东西,只需要传递给函数的类型支持您使用的操作.将接受的参数限制为从特定接口类继承的参数实际上阻碍了现有组件的重用,并且您经常最终编写包装器以使一个库的类符合另一个库的接口 - 即使它们已经具有完全相同的成员函数.

连同基于继承的多态通常还需要堆分配和引用语义以及与生命周期管理有关的所有问题这一事实,最好避免从C++中的抽象接口类继承.

隐式接口的通用模板

在C++中,您可以通过模板获得编译时多态性.在最简单的形式中,模板化函数或类中使用的对象需要符合的接口实际上并未在C++代码中指定,而是由在其上调用的函数所暗示.

这是STL中使用的方法,它非常灵活.以std::vector为例.T您存储在其中的对象的值类型的要求取决于您对向量执行的操作.这允许例如存储仅移动类型,只要您不使用任何需要复制的操作即可.在这种情况下,定义值类型需要符合的接口会大大降低其有用性std::vector,因为您需要删除需要复制的方法,或者您需要排除仅存储移动类型它.

但这并不意味着您不能使用依赖项反转:使用模板实现的依赖项反转的常见Button-Lamp示例如下所示:

class Lamp {
public:
    void activate();
    void deactivate();
};

template 
class Button {
    Button(T& switchable)
        : _switchable(&switchable) {
    }

    void toggle() {
        if (_buttonIsInOnPosition) {
            _switchable->deactivate();
            _buttOnIsInOnPosition= false;
        } else {
            _switchable->activate();
            _buttOnIsInOnPosition= true;
        }      
    }

private:
   bool _buttonIsInOnPosition{false};
   T* _switchable;  
}

int main() {
   Lamp l;
   Button b(l)

   b.toggle();
}

这里Button::toggle隐含地依赖于一个Switchable接口,需要T具有成员函数T::activateT::deactivate.由于Lamp恰好实现了该接口,因此可以与Button该类一起使用.当然,在实际代码中,您还要TButton类的文档中说明这些要求,以便用户不需要查找实现.

同样,您也可以将setSomeString方法声明为

template 
void setSomeString(String const& string);

然后这将适用于实现您在实现中使用的所有方法的所有类型setSomeString,因此仅依赖于抽象 - 尽管是隐式 - 接口.

与往常一样,需要考虑一些缺点:

在字符串示例中,假设您只使用.begin().end()成员函数返回迭代器,这些迭代器返回一个char取消引用的时间(例如,将其复制到类的本地,具体的字符串数据成员),您也可以意外地传递std::vector给它,即使它在技​​术上不是一个字符串.如果你认为这个问题是有争议的,那么在某种程度上,这也可以被视为仅仅依赖于抽象的缩影.

如果传递的类型的对象没有所需的(成员)函数,那么最终可能会出现可怕的编译器错误消息,这使得很难找到错误的来源.

仅在非常有限的情况下,可以将模板化类或函数的接口与其实现分开,这通常使用单独的文件.h.cpp文件来完成.因此,这可能导致更长的编译时间.

使用Concepts TS定义接口

如果您真的关心模板化函数和类中使用的类型以符合固定接口,无论您实际使用什么,都有办法将模板参数仅限制为符合某个接口的类型std::enable_if,但这些不是很可读且非常冗长.为了使这种通用编程更容易,Concepts TS允许实际定义由编译器检查的接口,从而大大改进了诊断.使用Concepts TS,上面的Button-Lamp示例转换为

template 
concept bool Switchable = requires(T t) {
    t.activate();
    t.deactivate();
};

// Lamp as before

template 
class Button {
public:
    Button(T&);    // implementation as before
    void toggle(); // implementation as before
private:
    T* _switchable;
    bool _buttonIsInOnPosition{false};
};

如果你不能使用Concepts TS(它现在只在GCC中实现),你可以得到的最接近的是Boost.ConceptCheck库.

为运行时多态性键入擦除

有一种情况是编译时多态性不够,那就是在传递给特定函数或从特定函数获取的类型在编译时没有完全确定但依赖于运行时参数(例如来自配置文件,传递给可执行文件的命令行参数,甚至传递给函数本身的参数值.

如果需要存储依赖于运行时参数的类型的对象(甚至是变量),传统方法是将指针存储到公共基类,并通过虚拟成员函数使用动态调度来获取所需的行为.但是这仍然存在以前描述的问题:您不能使用有效地执行您需要但在外部库中定义的类型,因此不会从您定义的基类继承.所以你必须编写一个包装类.

或者您执行您在问题中描述的内容并创建类型擦除类.标准库的一个例子是std::function.您只声明函数的接口,它可以存储具有该接口的任意函数指针和可调用对象.一般来说,编写类型擦除类可能非常繁琐,所以我不在Switchable这里举一个类型擦除的例子,但我强烈推荐Sean Parent的讲话继承是邪恶的基类,在那里他演示了"可绘制的"对象,并在20分钟内探索您可以在其上构建的内容.

有些库可以帮助编写类型擦除类,例如Louis Dionne的实验性dyno,你可以通过他在C++代码中直接用"概念图"来定义接口,或者Zach Laine的emtypen使用python工具创建类型擦除您提供的C++头文件中的类.后者还附带了一个CppCon演讲,描述了这些特性以及一般的想法以及如何使用它.

结论

继承公共基类只是为了定义接口,虽然简单,但却导致许多问题可以使用不同的方法来避免:

(约束)模板允许编译时多态,这对于大多数情况是足够的,但是当与不符合接口的类型一起使用时,可能导致难以理解的编译器错误.

如果你需要运行时多态性(实际上我的经验实际上很少见),你可以使用typ-erasure类.

因此,即使STL和其他C++库中的类很少派生自抽象接口,如果您真的想要,仍然可以使用上述两种方法之一应用依赖项反转.

但是,一如既往,根据具体情况使用良好的判断,无论您是否真的需要抽象,或者只是简单地使用具体类型.您提出的字符串示例是我将使用具体类型的示例,因为不同的字符串类不共享公共接口(例如,std::string.find(),但QString调用相同函数的s版本.contains()).编写转换函数并在项目中明确定义的边界使用它时,为两者编写包装类可能同样费力.


推荐阅读
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 如何在跨函数中使用内存?
    本文介绍了在跨函数中使用内存的方法,包括使用指针变量、动态分配内存和静态分配内存的区别。通过示例代码说明了如何正确地在不同函数中使用内存,并提醒程序员在使用动态分配内存时要手动释放内存,以防止内存泄漏。 ... [详细]
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
author-avatar
土土不怕苦_402
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有