前言:在平时coding过程中,大部分程序员可能把更多精力和时间花在功能的实现和完成上面,对于代码的可读性、可读性及可扩展性没有过多的关注,这可能会造成后期功能扩展困难、新人无法理解等问题。这里介绍一些软件代码设计原则,帮助大家提升代码质量。
目录
[二、单一职责原则(Single Responsible Principle)](#二、单一职责原则(Single Responsible Principle))
[二、开闭原则(Open Close Principle)](#二、开闭原则(Open Close Principle))
[三、里氏替换原则(Liskov Substitution Principle)](#三、里氏替换原则(Liskov Substitution Principle))
[四、接口隔离原则(Interface Segregation Principle)](#四、接口隔离原则(Interface Segregation Principle))
[五、依赖倒置原则(Dependency Inversion Principle)](#五、依赖倒置原则(Dependency Inversion Principle))
一、SOLID软件设计原则
SOLID是面向对象软件设计中5个基础设计原则的简写,由Robert C. Martin提出,是设计模式的指导思想。这些原则包括单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。下面章节分别展开介绍。
二、单一职责原则(Single Responsible Principle)
1)定义:一个类应当只做一件事情,只有一个引起它变化的原因
2)思想:避免一个类同时负责多个功能,否则对一个功能的修改可能会影响到其他功能,修改扩展成本增加。
3)示例:
反例:一个**UserManager** 类同时负责用户信息管理 和用户日志记录,如果日志的存储方式修改(比如从本地文件改成数据库),就需要修改这个类,违反了单一职责。
正例:拆分为两个类:
cpp
// 只负责用户信息的管理
class UserManager {
public:
void addUser(const std::string& username) {
// 处理用户添加的逻辑
}
void deleteUser(const std::string& username) {
// 处理用户删除的逻辑
}
};
// 只负责用户相关的日志记录
class UserLogger {
public:
void logUserOperation(const std::string& username, const std::string& operation) {
// 处理日志记录的逻辑
}
};
二、开闭原则(Open Close Principle)
1)定义:软件模块(类、函数等)应该对扩展开放,对修改关闭。
2)思想:当需要新增功能时,应当通过扩展已有代码模块来实现,而不是在已有代码上直接修改、打补丁,防止补丁上打补丁,时间长了就难以维护了。
3)示例:
反例:一个**ShapeCalculator**类,计算不同图形的面积,如果新增一种图形(比如椭圆),就需要修改类的代码:
cpp
// 反例:新增图形需要修改这个类
class ShapeCalculator {
public:
double calculateArea(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;
}
};
正例:通过抽象类+继承实现
cpp
// 抽象的图形类
class Shape {
public:
virtual double calculateArea() const = 0;
virtual ~Shape() = default;
};
// 圆形类,继承Shape
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double calculateArea() const override {
return M_PI * radius * radius;
}
};
// 矩形类,继承Shape
class Rectangle : public Shape {
private:
double width;
double height;
};
// 面积计算器,只依赖抽象类,新增图形不需要修改这个类
class ShapeCalculator {
public:
double calculateArea(const Shape& shape) {
return shape.calculateArea();
}
};
上述代码中出现了虚函数和抽象类的概念,如需要可参考之前的文章:C++ 虚函数 解析指南-CSDN博客
三、里氏替换原则(Liskov Substitution Principle)
1)定义:子类对象可以替换父类对象在程序中的所有使用场景,且不会改变程序的正确性。
2)思想:子类必须完全实现父类功能,不能破坏父类的行为,避免继承关系的滥用。
3)示例:
反例:正方形继承矩形,但是正方形的宽和高必须相等,修改宽的时候高也会变化,破坏了矩形的行为
cpp
class Rectangle {
protected:
double width;
double height;
public:
void setWidth(double w) { width = w; }
void setHeight(double h) { height = h; }
double getArea() { return width * height; }
};
// 反例:正方形继承矩形,但是setWidth和setHeight的行为不符合父类的约定
class Square : public Rectangle {
public:
void setWidth(double w) override {
width = w;
height = w;
}
void setHeight(double h) override {
width = h;
height = h;
}
};
正例:重新设计继承关系,使得正方形与矩形都继承抽象的Shape类。
四、接口隔离原则(Interface Segregation Principle)
1)定义:客户端不应当依赖它不需要的接口,一个类对另一个类的依赖应当建立在最小接口上。
2)思想:避免设计大而全的接口,接口应当拆分多个小的,高内聚、低耦合。
3)示例:
反例:一个大的Worker接口,包含了所有工作相关的方法,但是不同的 worker 只需要其中一部分
cpp
// 反例:大而全的接口
class Worker {
public:
virtual void work() = 0;
virtual void eat() = 0;
virtual void sleep() = 0;
};
// 机器人只需要work,但是必须实现eat和sleep
class Robot : public Worker {
public:
void work() override { /* 工作逻辑 */ }
void eat() override { /* 机器人不需要吃饭,空实现 */ }
void sleep() override { /* 机器人不需要睡觉,空实现 */ }
};
正例:拆分为小接口
cpp
// 只包含工作的接口
class Workable {
public:
virtual void work() = 0;
virtual ~Workable() = default;
};
// 只包含休息相关的接口
class Restable {
public:
virtual void eat() = 0;
virtual void sleep() = 0;
virtual ~Restable() = default;
};
// 机器人只实现Workable接口
class Robot : public Workable {
public:
void work() override { /* 工作逻辑 */ }
};
// 人类实现Workable和Restable接口
class HumanWorker : public Workable, public Restable {
public:
void work() override { /* 工作逻辑 */ }
void eat() override { /* 吃饭逻辑 */ }
void sleep() override { /* 睡觉逻辑 */ }
};
五、依赖倒置原则(Dependency Inversion Principle)
1)定义:上层模块不应当依赖底层模块抽象不应当依赖细节,细节应当依赖抽象。
2)思想:通过抽象(抽象类或接口)实现上层与底层解耦,使得模块之间依赖关系倒置,提高灵活性。
3)示例:
反例:上层模块ReportGenerator 直接依赖底层模块MySQLDatabase ,如果需要切换数据库(比如改成 PostgreSQL ),就需要修改ReportGenerator
cpp
// 反例:上层依赖底层的具体实现
class MySQLDatabase {
public:
std::string getData() {
return "从MySQL获取的数据";
}
};
class ReportGenerator {
private:
MySQLDatabase db;
public:
std::string generateReport() {
return "报告内容:" + db.getData();
}
};
正例:通过抽象接口解耦
cpp
// 抽象的数据库接口
class Database {
public:
virtual std::string getData() const = 0;
virtual ~Database() = default;
};
// MySQL的具体实现,依赖抽象接口
class MySQLDatabase : public Database {
public:
std::string getData() const override {
return "从MySQL获取的数据";
}
};
// PostgreSQL的具体实现,依赖抽象接口
class PostgreSQLDatabase : public Database {
public:
std::string getData() const override {
return "从PostgreSQL获取的数据";
}
};
// 高层模块依赖抽象接口,而不是具体实现
class ReportGenerator {
private:
const Database& db;
public:
// 通过构造函数注入依赖
ReportGenerator(const Database& database) : db(database) {}
std::string generateReport() {
return "报告内容:" + db.getData();
}
};
结束语:一般这5种原则不会孤立存在,通常两三个会结合在一起使用。读者可参考进行理解消化,最终目的是应用于实际项目中,实现代码质量显著提升。