“屎山代码”?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 ifreturn 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 w) override {width = w;height = w;}voidsetHeight(double h) override {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 Workable, public 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()const= 0;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();}};
夜雨聆风