1, 介绍
在电子产品开发的时候,难免需求变化,或者后期重构、维护、增加功能。怎么将代码写的可扩展性强,可维护性高?平时会阅读不少内核代码、开源代码。怎么能高效的阅读其中的代码逻辑?这就需要我们了解程序的设计原则。虽然程序的设计原则是为面向对象编程提出的,但是C语言能够实现封装、继承、多态(详见文章:《一文搞懂怎么用C实现封装、继承、多态》),因此C语言程序设计也可以使用这些原则。
为了降低软件模块之间的耦合,提高代码的灵活性、兼容性、可复制性、可维护性和可扩展性,编程大佬们从宏观到微观对各种软件系统、模块进行拆分、抽象、封装,确定某块之间的交互关系,最终归纳、总结,将一些软件模式沉淀下来成为通用的解决思想,这便形成了7大程序设计原则,在设计原则的基础上又进一步实践、归纳、总结出一些套路,这便是23种设计模式(后面会分别以一篇文章介绍这些设计模式)的由来。
这些程序原则可以提升代码的质量和可维护性,但是它们也会增加代码的复杂度。因此在实际的代码开发时,我们要综合考虑人力、质量、复杂度,适当的采用设计原则,从而平衡软件的复杂度及稳定性、可维护性。
这7大程序设计原则的目的是:单一职责让我们的类各司其职,职责单一。里氏替换告诉我们优化继承体系。依赖倒置是面向接口编程,通过构造函数等其它方式注入。接口隔离告诉我们设计接口要单一。迪米特告诉我们要解耦。最后达到我们的开闭原则,遵循扩展开发,修改关闭。
接下来会分别介绍7大程序设计的原则,每个原则会给出定义及C语言举例,从而让大家能够很好的理解每一种原则。
2,单一职责原则(Single Responsibility Principle,SRP)
2.1 单一职责原则定义
一个类(数据结构)、方法(函数)或模块只负责一项职责,也就是说这个类、方法或模块只有一个引起它被修改的原因。我们可以将某个业务功能划分到一个类中,也可以拆分为几个类分别实现,但不管对其负责的业务范围大小做怎样的权衡与调整,这个类的角色职责是单一的,其方法所完成的功能也是单一的。
2.2 怎么用C语言实现单一职责原则
我们可以让每个函数、每个结构体,只有一个职责,从而实现单一职责原则。
2.2.1 函数功能只负责一件事
使用函数封装功能模块,每个函数只负责一件事情,比如读取文件、计算数据等等。
例如,下面的代码演示了如何将读取文件和计算数据分别封装成两个函数:
void read_file(const char* filename)
{
// 读取文件的代码
}
void calculate_data(float* data, int size)
{
// 计算数据的代码
}
2.2.2 结构体封装的模块只负责一件事
通过结构体的方式,将数据和操作封装在一起,确保每个结构体只负责某一特定的功能任务。
我们要实现一个简单的文本编辑器,包含如下功能:打开文件、读取文件、编辑文件、保存文件等。如果使用单一职责原则,我们可以将这些功能分别封装在不同的结构体中。
定义一个通用文件操作的结构体:
typedef struct {
char* filename;
int (*open)(char* filename);
int (*read)(char* filename, char *context, int size);
int (*write)(char* filename, char *context, int size);
} FileHandler;
接下来,我们可以定义两个子结构体,一个用于处理纯文本文件,一个用于处理二进制文件,这便体现了单一职责原则。
// 处理文本文件的操作结构体
typedef struct {
FileHandler filehandler;
char* content;
int (*edit)(char *content);
} TextHandler;
// 处理二进制文件的操作结构体
typedef struct {
FileHandler filehandler;
char* data;
int (*compress)(char* data);
int (*decompress)(char* data);
} BinaryHandler;
在这两个子结构体中,我们添加了一些特定的文件类型方法。例如,TextHandler中有一个edit方法,可以允许用户编辑文件内容。而BinaryHandler中有compress和decompress方法,可以用于压缩和解压缩文件内容。
这样一来,我们就可以根据文件的类型选择不同的文件处理模块。例如,对于纯文本文件,我们可以使用TextHandler进行操作,对于二进制文件,我们可以使用BinaryHandler进行操作。每个文件处理模块只需要负责处理特定类型文件相应操作,修改的话只对相应处理模块进行即可。
2.3 单一职责原则的优缺点
单一职责原则的核心思想是一个类只有一个引起它变化的原因。这意味着这个类只有一个职责,它应该专注于处理一件事情,并且在改变时只影响到自己,并不影响到他人。
2.3.1 适当使用单一职责原则的优点
- 降低了类的复杂度:使得每个类都有一个明确的责任,代码变得更加简洁和易于理解。提高代码的可读性、灵活性和可维护性。
- 增加代码的安全性:将一个复杂的类拆分成多个职责单一的小类后,这样在维护和修改代码时就更方便,容易找到并修改需要改变的部分,并不影响其他模块。
- 提高代码的可重用性:类的职责单一,其内部逻辑相对独立,高内聚,便于被其他模块进行调用和重用。
2.3.2过度使用单一职责原则的缺点
- 增加代码的数量:因为需要分解一个大类为多个小类。当每个小类功能都比较简单时,类的数量将会显著增加,使得代码量更大。
- 增加代码的复杂度:严格遵循单一职责原则并不总是可能的,有时候职责之间的联系是紧密的,分离职责会使得代码的逻辑更加复杂。
- 增加开发、维护成本:过度分解一个大类为多个小类可能会增加系统的开发和维护的成本,需要仔细权衡利弊。
3,开闭原则(Open Close Principle,OCP)
3.1 开闭原则的定义
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。也就是说,我们应该通过扩展来实现软件功能的增加,而不是通过修改现有代码来实现,这样可以提高软件的稳定性和可复用性。
3.2 怎么用C语言实现开闭原则
在C语言中,我们可以利用函数指针、接口结构体实现开闭原则。无论是利用函数指针还是接口结构体,实现开闭原则的核心思路都是将可变部分抽象出来,从而达到避免修改代码而扩展功能的目的。
3.2.1 利用函数指针实现
函数指针可以实现回调函数的功能,在C语言中常见的使用场景就是回调函数。在应用开闭原则时,我们可以将需要变化的部分封装为函数指针参数,通过传入不同实现的函数指针来实现代码的扩展。这样,即使需要修改代码,也只需要修改函数指针指向的实现,而不用修改主逻辑代码。
举例来说,我们可以定义一个处理消息的函数,并将其实现封装为函数指针:
typedef int (*process_msg)(char *msg);
int send_msg(process_msg msg_handler, char *msg) {
return msg_handler(msg);
}
int msg_handler_1(char *msg) {
// 处理方式1
}
int msg_handler_2(char *msg) {
// 处理方式2
}
int main() {
char *msg = "hello, world!";
send_msg(msg_handler_1, msg); // 调用处理方式1
send_msg(msg_handler_2, msg); // 调用处理方式2
}
在上述代码中,我们定义了一个process_msg类型的函数指针,用于作为处理消息的函数参数。定义了两个不同的消息处理函数,并通过send_msg函数来调用不同的实现,从而实现了开闭原则。
3.2.2 利用接口结构体实现
C语言中没有像C++和Java那样的接口和类的概念,但可以通过结构体来模拟。具体地,在实现中,我们可以定义一个接口结构体,包含一系列指向同一类型函数的函数指针,并通过结构体定义不同的实现。这样,在使用时,我们可以将接口结构体作为参数传入函数,并通过调用不同实现的函数指针来实现代码的扩展,从而避免对主逻辑代码的修改。
举例来说,我们可以定义一个msg_handler接口结构体,并通过msg_handler_1和msg_handler_2结构体来实现不同的消息处理方式:
struct msg_handler {
int (*handler)(char *msg);
};
struct msg_handler_1 {
struct msg_handler base;
};
struct msg_handler_2 {
struct msg_handler base;
};
int msg_handler_1_impl(char *msg) {
// 处理方式1
return 0;
}
int msg_handler_2_impl(char *msg) {
// 处理方式2
return 0;
}
struct msg_handler_1* new_msg_handler_1() {
struct msg_handler_1* obj = malloc(sizeof(struct msg_handler_1));
obj->base.handler = msg_handler_1_impl;
return obj;
}
struct msg_handler_2* new_msg_handler_2() {
struct msg_handler_2* obj = malloc(sizeof(struct msg_handler_2));
obj->base.handler = msg_handler_2_impl;
return obj;
}
int send_msg(struct msg_handler* handler, char *msg) {
return handler->handler(msg);
}
int main() {
char *msg = "hello, world!";
struct msg_handler_1* h1 = new_msg_handler_1();
send_msg((struct msg_handler*)h1, msg); // 调用处理方式1
struct msg_handler_2* h2 = new_msg_handler_2();
send_msg((struct msg_handler*)h2, msg); // 调用处理方式2
return 0;
}
在上述代码中,我们定义了msg_handler接口结构体,其中包含了一个指向处理函数的函数指针。同时,我们通过msg_handler_1和msg_handler_2结构体分别实现了不同的处理方式,并通过new_msg_handler_1()和new_msg_handler_2()函数创建不同的实例。在send_msg函数中,我们传入msg_handler接口结构体作为参数,并通过调用函数指针来实现不同的处理方式。这样,即使需要新增处理方式,我们也只需要定义新的实现结构体并通过接口结构体传入即可,避免了对主逻辑代码的修改。
3.3 开闭原则的优缺点
3.3.1 适当使用开闭原则的优点
- 可扩展性:遵循开闭原则,软件系统可以更容易地添加新的功能模块,而不会对原有模块产生任何影响。
- 可维护性:遵循开闭原则的代码更容易维护。因为修改一个软件设计时需要充分考虑该代码对其他模块的影响,迫使程序员写出更加模块化、可维护的代码。
- 代码复用:开闭原则可以鼓励我们构建一个可重用、通用的代码库,因为新的需求只需要扩展而不需要更改原有代码。
- 提高软件质量:遵循开闭原则可以使得代码更加灵活,不需要修改原有的代码,只需要增加新的代码,有利于提高软件质量。
3.3.2 过度使用开闭原则的缺点
- 增加代码的复杂度:开闭原则可能导致代码过度设计,过多的使用抽象和接口,增加代码复杂度。
- 增加开发时间及成本:在某些情况下,为了扩展一个软件实体,需要大量的代码重构。这在一些情况下是必要的,但在其他情况下可能会增加开发时间和成本。
4,里氏替换原则(Liskov Substutution Principle,LSP,LSP)
4.1 里氏替换原则的定义
简单来说,一个软件系统中所有用到父类的地方都替换成其子类,系统应该仍然可以正常工作。也就是说,在任何使用父类对象的地方,都应该能够使用子类对象来无缝代替,而不会引起任何错误或异常,这样可以保证继承关系的合理性和多态的正确性,其实里氏替换原则就是为了约束继承与多态而生的。
继承优点是减少代码工作量,提高代码重用性,子类继承于父类,但异于父类,提高可扩展性。继承缺点是继承有侵入性,当父类代码修改过后,必须要考虑是否影响到其他子类的职责,缺乏代码规范的情况下,这时候会造成大量的重构改动。
里氏替换原则可以帮助我们克服上面的缺陷,这一原则告诉我们,软件中将一个基类替换成它的子类对象,不会有任何异常,但如果把子类替换成基类,则不成立的,因为子类有很多属于子类自己的东西。这个原则保证了子类可以扩展父类的功能,但不能改变父类原有的功能。
我们在设计的时候一定要充分利用这一原则特性,写框架代码的时候要面向接口编程,而不是深入到具体的子类中,这样才能保证子类多态替换的可能性。
使用需要注意几点:
- 子类所有方法必须在父类中声明,或者子类必须实现父类中声明的所有方法。为了保证程序扩展性,在程序中通常用父类来定义,如果一个方法只存在子类,则肯定不可以用父类来调用。
- 我们在运用时候,尽量把父类设置成接口或者抽象类,当需要扩展的时候,只需要新增集成的子类,不需要修改原有的代码。
4.2 怎么用C语言实现里氏替换原则
4.2.1使用结构体继承
在C语言中,我们可以通过结构体嵌套来实现类的继承。比如,定义一个基类结构体和一个子类结构体,基类结构体中定义一些公共成员变量和成员函数,子类结构体中通过嵌套基类结构体来实现继承,并在其中添加额外的成员变量和成员函数。然后在程序中通过子类替换基类结构体指针来调用这些函数实现里氏替换原则的应用。
以下是一个使用结构体继承实现里氏替换原则的示例:
typedef struct {
/* 父类成员 */
int x;
int y;
} Point;
typedef struct {
/* 子类成员 */
Point point;
int radius;
} Circle;
void draw(Point* p) {
/* 绘制函数 */
}
int main() {
Circle circle;
// 父类指针指向子类对象
Point* p = (Point *)circle;
draw(p);
return 0;
}
4.2.2 使用函数指针
虚函数是一种C++中常用的实现里氏替换原则的方法。在C语言中也可以通过函数指针来模拟虚函数的实现。我们可以定义一个父类结构体,其中包含一些函数指针,子类可以根据自己的需求指向不同的函数,从而实现不同的行为,这也是一种里氏替换原则的应用。
以下是一个使用函数指针实现里氏替换原则应用的示例:
typedef struct {
/* 父类成员 */
void (*draw)(void* self);
int x;
int y;
} Shape;
typedef struct {
/* 子类成员 */
Shape base;
int radius;
} Circle;
void drawCircle(void* self) {
Circle* circle = (Circle*) self;
/* 绘制圆形 */
}
int main() {
Circle circle;
circle.base.draw = drawCircle;
circle.base.x = 0;
circle.base.y = 0;
circle.radius = 10;
// 调用函数指针
circle.base.draw(&circle);
return 0;
}
在这个例子中,我们定义了一个父类 Shape,其中包含一个函数指针 draw。子类 Circle可以根据需要重写draw函数,并将其指向不同的函数。在main函数中,我们创建了一个 Circle对象,并指定它的draw函数为drawCircle这样就可以调用函数指针并绘制圆形。
4.3 里氏替换原则的优缺点
4.3.1 适当使用里氏替换原则的优点
- 更高的代码重用性:因为使用了里氏替换原则,我们可以在不修改已经存在的代码的情况下,利用继承机制,一定程度上实现代码的重用。
- 更好的代码拓展性:在原有的代码上新增子类,其实现方式是“增量式”的,因为子类不需再实现原来已经在父类中实现的方法或属性。
- 更好的代码的读性和可维护性:在应用了里氏替换原则之后,不会出现很多复杂的条件分支语句,可读性和可维护性更好。
4.3.2 过度使用里氏替换原则的缺点
- 可能会增加类型的层级:如果过于追求继承和多态,特别是在类的数量过多而且没有很好的抽象,那么可能会造成类型的层级过于复杂,使得代码难以管理、理解和维护。
- 可能会增加开发成本:如果过度追求里氏替换原则,可能会使类的设计变得过度复杂,增加了系统的开发成本。
5,依赖倒置原则(Dependence inversion principle,DIP)
5.1 依赖倒置原则的定义
高层模块不应该依赖低层模块,二者都应该依赖于抽象。进一步说,抽象不应该依赖于细节,细节应该依赖于抽象。依赖倒转原则的核心思想就是面向接口编程。
依赖倒置原则也可以理解为“依赖抽象原则”。为什么说依赖抽象就是依赖倒置呢?因为在日常生活中,人们习惯于依赖于具体事务(细节),而不是抽象。比如说我们穿鞋子就是穿具体的鞋子,看书就是看具体的书。那么如果要倒过来去依赖抽象,就是依赖倒置。
依赖倒置原则是我们在程序代码传递参数关联时,尽量引用高层次的接口及抽象类,为了确保这一原则,所以具体类应该只实现接口或者抽象类存在的方法,否则都通过接口来调用子类新增的方法。
引用接口和抽象类,系统更具有灵活性,这样一来,系统发生变化,在抽象类或者接口进行扩展。
5.2 怎么用C语言实现依赖倒置原则
5.2.1 使用函数指针实现
// 定义接口函数指针类型
typedef void (*do_work_func_t)(void);
// 底层具体实现函数
void do_work_impl(void) {
printf("do some work\n");
}
// 高层模块
void high_level_module(do_work_func_t func) {
// 执行具体实现函数
(*func)();
}
int main() {
// 高层模块
high_level_module(do_work_impl);
return 0;
}
5.2.2 使用接口结构体实现
// 定义接口结构体类型
typedef struct {
void (*do_work)(void);
} worker_t;
// 底层具体实现函数1
void do_work_impl1(void) {
printf("do some work 1\n");
}
// 底层具体实现函数2
void do_work_impl2(void) {
printf("do some work 2\n");
}
// 高层模块
void high_level_module(worker_t* worker) {
// 执行具体实现函数
worker->do_work();
}
int main() {
// 底层模块1
worker_t worker1 = {do_work_impl1};
// 高层模块
high_level_module(&worker1);
// 底层模块2
worker_t worker2 = {do_work_impl2};
// 高层模块
high_level_module(&worker2);
return 0;
}
这些例子中,高层模块都不直接依赖于底层模块,而是通过函数指针或接口结构体来实现调用。这样可以实现松耦合,方便后期维护和修改,在程序扩展时也更加容易。
5.3 依赖倒置原则的优缺点
5.3.1 适当应用依赖倒置原则的优点
- 降低了模块之间的耦合度:提高了系统的灵活性和可维护性;
- 提高了代码的可读性:通过面向抽象编程,让代码更加易于理解;
- 降低了开发的难度:提高了开发的效率;
5.3.2 过度使用依赖倒置原则的缺点
- 可能会增加代码的复杂度和难度:需要在设计和实现的过程中进行充分考虑和把握;
- 影响代码开发效率:需要合理规划抽象接口的设计,过于精细的接口设计可能会影响代码的效率和开发效率;
6,接口隔离原则(interface segregation principle,ISP)
6.1 接口隔离原则的定义
使用多个专门的接口,不使用一个大而全、庞杂的总接口,用户不应该依赖那些不需要的接口。
当一个接口太大的时候,我们需要将它分割成一些更细小的接口,用户仅仅需要知道相关的方法即可。每个接口承担独立的角色。这里的接口有两层定义,一种是类具有的方法和特征,逻辑上的接口隔离。另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,比如Java语言中的interface。
6.2 怎么用C语言实现接口隔离原则
接口隔离原则表明接口应该尽可能小而精简,它指导我们设计可维护和可复用的系统。在C语言中实现接口隔离原则可以采用以下三种方式:
6.2.1 使用函数指针实现
使用函数指针可以实现接口隔离,通过函数指针,可以将接口定义成一个函数指针类型,并且只提供必要的接口函数,例如:
//定义功能接口
typedef struct {
void (*feature1)(void);
void (*feature2)(void);
} Interface;
// 实现函数1
void implement1(void) {
printf("Function 1 implemented \n");
}
// 实现函数2
void implement2(void) {
printf("Function 2 implemented \n");
}
// 定义接口对象
Interface interface = {
.feature1 = &implement1,
.feature2 = &implement2
};
// 用户代码
void user_code(void) {
// 使用interface对象中定义好的功能接口
interface.feature1();
interface.feature2();
}
6.2.2 使用抽象类继承实现
在C语言中,我们可以使用结构体和函数指针模拟抽象类和虚函数表的实现,例如:
// 抽象类:定义功能接口
typedef struct {
void (*feature1)(void);
void (*feature2)(void);
} AbsClass;
// 子类:实现功能接口
typedef struct {
AbsClass parent;
void (*feature3)(void);
} ChildClass;
// 实现函数1
void implement1(void) {
printf("Function 1 implemented \n");
}
// 实现函数2
void implement2(void) {
printf("Function 2 implemented \n");
}
// 实现函数3
void implement3(void) {
printf("Function 3 implemented \n");
}
// 定义子类对象
ChildClass child = {
.parent = {
.feature1 = &implement1,
.feature2 = &implement2
},
.feature3 = &implement3
};
// 用户代码
void user_code(void) {
// 使用child对象中定义好的功能接口
AbsClass * abs = (AbsClass*)&child; // 指针转换
abs->feature1();
abs->feature2();
ChildClass * chd = (ChildClass*)abs; // 指针转换
chd->feature3();
}
6.2.3 使用函数库实现
使用函数库可以将实现细节隐藏起来,只暴露必要的接口函数,例如:
// 定义功能接口
void feature1(void);
void feature2(void);
// 实现功能函数1
void implement1(void) {
printf("Function 1 implemented \n");
}
// 实现功能函数2
void implement2(void) {
printf("Function 2 implemented \n");
}
// 用户代码
void user_code(void) {
// 调用库函数中定义好的功能接口
feature1();
feature2();
}
6.3 接口隔离原则的优缺点
6.3.1 适当使用接口隔离原则的优点
- 降低用户和实现类的耦合度:将大接口拆分成多个小接口,可以让客户端只依赖它需要的接口。
- 易于修改和维护:当系统需要修改一个接口时,只需要修改这个接口的实现类和它的用户,而不需要修改其它不需要的接口。
- 接口隔离原则有助于优化代码结构:使系统更加灵活,易于扩展和重构。
6.3.2 过度使用接口隔离原则的缺点
- 接口数量过多,会增加系统的复杂度:因为对于一个接口,除了它的实现类,还需要有对应的用户代码。
- 可能会影响系统的扩展和维护。
7,迪米特法则(law of demeter lod,LOD)
7.1 迪米特法则的定义
一个软件实体尽可能少的与其他实体发生相互作用。它规定了一个对象应当对其他对象尽可能少的了解,也就是一个对象不应该知道太多关于其他对象的信息。该原则可以帮助我们减少系统中各个对象之间的耦合度,改变则不会影响其他对象,从而使系统更加易于维护和扩展。
迪米特法则也叫最小知道原则,比如项目中每个功能尽可能的不要相互依赖,而是去使用某个功能的时候,才去调用用到它。
7.2 怎么用C语言实现迪米特法则
在C语言中,实现迪米特法则的几种方式包括:
7.2.1 使用结构体来封装数据和操作
typedef struct {
int data;
} Data;
typedef struct {
void (*setData)(Data* data, int value);
int (*getData)(Data* data);
} IData;
typedef struct {
IData* idata;
} Container;
void setData(Data* data, int value) {
data->data = value;
}
int getData(Data* data) {
return data->data;
}
void init(Container* container) {
container->idata->setData = setData;
container->idata->getData = getData;
}
int main() {
Data data;
Container container;
container.idata = (IData*)malloc(sizeof(IData));
init(&container);
container.idata->setData(&data, 42);
printf("%d\n", container.idata->getData(&data));
free(container.idata);
return 0;
}
7.2.2 使用抽象类来定义接口,然后派生出具体类
typedef struct {
int data;
} Data;
typedef struct _IContainer {
void (*setData)(struct _IContainer* container, Data* data, int value);
int (*getData)(struct _IContainer* container, Data* data);
} IContainer;
typedef struct {
IContainer* icontainer;
Data data;
} Container;
void Container_setData(IContainer* icontainer, Data* data, int value) {
data->data = value;
}
int Container_getData(IContainer* icontainer, Data* data) {
return data->data;
}
IContainer containerInterface = { Container_setData, Container_getData };
void Container_init(Container* container) {
container->icontainer = &containerInterface;
}
int main() {
Container container;
Container_init(&container);
container.icontainer->setData(container.icontainer, &container.data, 42);
printf("%d\n", container.icontainer->getData(container.icontainer, &container.data));
return 0;
}
这里我们定义了一个抽象的IContainer接口,它有两个方法setData和getData,然后我们派生出具体类Container,并实现setData和getData方法。
7.2.3 使用函数指针来实现
typedef struct {
int data;
} Data;
typedef struct {
void (*setData)(int value, void* context);
int (*getData)(void* context);
} IData;
void setData(int value, void* context) {
Data* data = (Data*)context;
data->data = value;
}
int getData(void* context) {
Data* data = (Data*)context;
return data->data;
}
void init(IData* idata, void* context) {
idata->setData = setData;
idata->getData = getData;
idata->context = context;
}
int main() {
Data data;
IData idata;
init(&idata, &data);
idata.setData(42, &data);
printf("%d\n", idata.getData(&data));
return 0;
}
这里我们定义了一个IData接口,它有两个方法setData和getData,以及一个context指针,用于保存需要操作的数据。然后我们定义了setData和getData方法,它们的第二个参数都是void指针,可以接收任意类型的数据。最后我们定义了一个init函数,用于初始化IData和context,从而可以将context和具体实现绑定在一起。在程序中,我们先定义一个Data对象,然后初始化IData,并将其和Data对象绑定在一起,最后调用setData和getData方法操作数据。
7.3 迪米特法则的优缺点
7.3.1 适当使用迪米特法则的优点
- 降低类之间的耦合度:减少模块间相互影响的风险,从而提高系统的稳定性和可维护性;
- 更好地保护了数据的隐私性:因为一个对象只与直接的朋友通信,而不与陌生人通信,从而可以更好地控制数据的访问权限;
- 简化了系统的设计:因为每个对象只需要与其直接的朋友通信,而不必关心其它对象的内部机制。这样可以使系统更加简单易懂,易维护。
7.3.2 过度使用迪米特法则的缺点
- 违背了面向对象的封装性原则:对象之间将会过多地暴露其内部的信息;
- 降低系统的效率:在实际应用中,有时候需要直接通信的对象之间,也需要进行传递消息。如果过分地使用迪米特法则,会导致过多的中间对象,降低系统的效率。
8,组合复用原则 (Composite Reuse Principle,CRP)
8.1 组合复用原则的定义
尽量使用对象组合,而不是继承关系来实现代码的复用。它强调了可以通过将已有的对象组合成更复杂的对象来实现代码复用的目标,而不是通过继承一个基类,然后添加新的属性和行为(C语言中很难区分对象及类,这一原则比较难完全用C实现)。
8.2 怎么用C语言实现组合复用原则
在C语言中,可以通过结构体来实现对象的组合关系。
举个例子,假设我们有一个图形计算程序,其中需要实现矩形对象和圆形对象的计算。我们可以定义一个形状结构体,用于表示矩形和圆形对象的共同属性:
typedef struct shape {
int x;
int y;
} Shape;
然后定义一个矩形结构体和圆形结构体,它们分别包含一个形状结构体:
typedef struct rectangle {
Shape shape;
int width;
int height;
} Rectangle;
typedef struct circle {
Shape shape;
int radius;
} Circle;
这样,我们就可以通过组合的方式来复用Shape结构体的属性和方法,避免了代码的重复。对于矩形对象和圆形对象的计算,我们可以在相应的结构体中定义方法,通过传递形状结构体来实现计算:
float calculateRectangleArea(Rectangle rect) {
return rect.width * rect.height;
}
float calculateCircleArea(Circle circle) {
return 3.14 * circle.radius * circle.radius;
}
这个例子中,我们通过组合的方式实现了代码的复用,同时也体现了组合复用原则的思想。
8.3 组合复用原则的优缺点
8.3.1 适当使用组合复用原则的优点
- 提高代码重用性:组合复用原则可以使得一个对象的行为和状态能够被多个其他对象共享和重用,减少了代码冗余和重复,并且有助于提高系统的可维护性和可扩展性。
- 降低系统耦合性:组合复用原则要求尽可能使用组合关系而不是继承关系,这可以降低类与类之间的依赖关系,避免继承的缺点,从而提高系统的松耦合性。
- 提高系统灵活性:组合复用原则可以使得系统中的对象之间互相合作,从而能够更灵活地应对需求变化和系统的复杂性差异。
8.3.2 过度使用组合复用原则的缺点
- 增加代码复杂性:组合复用原则需要更多的协作和交互,这可能会增加代码的复杂性和难度。
- 需要更多的详细设计:由于组合复用原则强调对象之间的合作和交互,因此在系统设计过程中需要更多的详细设计和构建过程,这可能会导致耗时较长。