文章目录
-
-
- [** 1单一职责原则 (Single Responsibility Principle, SRP)**](#** 1单一职责原则 (Single Responsibility Principle, SRP)**)
- [**2 开闭原则 (Open/Closed Principle, OCP)**](#2 开闭原则 (Open/Closed Principle, OCP))
- [**3 里氏替换原则 (Liskov Substitution Principle, LSP) **](#**3 里氏替换原则 (Liskov Substitution Principle, LSP) **)
- [**4 接口隔离原则 (Interface Segregation Principle, ISP)**](#4 接口隔离原则 (Interface Segregation Principle, ISP))
- [**5 依赖倒置原则 (Dependency Inversion Principle, DIP) **](#**5 依赖倒置原则 (Dependency Inversion Principle, DIP) **)
- [**6 迪米特法则 (Law of Demeter, LoD) 或最少知识原则 **](#**6 迪米特法则 (Law of Demeter, LoD) 或最少知识原则 **)
- **总结**
- 参考
-
设计模式通常和面向对象编程相关,但C语言是过程式的,没有类的支持,C语言没有类,但可以通过结构体和函数指针模拟面向对象的概念。
** 1单一职责原则 (Single Responsibility Principle, SRP)**
- 原话:There should never be more than one reason for a class to change
- 本质:一个类应该只有一个引起它变化的原因,一个类只负责一个功能领域的职责,避免因一个功能修改影响其他功能。
- 目的:降低耦合,提高内聚。当需求变化时,修改只影响少数模块,减少出错风险,易于理解和维护。
- 举例:单一职责原则强调一个类只负责一个功能,这样修改一个功能时不会影响其他部分。如果违反这个原则,比如一个类同时处理用户认证和日志记录,当需要修改日志格式时,可能会意外影响到认证逻辑,导致bug。
- 说明:利用功能抽象分类,一个类/模块/函数应该只负责一项职责,也就是只对一类行为负责,只要是能归结成一类的行为,都可以属于某个模块的功能,强调内聚性。这样修改时,只影响本模块,不影响其他已测试通过模块
- 核心思想:一个类(在C中可能是模块或函数或结构体)应该只有一个引起变化的原因,即一个类只负责一项职
- 在c语言中:强调每个结构体或函数应该只有一个职责
- 关键点:拆分功能,一个函数只做一件事
示例1.1
//分离process_data和display_data职责,使得修改显示逻辑不会影响数据处理逻辑
// 数据处理模块
void process_data(int* data, int size) {
for (int i = 0; i < size; i++) {
data[i] *= 2; // 仅处理数据计算
}
}
// 显示模块
void display_data(int* data, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", data[i]); // 仅负责输出
}
printf("\n");
}
示例1.2
//分离save_to_file和process_data职责,使得修改保存逻辑不会影响数据处理逻辑,提高可维护性。
// 数据存储和数据处理分离
typedef struct {
int id;
char name[50];
} Data;
// 职责1:数据存储
void save_to_file(Data* data, const char* filename) {
FILE* fp = fopen(filename, "w");
fprintf(fp, "%d,%s", data->id, data->name);
fclose(fp);
}
// 职责2:数据处理
void process_data(Data* data) {
printf("Processing: %d - %s\n", data->id, data->name);
}
示例1.3

2 开闭原则 (Open/Closed Principle, OCP)
- 原话:software entities should be open for extension,but closed for modification
- 本质:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即,应该能通过添加新代码(扩展)来满足新需求,而不是修改已有的、经过测试的稳定代码。
- 目的:提高系统的稳定性和可维护性。这是设计模式最核心追求的目标之一,许多模式(如策略、模板方法、装饰器)都是为了实现开闭原则。
- 示例:一个计算面积的类,如果每次添加新形状都要修改原有代码,就违反了开闭原则。正确的做法是通过继承或多态来实现扩展,这样新增形状时不需要修改现有代码。
- 说明,充分利用抽象继承,通过继承扩展新功能,所以闭的是已有模块,开的是新增加功能(通过增加继承实现)
- 核心思想:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭(通过抽象),
- 在c语言中:强调通过函数指针实现扩展
- 关键点:通过抽象(函数指针)支持扩展,避免修改已有代码
示例2.1
//新增排序算法只需实现SortAlgorithm接口,无需修改execute_sort函数
// 抽象接口
typedef void (*SortAlgorithm)(int*, int);
// 具体实现1:冒泡排序
void bubble_sort(int* arr, int n) { /*...*/ }
// 具体实现2:快速排序
void quick_sort(int* arr, int n) { /*...*/ }
// 客户端代码
void execute_sort(SortAlgorithm algo, int* arr, int n) {
algo(arr, n); // 通过函数指针扩展算法
}
示例2.2
//新增策略只需添加新函数,不修改run_strategy
// 抽象策略
typedef struct {
void (*execute)(void);
} Strategy;
// 具体策略A
void strategyA_execute() { printf("Strategy A\n"); }
Strategy createStrategyA() {
return (Strategy){strategyA_execute};
}
// 具体策略B
void strategyB_execute() { printf("Strategy B\n"); }
Strategy createStrategyB() {
return (Strategy){strategyB_execute};
}
// 上下文(无需修改)
void run_strategy(Strategy s) {
s.execute();
}
示例2.3

**3 里氏替换原则 (Liskov Substitution Principle, LSP) **
- 原话: If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
- 本质:子类型必须能够替换掉它们的基类型。即,任何基类可以出现的地方,子类一定可以出现,且程序的行为不会改变(在遵循基类约定的前提下)。
- 目的:保证继承关系的正确性和健壮性,确保多态能够安全、可靠地工作。是良好继承设计的基石。
- 示例:比如,如果有一个交通工具类,子类飞机如果覆盖了父类的移动方法,导致行为不一致,就可能引发问题。比如飞机移动时需要检查跑道,而父类没有这个逻辑,替换后可能导致错误。
- 说明:里氏替换原则是针对继承而言的,如果继承的目的是共享父类的方法,则子类不应重写父类的方法,如果继承的目的是多态,则父类的方法定义为抽象方法,让子类完全实现,以上2种都是符合里氏替换原则,不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这样类和子类间的强耦合,不利于程序扩展和维护,针对后一种情况,应该遵守:前置条件即输入参数是不能被加强的,后置条件(输出)不能被削弱,不能违背对异常的约定
- 核心思想: 子类型必须能够替换它们的基类型
- 在c语言中:强调通过结构体和函数指针来模拟继承
- 关键点:子类型严格遵循基类契约(相同参数和返回类型),也即任何使用基类(Shape)的地方,都可以用子类(Circle或Rectangle)替换,且行为正确
示例3.1
//派生类结构体必须将基类作为第一个成员,通过强制类型转换实现多态
// 基类
typedef struct {
int width;
int height;
} Shape;
// 派生类(通过结构体包含实现继承)
typedef struct {
Shape base; // 必须作为第一个成员
int radius;
} Circle;
// 多态函数
void print_area(Shape* s) {
// Circle可安全转换为Shape*使用
printf("Area: %d\n", s->width * s->height);
}
示例3.2
//所有子类型(如减法)必须遵守基类约定
// 基类型
typedef struct {
int (*calculate)(int, int);
} Calculator;
// 加法实现(子类型)
int add(int a, int b) { return a + b; }
Calculator createAddCalculator() {
return (Calculator){add};
}
// 使用基类型接口
void calculate(Calculator c) {
printf("Result: %d\n", c.calculate(5, 3));
}
int main() {
Calculator adder = createAddCalculator();
calculate(adder); // 输出8
}
示例3.3

4 接口隔离原则 (Interface Segregation Principle, ISP)
- 原话: Clients should not be forced to depend upon interfaces that they do not use.
- 本质:客户端不应该依赖于它不需要的接口。应该将庞大的接口拆分成更小、更具体的接口,让客户端只需知道它们需要的方法。
- 目的:减少接口的臃肿和实现类的负担,避免"胖接口"导致的强制实现无关方法,降低耦合。
- 示例:一个用户服务接口包含创建、读取、更新、删除(CRUD)方法,但某些客户端只需要读取功能,强制实现所有方法会导致不必要的代码。拆分成更小的接口,如只读接口和可写接口,更符合需求。
- 说明,强调抽象类的接口"要少",针对的是灵活性,这样子类再实现时,就不会存在不需要的空函数,也就是隔离了不必要的空函数
- 核心思想: 客户端不应依赖不需要的接口,
- 在c语言中:强调大的结构体拆分成小的结构体,避依赖不需要的接口
- 关键点:定义细粒度接口,避免"胖接口"
示例4.1
//读写操作分离为不同接口,客户端可仅实现需要的接口
// 分离的接口
typedef struct {
void (*read)(void* data);
} Readable;
typedef struct {
void (*write)(const void* data);
} Writable;
// 具体实现
typedef struct {
Readable read_interface;
Writable write_interface;
} FileHandler;
示例2,避免强制实现不需要的方法
// 细粒度接口1
typedef struct {
void (*print)(void);
} Printer;
示例4.2
// 细粒度接口2
typedef struct {
void (*scan)(void);
} Scanner;
// 实现分离
struct MultiFunctionDevice {
Printer printer;
Scanner scanner;
};
void print_impl() { printf("Printing...\n"); }
void scan_impl() { printf("Scanning...\n"); }
// 客户端按需使用
void client_printer(Printer p) {
p.print(); // 不依赖scan
}
示例4.3

**5 依赖倒置原则 (Dependency Inversion Principle, DIP) **
-
原话:The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
-
本质:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。即,要面向接口/抽象编程,而不是面向具体实现编程。
-
目的:降低模块间的耦合度,提高系统的灵活性和可测试性。这是实现松耦合的关键,是许多设计模式(如工厂、策略、依赖注入)的核心思想。
-
示例:一个订单处理系统直接依赖数据库类,当需要切换到其他存储方式时,必须修改高层代码。正确的做法是定义接口,让高层依赖接口,低层实现接口,这样切换实现时不需要改动高层。
-
说明,利用分层思想,在设计模块时,应使用中间层,应用(高层)调用(依赖)中间层,而中间层再调用具体的实现(低层),这样中间层不变,低层变,上层不变
-
核心思想: 依赖抽象而非具体实现,高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象
-
在c语言中:通过抽象接口,使用函数指针来解耦高层模块和低层模块
-
关键点:高层模块依赖抽象接口(结构体+函数指针),而非具体实现
示例5.1
//高层模块network_transmit不直接依赖具体设备,通过函数指针实现依赖注入
// 抽象设备接口
typedef struct {
void (*send)(const char* data);
} Device;
// 具体实现
void wifi_send(const char* data) { /*...*/ }
Device wifi_device = {wifi_send};
// 客户端代码
void network_transmit(Device* dev, const char* data) {
dev->send(data); // 依赖抽象接口
}
示例5.2
//notification依赖接口,与具体实现(Email/SMS)解耦。
// 抽象层
typedef struct {
void (*send)(const char*);
} MessageService;
// 高层模块
void notification(MessageService service) {
service.send("Hello DIP!");
}
// 具体实现
void email_send(const char* msg) {
printf("Email: %s\n", msg);
}
MessageService createEmailService() {
return (MessageService){email_send};
}
int main() {
MessageService email = createEmailService();
notification(email); // 依赖抽象接口
}
示例5.3

**6 迪米特法则 (Law of Demeter, LoD) 或最少知识原则 **
- 本质:一个对象应该对其他对象有最少的了解,只和"朋友"(直接关联的对象)交谈。即,一个类只与它的直接朋友(自身、方法参数、方法创建的对象、组件对象)通信,避免过度耦合。
- 目的:降低类之间的耦合度,提高模块的独立性,使系统更易于维护和修改。
- 示例:一个报告生成类直接访问数据库类的内部结构,当数据库结构变化时,报告类也需要修改。更好的做法是通过方法暴露必要的数据,隐藏内部实现。
- 说明:只通过调用函数去得到对方的结果,而不是得到对方的内部数据,自己计算结果,这样对方修改内部实现时,自己不需要修改
- 核心思想: 最小化类之间的直接交互或耦合,一个对象应该对其他对象有最少的了解,即只与直接的朋友通信
- 在c语言中:强调控制结构体之间的直接访问,通过函数来操作数据
- 关键点:封装内部结构,通过接口访问,减少直接耦合
示例6.1
示例1,不直接访问结构体成员,
// 封装结构体细节
typedef struct {
int internal_data;
} Module;
// 通过访问函数操作数据,通过专用函数进行数据操作
void module_set_data(Module* m, int val) {
m->internal_data = val;
}
int module_get_data(Module* m) {
return m->internal_data;
}
示例6.2
通过封装限制直接访问内部对象,降低耦合
typedef struct {
char data[100];
} Database;
typedef struct {
Database* db;
// 封装间接访问
const char* (*get_data)(struct DataFetcher*);
} DataFetcher;
const char* fetch_data(DataFetcher* fetcher) {
return fetcher->db->data; // 避免直接暴露db结构
}
int main() {
Database db = {"Secret Data"};
DataFetcher fetcher = {&db, fetch_data};
printf("%s\n", fetcher.get_data(&fetcher)); // 通过接口访问
}
总结
- 结构体封装:用结构体+函数指针模拟对象
- 函数指针:实现多态和接口抽象的核心手段
- 模块化设计:通过文件分割实现高内聚低耦合
- 内存管理:需特别注意手动内存管理带来的责任
- 编译时多态:通过宏和函数指针组合实现
参考
【1】设计模式六大原则
【2】SOLID Principles: Object-Oriented Design Principles Explained
【3】SOLID principles. How to create maintainable code
【4】SOLID Principles with Real Life Examples
【5】SOLID Design Principles in Software Development
|---------------------|
| 点击下面关注,获取最新最全分享,不迷路 |