C 的 SOLID 原则实现

SOLID principles implementation for C

本文关键字:实现 原则 SOLID      更新时间:2023-10-16

我知道SOLID原则是为面向对象语言编写的。

我在书中找到了Robert Martin的"嵌入式C的测试驱动开发",在本书的最后一章中有这样一句话:

"应用开闭原理和利斯科夫替代原理可以做出更灵活的设计。

由于这是一本C(没有C ++或C#(的书,因此应该有一种方法来实现此原则。

在 C 语言中实现此原则有什么标准方法吗?

开闭原则指出,系统的设计应使其可以扩展,同时保持其封闭性以进行修改,或者可以在不修改的情况下使用和扩展。正如Dennis所提到的,I/O子系统是一个相当常见的例子:在可重用的系统中,用户应该能够指定数据的读取和写入方式,而不是假设数据只能写入文件。

实现此目的的方法取决于您的需求:您可以允许用户传入打开的文件描述符或句柄,这已经允许使用套接字或管道以及文件。或者,您可以允许用户传入指向应该用于读取和写入的函数的指针:这样,除了操作系统允许的内容外,您的系统还可以与加密或压缩的数据流一起使用。

Liskov 替换原则指出,应该始终可以用子类型替换类型。在 C 中,您通常没有子类型,但您可以在模块级别应用该原则:代码应该设计为使用模块的扩展版本(如较新版本(不应破坏它。模块的扩展版本可能使用比原始字段更多的structenum中更多的字段以及类似的东西,因此您的代码不应假定传入的结构具有特定大小,或者枚举值具有特定的最大值。

这方面的一个例子是套接字地址在BSD套接字API中的实现方式:有一个"抽象"套接字类型struct sockaddr可以代表任何套接字地址类型,以及每个实现的具体套接字类型,例如Unix域套接字的struct sockaddr_un和IP套接字的struct sockaddr_in。处理套接字地址的函数必须传递指向数据和具体地址类型大小的指针

首先,思考为什么我们有这些设计原则会有所帮助。 为什么遵循 SOLID 原则会使软件变得更好?努力理解每个原则的目标,而不仅仅是将它们与特定语言一起使用所需的特定实现细节。

  • 单一责任原则通过增加内聚力;更好的模块化导致更好的可测试性,可用性和可重用性。
  • 开放/封闭原则支持异步部署将实现彼此分离。
  • Liskov替代原则通过以下方式促进模块的模块化和重用确保其接口的兼容性。
  • 接口隔离原则减少了界面的不相关消费者,同时提高可读性和可理解性。
  • 依赖反演原则减少了耦合,并且它强烈启用可测试性。

注意每个原则如何推动系统某个属性的改进,无论是更高的内聚力、更松散的耦合还是模块化。

请记住,您的目标是生产高质量的软件。 质量由许多不同的属性组成,包括正确性、效率、可维护性、可理解性等。 遵循这些原则时,SOLID 原则可帮助您实现目标。 因此,一旦你掌握了原则的"为什么",实现的"方式"就会变得容易得多。

编辑:

我会尝试更直接地回答你的问题。

对于打开/关闭原则,规则是旧接口的签名和行为必须在任何更改之前和之后保持不变。 不要中断任何调用它的代码。 这意味着它绝对需要一个新的接口来实现新的东西,因为旧的东西已经有行为了。 新接口必须具有不同的签名,因为它提供了新的和不同的功能。 因此,您在 C 中满足这些要求的方式与在 C++ 中一样。

假设您有一个函数int foo(int a, int b, int c)并且想要添加一个几乎完全相同的版本,但它需要第四个参数,如下所示:int foo(int a, int b, int c, int d) 。 要求新版本向后兼容旧版本,并且新参数的某些默认值(例如零(会发生这种情况。您将实现代码从旧的foo移动到新的foo中,在旧的foo中,您将执行以下操作: int foo(int a, int b, int c) { return foo(a, b, c, 0);} 因此,即使我们从根本上改变了int foo(int a, int b, int c)的内容,我们保留了它的功能。 它仍然对改变关闭。

Liskov替代原则指出,不同的亚型必须兼容工作。 换句话说,具有共同签名的事物可以相互替代,在理性上必须表现相同。

在 C 语言中,这可以通过指向采用相同参数集的函数的函数指针来实现。 假设您有以下代码:

#include <stdio.h>
void fred(int x)
{
    printf( "fred %dn", x );
}
void barney(int x)
{
    printf( "barney %dn", x );
}
#define Wilma 0
#define Betty 1
int main()
{
    void (*flintstone)(int);
    int wife = Betty;
    switch(wife)
    {
    case Wilma:
        flintstone = &fred;
    case Betty:
        flintstone = &barney;
    }
    (*flintstone)(42);
    return 0;
}

当然,fred(( 和 barney(( 必须有兼容的参数列表才能正常工作,但这与子类从它们的超类继承它们的 vtable 没有什么不同。 行为契约的一部分是 fred(( 和 barney(( 都应该没有隐藏的依赖关系,或者如果它们有,它们也必须兼容。 在这个简单的示例中,这两个函数都只依赖于 stdout,所以这没什么大不了的。 这个想法是,在这两种情况下,您都可以保留正确的行为,其中任何一个函数都可以互换使用。

我能想到的最接近的事情(它并不完美,所以如果有人有更好的想法,欢迎他们来找我(主要是当我为某种库编写函数时。

对于 Liskov 替换,如果你有一个定义许多函数的头文件,你不希望该库的功能取决于你拥有函数实现;你应该能够使用任何合理的实现,并期望你的程序做它的事情。

至于开放/关闭原则,如果你想实现一个 I/O 库,你需要有最低限度的函数(如readwrite(。同时,您可能希望使用这些来开发更复杂的I/O功能(如scanfprintf(,但您不会修改最低限度的代码。

我看到这个问题被打开已经有一段时间了,但我认为值得一些更新的目光。

五个 SOLID 原则是指软件实体的五个方面,如 SOLID 图所示。虽然这是一个类图,但它基本上可以为其他类型的软件标识提供服务。为调用方公开的接口(左箭头代表接口隔离(和作为被调用方请求的接口(右箭头,代表依赖关系反转(也可以是经典的 C 函数和参数接口。

顶部箭头(扩展箭头,代表 Liskov 替换原则(适用于类似实体的任何其他实现。 例如,如果你有一个链表的API,你可以改变其函数的实现,甚至是向量"对象"的结构(例如,假设它保留了原始结构, 如 BSD 套接字示例,或者它是一个不透明的类型(。当然,这不像OOP语言中的对象那样优雅,但它遵循相同的原理,并且可以使用,例如,使用动态链接。

以类似的方式,底部箭头(泛化箭头,代表打开/关闭原则(,定义由您的实体和打开的内容定义。例如,某些功能可能在一个文件中定义,不应替换,而其他功能可能会调用另一组 API,这允许使用不同的实现。

通过这种方式,您也可以使用 C 编写 SOLID SW,尽管它可能会使用更高级别的实体来完成,并且可能需要更多工程。