我知道 SOLID 原则是为面向对象的语言编写的。
我在书中找到:罗伯特·马丁(Robert Martin)的“嵌入式C的测试驱动开发”,本书最后一章中的以下句子:
“应用开闭原则和 Liskov 替换原则使设计更加灵活。”
由于这是一本 C 的书(没有 c++ 或 c#),所以应该有一种方法来实现这些原则。
在 C 中实现这一原则是否存在任何标准方法?
我知道 SOLID 原则是为面向对象的语言编写的。
我在书中找到:罗伯特·马丁(Robert Martin)的“嵌入式C的测试驱动开发”,本书最后一章中的以下句子:
“应用开闭原则和 Liskov 替换原则使设计更加灵活。”
由于这是一本 C 的书(没有 c++ 或 c#),所以应该有一种方法来实现这些原则。
在 C 中实现这一原则是否存在任何标准方法?
开闭原则指出,一个系统应该被设计成对扩展开放,同时保持对修改的封闭,或者它可以在不修改的情况下使用和扩展。Dennis 提到的 I/O 子系统是一个相当常见的示例:在可重用系统中,用户应该能够指定如何读取和写入数据,而不是假设数据只能写入文件。
实现这一点的方式取决于您的需要:您可以允许用户传入一个打开的文件描述符或句柄,这已经启用了除了文件之外的套接字或管道的使用。或者您可以允许用户传入指向应该用于读取和写入的函数的指针:这样,除了操作系统允许的内容之外,您的系统还可以用于加密或压缩数据流。
Liskov 替换原则指出,应该总是可以用子类型替换类型。在 C 语言中,您通常没有子类型,但您可以在模块级别应用该原则:代码应该设计成使用模块的扩展版本,如更新版本,不应该破坏它。模块的扩展版本可能使用 astruct
比原始版本具有更多字段,在 an 中具有更多字段enum
以及类似的东西,因此您的代码不应假定传入的结构具有一定的大小,或者枚举值具有某个最大值。
这方面的一个例子是如何在 BSD 套接字 API 中实现套接字地址:有一个“抽象”套接字类型struct sockaddr
可以代表任何套接字地址类型,以及每个实现的具体套接字类型,例如struct sockaddr_un
Unix 域套接字和struct sockaddr_in
IP插座。处理套接字地址的函数必须传递一个指向数据的指针和具体地址类型的大小。
首先,它有助于思考为什么我们有这些设计原则。为什么遵循 SOLID 原则会使软件变得更好?努力了解每个原则的目标,而不仅仅是将它们与特定语言一起使用所需的具体实现细节。
请注意每个原则如何推动系统特定属性的改进,无论是更高的内聚性、更松散的耦合还是模块化。
请记住,您的目标是生产高质量的软件。质量由许多不同的属性组成,包括正确性、效率、可维护性、可理解性等。遵循 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 %d\n", x );
}
void barney(int x)
{
printf( "barney %d\n", 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() 都应该没有隐藏的依赖关系,或者如果它们有,它们也必须兼容。在这个简单的例子中,两个函数都只依赖于标准输出,所以这没什么大不了的。这个想法是在两种情况下都可以保持正确的行为,其中任一功能都可以互换使用。
我能想到的最接近的事情(而且它并不完美,所以如果有人有更好的想法,欢迎他们与我合作)主要是在我为某种库编写函数时.
对于 Liskov 替换,如果您有一个定义了许多函数的头文件,那么您不希望该库的功能依赖于您对函数的实现;您应该能够使用任何合理的实现并期望您的程序完成它的工作。
至于 Open/Closed 原则,如果你想实现一个 I/O 库,你希望拥有做最少的函数(比如read
and write
)。同时,您可能希望使用它们来开发更复杂的 I/O 函数(如scanf
和printf
),但您不会修改只做最低限度的代码。
我看到这个问题已经有一段时间了,但我认为它值得一些新的发现。
SOLID 的五项原则是指软件实体的五个方面,如SOLID 图所示。虽然这是一个类图,但它基本上可以服务于其他类型的 SW 身份。为调用者公开的接口(左箭头,代表接口隔离)和作为被调用者请求的接口(右箭头,代表依赖反转)也可以是经典的 C 函数和参数接口。
顶部箭头(扩展箭头,代表 Liskov 替换原则)适用于类似实体的任何其他实现。例如,如果您有一个用于链接列表的 API,您可以更改其功能的实现,甚至可以更改向量“对象”的结构(例如,假设它保留了原始对象的结构,就像在 BSD 中一样)套接字示例,或者它是不透明类型)。当然,这不像 OOP 语言中的 Object 那样优雅,但它遵循相同的原则,并且可以使用,例如,使用动态链接。
以类似的方式,底部箭头(泛化箭头,代表打开/关闭原则)定义了您的实体定义的 whet 以及打开的内容。例如,某些功能可能在一个文件中定义并且不应被替换,而其他功能可能会调用另一组 API,从而允许使用不同的实现。
这样,您也可以使用 C 编写 SOLID SW,尽管它可能会使用更高级别的实体来完成,并且可能需要更多的工程。