设计模式——七大设计原则
- 1、单一职责原则(SRP)
- 2、开放封闭原则(OCP)
- 3、依赖倒转原则(DIP)
- 4、里氏替换原则 (LSP)
- 5、接口隔离原则 (ISP)
- 6、合成/聚合复用原则 (CARP)
- 7、迪米特法则 (LoD)
了解 设计模式 的朋友们,想必都听说过“七大设计原则”吧。我们在进行程序设计的时候,要尽可能地保证程序的 可扩展性、可维护性 和 可读性,最经典的 23 种设计模式中或多或少地都在使用这些设计原则,也就是说,设计模式是站在设计原则的基础之上的。所以在学习设计模式之前,很有必要对这些设计原则先做一下了解。
1、单一职责原则(SRP)
There should never be more than one reason for a class to change.
理解:一个类只负责一项职责,不同的类具备不同的职责,各司其职。
面向对象三大特性之一的 封装 指的就是将单一事物抽象出来组合成一个类,所以我们在设计类的时候每个类中处理的是单一事物而不是某些事物的集合。
设计模式中所谓的 单一职责原则(Single Responsibility Principle
- SRP
),就是对一个类而言,应该仅有一个引起它变化的原因,其实就是将这个类所承担的职责单一化。如果一个类承担的职责过多,就等于把这些 职责耦合 到了一起,一个职责的变化可能会 削弱或者抑制 这个类完成其他职责的能力。这种耦合会导致设计变得脆弱,当变化发生时,设计会遭受到意想不到的破坏。
#include <iostream>
#include <string>// 单一职责原则示例:一个类只负责一个职责class File {
public:void writeToFile(const std::string& data) {// 写入文件的具体实现std::cout << "Writing to file: " << data << std::endl;}
};class Logger {
public:void log(const std::string& message) {// 记录日志的具体实现std::cout << "Logging: " << message << std::endl;}
};int main() {File file;file.writeToFile("Data to be written");Logger logger;logger.log("Log message");return 0;
}
软件设计真正要做的事情就是,发现根据需求发现职责,并把这些职责进行分离,添加新的类,给当前类减负,越是这样项目才越容易维护。杜绝万能类或万能函数!!!
2、开放封闭原则(OCP)
Software entities like classes,modules and functions should be open for extension but closed for modifications.
理解:类、模块、函数,对 扩展开放,对 修改封闭。
开放 – 封闭原则 (Open/Closed Principle
- OCP
) 说的是软件实体(类、模块、函数等)可以扩展,但是不可以修改。也就是说对于扩展是开放的,对于修改是封闭的。
该原则是程序设计的一种理想模式,在很多情况下无法做到完全的封闭。但是作为设计人员,应该能够对自己设计的模块在哪些位置产生何种变化了然于胸,因此 需要在这些位置创建 抽象类 来隔离以后发生的这些同类变化(其实就是对 多态 的应用,创建新的子类并重写父类虚函数,用以更新处理动作)。
此处的 抽象类,其实并不等价与C++中完全意义上是 抽象类 (需要有纯虚函数),这里所说的 抽象类 只需要包含虚函数(纯虚函数 或 非纯虚函数)能够实现 多态 即可。
#include <iostream>
#include <vector>// 开闭原则示例:通过抽象类和继承来实现开闭原则class Shape {
public:virtual void draw() const = 0;
};class Circle : public Shape {
public:void draw() const override {std::cout << "Drawing Circle" << std::endl;}
};class Square : public Shape {
public:void draw() const override {std::cout << "Drawing Square" << std::endl;}
};class Drawing {
public:void drawShapes(const std::vector<Shape*>& shapes) const {for (const auto& shape : shapes) {shape->draw();}}
};int main() {Circle circle;Square square;Drawing drawing;std::vector<Shape*> shapes = {&circle, &square};drawing.drawShapes(shapes);return 0;
}
开放 – 封闭原则 是面向对象设计的核心所在,这样可以给我们设计出的程序带来巨大的好处,使其可维护性、可扩展性、可复用性、灵活性更好。
3、依赖倒转原则(DIP)
High level modules should not depends upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
理解:高层模块 不应该依赖于底层模块(具体),而 应该依赖于抽象。(面向接口编程)
关于依赖倒转原则,对应的是两条非常抽象的描述:
- 高层模块不应该依赖低层模块,两个都应该依赖抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
先用人话解释一下这两句话中的一些抽象概念:
- 高层模块:可以理解为上层应用,就是业务层的实现;
- 低层模块:可以理解为底层接口,比如封装好的API、动态库等;
- 抽象:指的就是抽象类或者接口(在C++中没有接口,只有抽象类)。
先举一个 高层模块 依赖 低层模块的例子:
大聪明的项目组接了一个新项目,低层使用的是
MySql
的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层数据的处理。而后由于某些原因,数据超大规模和高并发的需求,所以更换了Redis
数据库,由于低层的数据库接口变了,高层代码的数据库操作部分是直接调用了低层的接口,因此也需要进行对应的修改,无法实现对高层代码的直接复用,大聪明欲哭无泪。
- 通过上面的例子可以得知,当依赖的低层模块变了就会牵一发而动全身,如果这样设计项目架构,对于程序猿来说,其工作量无疑是很重的。
// 依赖倒置原则示例:高层模块不应该依赖于底层模块,二者都应该依赖于抽象// 数据库接口(抽象类)
class Database {
public:virtual void connect() = 0;virtual void query(const std::string& sql) = 0;virtual void disconnect() = 0;
};// MySQL 数据库实现(低层模块)
class MySQLDatabase : public Database {
public:void connect() override {// 连接 MySQL 数据库的具体实现}void query(const std::string& sql) override {// 执行 MySQL 查询的具体实现}void disconnect() override {// 断开 MySQL 数据库连接的具体实现}
};// Redis 数据库实现(低层模块)
class RedisDatabase : public Database {
public:void connect() override {// 连接 Redis 数据库的具体实现}void query(const std::string& command) override {// 执行 Redis 命令的具体实现}void disconnect() override {// 断开 Redis 数据库连接的具体实现}
};//高层模块
class AppService {
private:Database* database;public:AppService(Database* db) : database(db) {}void performTask() {database->connect();database->query("SELECT * FROM data");// 执行其他操作database->disconnect();}
};
- 如果要搞明白这个案例的解决方案以及 抽象和细节 之间的依赖关系,需要先了解另一个原则 — 里氏替换原则。
4、里氏替换原则 (LSP)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
理解:父类可被子类替换,但 反之不一定成立。
所谓的里氏替换原则就是子类类型必须能够替换掉它们的父类类型。
关于这个原理的应用其实也很常见,比如在Qt中,所有窗口类型的类的构造函数都有一个 QWidget*
类型的参数(QWidget类
是所有窗口的 基类),通过这个参数指定当前窗口的父对象。虽然参数是窗口类的基类类型,但是我们在给其指定实参的大多数时候,指定的都是 子类的对象,其实也就是相当于使用子类类型 替换掉了 它们的 父类类型。
这个原则的要满足的第一个条件就是 继承,其次还要求子类继承的所有父类的属性和方法对于子类来说都是合理的。关于这个是否合理下面举个栗子:
比如,对于哺乳动物来说都是胎生,但是有一种特殊的存在就是鸭嘴兽,它虽然是哺乳动物,但是是卵生。
如果我们设计了两个类:哺乳动物类 和 鸭嘴兽类,此时能够让鸭嘴兽类继承哺乳动物类吗?
- 答案肯定是否定的,因为如果我们这么做了,鸭嘴兽就继承了胎生属性,这个属性和它自身的情况是不匹配的。
- 如果想要遵循里氏替换原则,我们就不能让着两个类有继承关系。
如果我们创建了其它的胎生的哺乳动物类,那么它们是可以继承哺乳动物这个类的,在实际应用中就可以使用子类替换掉父类,同时功能也不会受到影响,父类实现了复用,子类也能在父类的基础上增加新的行为,这个就是 里氏替换原则。
#include <iostream>// 里氏替换原则示例:派生类可以替代基类// 基类:哺乳动物
class Mammal {
protected:bool isViviparous; // 出生方式为胎生public:virtual void giveBirth() const {std::cout << "Giving birth" << std::endl;}
};// 派生类:狗
class Dog : public Mammal {
public:// 重写基类的 giveBirth 方法void giveBirth() const override {std::cout << "Dog giving birth to puppies" << std::endl;}
};// 派生类:猫
class Cat : public Mammal {
public:// 重写基类的 giveBirth 方法void giveBirth() const override {std::cout << "Cat giving birth to kittens" << std::endl;}
};// 函数:繁殖哺乳动物
void reproduce(const Mammal& animal) {animal.giveBirth();
}int main() {Dog myDog;Cat myCat;// 使用 Dog 对象std::cout << "Dog: ";reproduce(myDog); // 输出: Dog giving birth to puppies// 使用 Cat 对象std::cout << "Cat: ";reproduce(myCat); // 输出: Cat giving birth to kittensreturn 0;
}
上面在讲 依赖倒转原则 的时候说过,抽象不应该依赖细节,细节应该依赖抽象
。也就意味着我们应该对细节进行封装,在C++中就是将其放到一个抽象类中(C++中没有接口,不能像Java一样封装成接口),每个细节就相当于上面例子中的哺乳动物的一个特性,这样一来这个抽象的哺乳动物类就成了项目架构中高层和低层的桥梁,将二者整合到一起。
- 抽象类中提供的接口是固定不变的
- 低层模块是抽象类的子类,继承了抽象类的接口,并且可以重写这些接口的行为
- 高层模块想要实现某些功能,调用的是抽象类中的函数接口,并且是通过抽象类的父类指针引用其子类的实例对象(用子类类型替换父类类型),这样就实现了多态。
基于 依赖倒转原则 将项目的结构换成上图的这种模式之后,低层模块发生变化,对应高层模块是没有任何影响的,这样程序猿的工作量降低了,代码也更容易维护(说白了,依赖倒转原则就是对多态的典型应用)。
5、接口隔离原则 (ISP)
The dependency of one class to another one should depend on the smallest possible interface.
理解:使用多个专门的接口,而不要使用一个单一的(大)接口(接口单一职责)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
(在C++中没有接口,只有抽象类)
#include <iostream>// 接口隔离原则示例:客户端不应该被迫依赖它不使用的接口class Worker {
public:virtual void work() const = 0;
};class Eater {
public:virtual void eat() const = 0;
};class Robot : public Worker {
public:void work() const override {std::cout << "Robot is working" << std::endl;}
};class Human : public Worker, public Eater {
public:void work() const override {std::cout << "Human is working" << std::endl;}void eat() const override {std::cout << "Human is eating" << std::endl;}
};int main() {Robot robot;Human human;robot.work(); // 输出: Robot is workinghuman.work(); // 输出: Human is workinghuman.eat(); // 输出: Human is eatingreturn 0;
}
6、合成/聚合复用原则 (CARP)
Strive for a design where you compose classes from smaller, more independent units, rather than inheriting from a single, monolithic base class.
理解:尽量 使用组合/聚合,而 不是继承。
#include <iostream>// 合成/聚合复用原则示例:优先使用合成/聚合,而不是继承class Engine {
public:void start() const {std::cout << "Engine started" << std::endl;}
};class Car {
private:Engine engine;public:void start() const {engine.start();std::cout << "Car started" << std::endl;}
};int main() {Car car;car.start(); // 输出: Engine started, Car startedreturn 0;
}
7、迪米特法则 (LoD)
Only talk to you immediate friends.
理解:尽量 减少对象之间的交互,从而减小类之间的耦合。
迪米特法则 ( Law of Demeter - LoD ) 又叫 最少知道原则 ,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public
方法,不对外泄露任何信息。
迪米特法则还有个更简单的定义:只与直接的朋友通信。
- 直接的朋友 :每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合 等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而 出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量 的形式出现在类的内部。
#include <iostream>// 迪米特法则示例:一个对象应该对其它对象有尽可能少的了解class Teacher {
public:void teach() const {std::cout << "Teaching..." << std::endl;}
};class Student {
public:void learn() const {std::cout << "Learning..." << std::endl;}
};class School {
private:Teacher teacher;Student student;public:void conductClass() const {teacher.teach();student.learn();std::cout << "Class is conducted" << std::endl;}
};int main() {School school;school.conductClass(); // 输出: Teaching..., Learning..., Class is conductedreturn 0;
}
- 一定要做到:低耦合、高内聚。
注:仅供学习参考,如有不足欢迎指正!