目录
[开放 - 封闭原则](#开放 - 封闭原则)
面向对象设计原则是指导开发者设计灵活、可维护、可扩展代码的基本准则。这些原则由 "四人组"(Gang of Four, GoF)在《设计模式:可复用面向对象软件的基础》中提出,其核心思想是 "高内聚、低耦合"。
单一职责原则
- 定义 :一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。
- 目的 :降低类的复杂度,提高代码的可读性和可维护性。
cpp
// 单一职责原则(SRP)示例
// 反例:一个类承担多个职责
class BadUserHandler {
private:
std::string username;
public:
// 职责1:用户管理
void setUsername(const std::string& name) {
username = name;
}
// 职责2:日志记录(不属于用户管理职责)
void logLogin() {
std::ofstream file("log.txt", std::ios::app);
file << username << " logged in at " << time(nullptr) << std::endl;
}
};
// 正例:职责分离
class UserManager { // 仅负责用户管理
private:
std::string username;
public:
void setUsername(const std::string& name) {
username = name;
}
std::string getUsername() const {
return username;
}
};
class Logger { // 仅负责日志记录
public:
static void log(const std::string& message) {
std::ofstream file("log.txt", std::ios::app);
file << message << " at " << time(nullptr) << std::endl;
}
};
开放 - 封闭原则
- 定义 :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 目的 :通过扩展已有代码来应对变化,而非修改原有代码,避免引入新 bug。
- 实现方式:依赖抽象(如接口或抽象类),通过子类继承或实现接口来扩展功能。
cpp
// 开放-封闭原则(OCP)示例
// 反例:新增功能需要修改原有代码
class BadShapeDrawer {
public:
void draw(const std::string& shape) {
if (shape == "circle") {
std::cout << "Drawing circle" << std::endl;
} else if (shape == "square") {
std::cout << "Drawing square" << std::endl;
// 新增图形需要修改这里
}
}
};
// 正例:对扩展开放,对修改关闭
class Shape { // 抽象基类
public:
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() = default;
};
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 ShapeDrawer { // 无需修改即可支持新图形
public:
void drawShape(const Shape& shape) {
shape.draw(); // 依赖抽象
}
};
里氏替换原则
- 定义 :子类对象必须能够替换其基类对象,且程序逻辑不受影响(即 "子类可以扩展父类,但不能改变父类原有功能")。
- 目的 :保证继承关系的合理性,防止子类破坏父类的预期行为。
cpp
#include <iostream>
// 抽象父类:定义所有形状的共同行为
class Shape {
public:
virtual int getArea() = 0; // 核心行为:计算面积
virtual ~Shape() = default; // 虚析构函数
};
// 长方形子类
class Rectangle : public Shape {
private:
int width;
int height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int getArea() override { return width * height; } // 实现面积计算
};
// 正方形子类
class Square : public Shape {
private:
int side;
public:
Square(int s) : side(s) {}
void setSide(int s) { side = s; }
int getArea() override { return side * side; } // 实现面积计算
};
// 通用函数:接收Shape基类对象,计算并打印面积
void printArea(Shape& shape) {
std::cout << "面积: " << shape.getArea() << std::endl;
}
//Square和Rectangle作为Shape的子类,可以安全地替换Shape对象,且不会破坏程序的预期行为。
int main() {
// 1. 创建Rectangle对象,用Shape&引用
Rectangle rect(2, 3);
Shape& shape1 = rect;
printArea(shape1); // 输出:6(正确)
// 2. 创建Square对象,用Shape&引用(替换父类对象)
Square square(2);
Shape& shape2 = square;
printArea(shape2); // 输出:4(正确)
// 3. 动态替换:在需要Shape的地方,可任意切换子类
Shape* shapePtr;
shapePtr = new Rectangle(3, 4);
printArea(*shapePtr); // 输出:12(正确)
delete shapePtr;
shapePtr = new Square(3); // 替换为Square对象
printArea(*shapePtr); // 输出:9(正确)
delete shapePtr;
return 0;
}
依赖倒置原则
- 定义 :高层模块不依赖低层模块,两者都依赖抽象;抽象不依赖细节,细节依赖抽象。 (要依赖抽象,不要依赖具体实现")。
- 目的 :降低耦合度,提高系统的灵活性和可扩展性(高层模块可通过抽象调用不同的低层实现)。
cpp
#include <iostream>
#include <string>
// 反例:违反依赖倒置原则
// 低层模块:MySQL数据库实现
class MySQLDatabase {
public:
void save(const std::string& data) {
std::cout << "保存数据到MySQL: " << data << std::endl;
}
};
// 高层模块:直接依赖具体的低层模块
class UserService {
private:
// 问题:直接依赖具体的MySQLDatabase
MySQLDatabase db;
public:
void saveUser(const std::string& username) {
db.save(username);
}
};
// 问题:如果要更换数据库为MongoDB,必须修改UserService
class MongoDatabase {
public:
void save(const std::string& data) {
std::cout << "保存数据到MongoDB: " << data << std::endl;
}
};
// 此时需要修改UserService的实现才能使用MongoDatabase
// 正例:遵循依赖倒置原则
// 抽象层:定义数据库操作接口(不依赖任何具体实现)
class Database {
public:
virtual void save(const std::string& data) = 0; // 纯虚函数
virtual ~Database() = default; // 虚析构函数
};
// 低层模块:实现抽象接口
class MySQLDB : public Database {
public:
void save(const std::string& data) override {
std::cout << "保存数据到MySQL: " << data << std::endl;
}
};
class MongoDB : public Database {
public:
void save(const std::string& data) override {
std::cout << "保存数据到MongoDB: " << data << std::endl;
}
};
// 高层模块:只依赖抽象接口,不依赖具体实现
class UserManager {
private:
Database* db; // 依赖抽象,而非具体类
public:
// 通过构造函数注入具体实现(依赖注入)
UserManager(Database* database) : db(database) {}
void saveUser(const std::string& username) {
db->save(username); // 调用抽象接口,不关心具体是哪种数据库
}
};
// 使用示例
int main() {
// 可以自由切换数据库,无需修改UserManager
Database* mysql = new MySQLDB();
UserManager userMgr1(mysql);
userMgr1.saveUser("Alice"); // 输出:保存数据到MySQL: Alice
Database* mongo = new MongoDB();
UserManager userMgr2(mongo);
userMgr2.saveUser("Bob"); // 输出:保存数据到MongoDB: Bob
delete mysql;
delete mongo;
return 0;
}
接口隔离原则
- 定义 :客户端不应该被迫依赖它不需要的接口。(即一个接口应只包含客户端需要的方法,避免 "胖接口")。
- 目的:减少接口冗余,防止客户端依赖无关方法,降低耦合。
cpp
#include <iostream>
// 反例:违反接口隔离原则(胖接口)
// 一个包含所有功能的大接口
class Worker {
public:
virtual void work() = 0; // 工作
virtual void eat() = 0; // 吃饭
virtual void sleep() = 0; // 睡觉
};
// 人类需要所有功能,没问题
class Human : public Worker {
public:
void work() override { std::cout << "人类工作" << std::endl; }
void eat() override { std::cout << "人类吃饭" << std::endl; }
void sleep() override { std::cout << "人类睡觉" << std::endl; }
};
// 机器人被迫实现不需要的方法(问题所在)
class Robot : public Worker {
public:
void work() override { std::cout << "机器人工作" << std::endl; }
// 机器人不需要吃饭和睡觉,但必须实现这些方法
void eat() override { /* 空实现或抛出异常 */ }
void sleep() override { /* 空实现或抛出异常 */ }
};
// 正例:遵循接口隔离原则(拆分接口)
// 拆分后的小接口:每个接口只包含相关的方法
class Workable {
public:
virtual void work() = 0;
virtual ~Workable() = default;
};
class Eatable {
public:
virtual void eat() = 0;
virtual ~Eatable() = default;
};
class Sleepable {
public:
virtual void sleep() = 0;
virtual ~Sleepable() = default;
};
// 人类需要所有接口,就实现所有接口
class HumanWorker : public Workable, public Eatable, public Sleepable {
public:
void work() override { std::cout << "人类工作" << std::endl; }
void eat() override { std::cout << "人类吃饭" << std::endl; }
void sleep() override { std::cout << "人类睡觉" << std::endl; }
};
// 机器人只需要工作接口,就只实现它
class RobotWorker : public Workable {
public:
void work() override { std::cout << "机器人工作" << std::endl; }
// 不需要实现eat和sleep,避免了冗余
};
// 使用示例
int main() {
// 人类可以做所有事
HumanWorker human;
human.work();
human.eat();
human.sleep();
// 机器人只需要工作
RobotWorker robot;
robot.work();
// 机器人没有eat和sleep方法,不会被误用
return 0;
}
迪米特法则
- 定义 :一个对象应该对其他对象保持最少的了解(只与直接朋友通信,不与 "陌生人" 交互)。
- "直接朋友":当前对象本身、方法参数、返回值、成员变量、局部变量。
- 目的:减少对象之间的交互,降低系统复杂度。
cpp
// 反例:违反迪米特法则
namespace BadExample {
// 引擎类
class Engine {
private:
int power; // 功率
public:
Engine(int p) : power(p) {}
int getPower() const { return power; }
};
// 汽车类
class Car {
private:
Engine engine; // 汽车包含引擎
public:
Car(Engine e) : engine(e) {}
// 问题:暴露了内部成员engine(朋友的朋友)
Engine getEngine() const { return engine; }
};
// 司机类
class Driver {
public:
// 司机直接访问了汽车的引擎(陌生人)
void checkCar(const Car& car) {
// 违反迪米特法则:Driver -> Car -> Engine(访问了朋友的朋友)
std::cout << "引擎功率: " << car.getEngine().getPower() << std::endl;
}
};
}
// 正例:遵循迪米特法则
namespace GoodExample {
// 引擎类
class Engine {
private:
int power;
public:
Engine(int p) : power(p) {}
// 引擎只对直接朋友Car暴露必要接口
int getPower() const { return power; }
};
// 汽车类
class Car {
private:
Engine engine; // 内部成员,不对外暴露
public:
Car(Engine e) : engine(e) {}
// 提供封装方法,隐藏内部细节
int getEnginePower() const {
// Car和Engine是直接朋友,交互合法
return engine.getPower();
}
};
// 司机类
class Driver {
public:
// 司机只和直接朋友Car交互,不知道Engine的存在
void checkCar(const Car& car) {
// 符合迪米特法则:只访问直接朋友Car的方法
std::cout << "引擎功率: " << car.getEnginePower() << std::endl;
}
};
}
合成复用原则
- 定义 :优先使用组合(
has-a
)或聚合(contains-a
)关系复用代码,而非继承(is-a
)。 - 目的:避免继承带来的强耦合(子类依赖父类实现),通过组合灵活替换组件。
cpp
#include <iostream>
#include <string>
// 反例:过度使用继承(违反合成复用原则)
namespace BadExample {
// 发动机类
class Engine {
protected:
std::string type; // 发动机类型
public:
Engine(std::string t) : type(t) {}
void start() {
std::cout << type << "发动机启动" << std::endl;
}
};
// 错误:汽车继承发动机("是一个"的关系不成立)
class Car : public Engine {
public:
// 汽车被迫继承发动机的构造函数
Car(std::string engineType) : Engine(engineType) {}
void drive() {
start(); // 复用发动机的启动功能
std::cout << "汽车行驶中" << std::endl;
}
};
}
// 正例:使用组合(遵循合成复用原则)
namespace GoodExample {
// 发动机类
class Engine {
private:
std::string type;
public:
Engine(std::string t) : type(t) {}
void start() {
std::cout << type << "发动机启动" << std::endl;
}
};
// 正确:汽车包含发动机("有一个"的关系)
class Car {
private:
// 组合发动机对象(核心:用成员变量持有,而非继承)
Engine engine;
public:
// 通过构造函数注入发动机(灵活更换)
Car(Engine e) : engine(e) {}
void drive() {
engine.start(); // 复用发动机的功能
std::cout << "汽车行驶中" << std::endl;
}
};
}
int main() {
// 违反合成复用原则的情况
BadExample::Car badCar("汽油");
std::cout << "违反合成复用原则:" << std::endl;
badCar.drive();
// 问题:如果想换发动机为电动,必须修改Car类或新建子类
// 遵循合成复用原则的情况
GoodExample::Engine electricEngine("电动");
GoodExample::Car goodCar(electricEngine); // 直接组合电动发动机
std::cout << "\n遵循合成复用原则:" << std::endl;
goodCar.drive();
// 优势:换发动机只需传入新对象,无需修改Car类
return 0;
}
总结
- 单一职责原则要求类 / 接口专注于单一职责,避免功能混杂;
- 开放 - 封闭原则强调对扩展开放、对修改关闭,通过抽象实现灵活扩展;
- 里氏替换原则确保子类可安全替换父类而不破坏程序逻辑;
- 依赖倒置原则主张依赖抽象而非具体实现,降低模块间耦合;
- 接口隔离原则反对 "胖接口",倡导拆分出专一接口以避免客户端依赖无关方法;
- 迪米特法则要求对象仅与直接朋友交互,减少对其他对象的了解;
- 合成复用原则优先通过组合 / 聚合复用代码,而非继承,以提高灵活性。
👉👈.