乐于分享
好东西不私藏

“屎山代码”?SOLID 软件设计原则 帮你解决

“屎山代码”?SOLID 软件设计原则 帮你解决

大家好,我是小栈!
大家在开发大型软件项目的时候,有没有这样的感受:一开始很顺利,到后面增加新功能的时候,感觉代码越来越乱,甚至自己都不敢动了,生怕把之前的东西搞坏了。

这不是因为你技术不行,而是平时写代码“随心所欲”。如何写出可扩展、易维护的高质量代码呢?今天我们来学一学SOLID设计原则。

SOLID是面向对象软件设计中5个基础设计原则的简写,分别是单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。

01 单一职责原则

a.定义:一个类应当只做一件事情,只有一个引起它变化的原因。

b.思想:避免一个类同时负责多个功能,否则对一个功能的修改可能会影响到其他功能,修改扩展成本增加。

c.示例

反例:一个UserManager类同时负责用户信息管理和用户日志记录,如果日志的存储方式修改(比如从本地文件改成数据库),就需要修改这个类,违反了单一职责。

正例:拆分为两个独立的类

// 只负责用户信息的管理class UserManager {public:    void addUser(conststd::string& username) {        // 处理用户添加的逻辑    }    void deleteUser(conststd::string& username) {        // 处理用户删除的逻辑    }};// 只负责用户相关的日志记录class UserLogger {public:    void logUserOperation(conststd::string& username, conststd::string& operation) {        // 处理日志记录的逻辑    }};

02 开闭原则

a.定义:软件模块(类、函数等)应该对扩展开放,对修改关闭。

b.思想:当需要新增功能时,应当通过扩展已有代码模块来实现,而不是在已有代码上直接修改、打补丁,防止补丁上打补丁,时间长了就难以维护了。

c.示例

反例:一个ShapeCalculator类,计算不同图形的面积,如果新增一种图形(比如椭圆),就需要修改类的代码。

// 反例:新增图形需要修改这个类class ShapeCalculator {public:    doublecalculateArea(const std::string& shapeType, double param1, double param2) {        if (shapeType == "circle") {            return M_PI * param1 * param1;        } else if (shapeType == "rectangle") {            return param1 * param2;        }        // 新增椭圆需要在这里加else if        return 0;    }};

正例:通过抽象类+继承实现

// 抽象的图形类class Shape {public:    virtualdoublecalculateArea() const = 0;    virtual ~Shape() = default;};// 圆形类,继承Shapeclass Circle : public Shape {private:    double radius;public:    Circle(double r) : radius(r) {}    doublecalculateArea() constoverride {        return M_PI * radius * radius;    }};// 矩形类,继承Shapeclass Rectangle : public Shape {private:    double width;    double height;};// 面积计算器,只依赖抽象类,新增图形不需要修改这个类class ShapeCalculator {public:    doublecalculateArea(const Shape& shape) {        return shape.calculateArea();    }};

03 里氏替换原则

a.定义:子类对象可以替换父类对象在程序中的所有使用场景,且不会改变程序的正确性。

b.思想:子类必须完全实现父类功能,不能破坏父类的行为,避免继承关系的滥用。

c.示例

反例:正方形继承矩形,但是正方形的宽和高必须相等,修改宽的时候高也会变化,破坏了矩形的行为。

class Rectangle {protected:    double width;    double height;public:    voidsetWidth(double w) { width = w; }    voidsetHeight(double h) { height = h; }    doublegetArea() { return width * height; }};// 反例:正方形继承矩形,但是setWidth和setHeight的行为不符合父类的约定class Square : public Rectangle {public:    voidsetWidth(double woverride {        width = w;        height = w;    }    voidsetHeight(double hoverride {        width = h;        height = h;    }};

正例:重新设计继承关系,使得正方形与矩形都继承抽象的Shape类。

04 接口隔离原则

a.定义:客户端不应当依赖它不需要的接口,一个类对另一个类的依赖应当建立在最小接口上。

b.思想:避免设计大而全的接口,接口应当拆分多个小的,高内聚、低耦合。

c.示例

反例:一个大的Worker接口,包含了所有工作相关的方法,但是不同的 worker只需要其中一部分。

// 反例:大而全的接口class Worker {public:    virtualvoidwork() = 0;    virtualvoideat() = 0;    virtualvoidsleep() = 0;};// 机器人只需要work,但是必须实现eat和sleepclass Robot : public Worker {public:    voidwork() override { /* 工作逻辑 */ }    voideat() override { /* 机器人不需要吃饭,空实现 */ }    voidsleep() override { /* 机器人不需要睡觉,空实现 */ }};

正例:拆分为小接口

// 只包含工作的接口class Workable {public:    virtualvoidwork() = 0;    virtual ~Workable() = default;};// 只包含休息相关的接口class Restable {public:    virtualvoideat() = 0;    virtualvoidsleep() = 0;    virtual ~Restable() = default;};// 机器人只实现Workable接口class Robot : public Workable {public:    voidwork() override { /* 工作逻辑 */ }};// 人类实现Workable和Restable接口class HumanWorker : public Workablepublic Restable {public:    voidwork() override { /* 工作逻辑 */ }    voideat() override { /* 吃饭逻辑 */ }    voidsleep() override { /* 睡觉逻辑 */ }};

05 依赖倒置原则

a.定义:上层模块不应当依赖底层模块,抽象不应当依赖细节,细节应当依赖抽象。

b.思想:通过抽象(抽象类或接口)实现上层与底层解耦,使得模块之间依赖关系倒置,提高灵活性。

c.示例

反例:上层模块ReportGenerator 直接依赖底层模块 MySQLDataBase,如果需要切换数据库(比如改成PostgreSQL),这时候就需要修改上层模块ReportGenerator了。

// 反例:上层依赖底层的具体实现classMySQLDatabase{public:    std::string getData() {        return "从MySQL获取的数据";    }};classReportGenerator{private:    MySQLDatabase db;public:    std::string generateReport() {        return "报告内容:" + db.getData();    }};

正例:通过抽象接口解耦 【关于抽象接口思想,童鞋们可参考小栈之前的文章:纯虚函数 是普通的虚函数吗?

// 抽象的数据库接口class Database {public:    virtual std::string getData()const0;    virtual ~Database() = default;};// MySQL的具体实现,依赖抽象接口class MySQLDatabase : public Database {public:    std::string getData()constoverride{        return "从MySQL获取的数据";    }};// PostgreSQL的具体实现,依赖抽象接口class PostgreSQLDatabase : public Database {public:    std::string getData()constoverride{        return "从PostgreSQL获取的数据";    }};// 高层模块依赖抽象接口,而不是具体实现class ReportGenerator {private:    const Database& db;public:    // 通过构造函数注入依赖    ReportGenerator(const Database& database) : db(database) {}    std::string generateReport(){        return "报告内容:" + db.getData();    }};