面向对象编程的 SOLID 设计原则

目录

背景

单一职责原则

开放封闭原则

里氏替换原则

接口隔离原则

依赖倒置原则

总结


背景

大家都知道C++是面向对象开发的,封装、继承与多态是面向对象开发的三大特征。相信大家都听说过设计模式,但SOLID设计原则不知道大家有没有了解过,这个问题面试也经常会问。

SOLID原则是五个面向对象设计原则的首字母缩写,它们旨在提高软件的可维护性和可扩展性。这些原则是由罗伯特·C·马丁(Robert C. Martin)在20世纪90年代提出的,分别是单一职责原则(S)、开放封闭原则(O)、里氏替换原则(L)、接口隔离原则(I)、依赖倒置原则(D)。


单一职责原则

单一职责原则(SRP - Single Responsibility Principle):一个类只负责一件事,应该只有一个引起它变化的原因。

例子:一个类应该只负责一个功能。

// 违反单一职责原则的类
class Logger {
public:
    void log(const std::string& message) {
        // 保存日志到文件
    }
    void sendAlert(const std::string& message) {
        // 发送警告邮件
    }
};

// 遵守单一职责原则的类
class FileLogger {
public:
    void log(const std::string& message) {
        // 保存日志到文件
    }
};

class AlertLogger {
public:
    void sendAlert(const std::string& message) {
        // 发送警告邮件
    }
};

开放封闭原则

开放封闭原则(OCP - Open/Closed Principle):类应该通过扩展而非修改来应对需求变化。软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。

例子:通过继承和多态实现扩展,而不是修改现有代码。

class Shape {
public:
    virtual double area() const = 0;
};

class Circle : public Shape {
public:
    double area() const override {
        return 3.14 * 4 * 4; // 假设半径为2
    }
};

class Square : public Shape {
public:
    double area() const override {
        return 4 * 4; // 边长为4
    }
};

// 新增形状时,只需添加新的类,无需修改现有代码
class Triangle : public Shape {
public:
    double area() const override {
        return 0.5 * 4 * 4; // 底和高都是4
    }
};

里氏替换原则

里氏替换原则(LSP - Liskov Substitution Principle):子类可以替代父类,不改变程序行为以及程序的正确性。

错误示例:

#include <iostream>

// 基类
class Rectangle {
public:
    virtual void setWidth(int width) {
        this->width = width;
    }

    virtual void setHeight(int height) {
        this->height = height;
    }

    virtual int getArea() const {
        return width * height;
    }

protected:
    int width;
    int height;
};

// 派生类
class Square : public Rectangle {
public:
    void setWidth(int width) override {
        this->width = height = width; // 保持宽高相等
    }

    void setHeight(int height) override {
        this->height = width = height; // 保持宽高相等
    }
};

void processShape(Rectangle& shape) {
    shape.setWidth(5);
    shape.setHeight(10);
    std::cout << "Expected area: 50, Got area: " << shape.getArea() << std::endl;
}

int main() {
    Rectangle rect;
    Square sqr;

    processShape(rect); // 处理矩形
    processShape(sqr);  // 处理正方形

    return 0;
}

在这个例子中,Square 类是从 Rectangle 类派生的。根据LSP,任何父类 (Rectangle) 的对象都可以被其子类 (Square) 的对象替换,而不改变程序的预期行为。在 processShape 函数中,我们期望无论传入的是 Rectangle 还是 Square 对象,理论应该都能正确计算面积。

然而,由于 Square 类重写了 setWidth 和 setHeight 方法,使得设置宽度和高度时会相互影响,这违反了LSP原则。因此,尽管 Square 是 Rectangle 的子类型,但它们的行为并不完全一致,导致在使用 Square 对象时,程序的行为与预期不符。

正确的示例应该创建一个更通用的 Shape 基类,让 RectangleSquare 都继承自这个基类去遵循里氏替换原则。


接口隔离原则

接口隔离原则(ISP - Interface Segregation Principle):不应强制客户端依赖于它们不使用的接口,接口应该小而精,避免无关功能。

这个原则很好理解,简单解释一下,现在有两个客户user1和user2,对于user1而言,他会用到所有OPS的接口,对于user2,这个客户只需要用OPS里面的op1和op2这两个接口,别的都用不到,这时候开发人员只需要在中间加一个IUOps隔离开然后交付给user2就可以了。

错误示例:

// 不合理的接口,违反了接口隔离原则
class IAnimal {
public:
    virtual void eat() = 0;
    virtual void fly() = 0;
};
// 鸟类实现了吃和飞
class Bird : public IAnimal {
public:
    void eat() override {
        std::cout << "Bird is eating." << std::endl;
    }
    void fly() override {
        std::cout << "Bird is flying." << std::endl;
    }
};

// 鱼类只实现了吃,没有实现飞
class Fish : public IAnimal {
public:
    void eat() override {
        std::cout << "Fish is eating." << std::endl;
    }
    void fly() override {
        // 鱼类不需要飞,这里可以抛出异常或者不实现该方法
        std::cout << "Fish cannot fly." << std::endl;
    }
};

以上代码由于Fish类不需要fly方法,我们可以认为IAnimal接口违反了接口隔离原则。为了解决这个问题,我们可以将IAnimal接口拆分成两个更小的接口:

正确代码:

// 将IAnimal拆分成两个接口
class IEat {
public:
    virtual void eat() = 0;
};

class IFly {
public:
    virtual void fly() = 0;
};
// 鸟类实现了吃和飞
class Bird : public IEat, public IFly {
public:
    void eat() override {
        std::cout << "Bird is eating." << std::endl;
    }
    void fly() override {
        std::cout << "Bird is flying." << std::endl;
    }
};

// 鱼类只实现了吃
class Fish : public IEat {
public:
    void eat() override {
        std::cout << "Fish is eating." << std::endl;
    }
};

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。

  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。


依赖倒置原则

依赖倒置原则(DIP - Dependency Inversion Principle):高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。

例子:通过抽象(接口或抽象类)来实现模块间的依赖关系。

#include <iostream>

// 定义一个抽象的日志记录器接口
class ILogger {
public:
    virtual ~ILogger() {}
    virtual void Log(const std::string& message) = 0;
};

// 实现具体的控制台日志记录器
class ConsoleLogger : public ILogger {
public:
    void Log(const std::string& message) override {
        std::cout << "Console Logger: " << message << std::endl;
    }
};

// 实现具体的文件日志记录器
class FileLogger : public ILogger {
public:
    void Log(const std::string& message) override {
        // 假设这里写入到文件,实际中需要打开文件等操作
        std::cout << "File Logger: " << message << std::endl;
    }
};

// 应用程序类,依赖于日志记录器的接口而不是具体实现
class Application {
private:
    ILogger* logger; // 使用接口指针来引用日志记录器
public:
    Application(ILogger* logger) : logger(logger) {}
    void Run() {
        // 业务逻辑...
        logger->Log("Application is running.");
    }
};

int main() {
    // 创建具体的日志记录器实例
    ConsoleLogger consoleLogger;
    FileLogger fileLogger;

    // 将具体的日志记录器注入到应用程序中
    Application app1(&consoleLogger);
    app1.Run();

    Application app2(&fileLogger);
    app2.Run();

    return 0;
}

在这个示例中,ILogger是一个抽象的日志记录器接口,它定义了一个Log方法。ConsoleLoggerFileLogger是这个接口的两个具体实现,分别用于在控制台和文件中记录日志。Application类依赖于ILogger接口,而不是具体的实现,这符合依赖倒置原则。通过这种方式,我们可以很容易地替换或扩展日志记录方式,而无需修改Application类的代码。


总结

1.单一职责原则(SRP - Single Responsibility Principle):一个类只负责一件事。
2.开放封闭原则(OCP - Open/Closed Principle):类应该通过扩展而非修改来应对需求变化。
3.里氏替换原则(LSP - Liskov Substitution Principle):子类可以替代父类,不改变程序行为。
4.接口隔离原则(ISP - Interface Segregation Principle):不应强制客户端依赖于它们不使用的接口,接口应该小而精,避免无关功能。
5.依赖倒置原则(DIP - Dependency Inversion Principle):高层模块依赖于抽象而非具体实现。

最后,如果觉得我的文章对你有帮助,请点赞收藏加关注,谢谢大家。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值