C++设计模式:面向对象设计原则

目录

单一职责原则

[开放 - 封闭原则](#开放 - 封闭原则)

里氏替换原则

依赖倒置原则

接口隔离原则

迪米特法则

合成复用原则

总结


面向对象设计原则是指导开发者设计灵活、可维护、可扩展代码的基本准则。这些原则由 "四人组"(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;
}
    

总结

  • 单一职责原则要求类 / 接口专注于单一职责,避免功能混杂;
  • 开放 - 封闭原则强调对扩展开放、对修改关闭,通过抽象实现灵活扩展;
  • 里氏替换原则确保子类可安全替换父类而不破坏程序逻辑;
  • 依赖倒置原则主张依赖抽象而非具体实现,降低模块间耦合;
  • 接口隔离原则反对 "胖接口",倡导拆分出专一接口以避免客户端依赖无关方法;
  • 迪米特法则要求对象仅与直接朋友交互,减少对其他对象的了解;
  • 合成复用原则优先通过组合 / 聚合复用代码,而非继承,以提高灵活性。

👉👈.

相关推荐
蒋星熠5 分钟前
C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
网络·c++·人工智能·深度学习·性能优化·系统架构
CHANG_THE_WORLD19 分钟前
# C++ 中的 `string_view` 和 `span`:现代安全视图指南
开发语言·c++
雨落倾城夏未凉19 分钟前
9.c++new申请二维数组
c++·后端
雨落倾城夏未凉1 小时前
8.被free回收的内存是立即返还给操作系统吗?为什么?
c++·后端
雨落倾城夏未凉1 小时前
6.new和malloc的区别
c++·后端
郝学胜-神的一滴1 小时前
深入理解QFlags:Qt中的位标志管理工具
开发语言·c++·qt·程序人生
INS_KF2 小时前
【C++知识杂记2】free和delete区别
c++·笔记·学习
一只鱼^_2 小时前
牛客周赛 Round 105
数据结构·c++·算法·均值算法·逻辑回归·动态规划·启发式算法
啊阿狸不会拉杆2 小时前
《算法导论》第 27 章 - 多线程算法
java·jvm·c++·算法·图论