跳转到内容

设计原则

单一职责原则

单一职责原则强调类的职责单一性和变化的原因唯一性,通过将职责分离到不同的类中,减少类的复杂度,提高代码的可读性、可维护性和可复用性。在实际开发中,遵循单一职责原则有助于构建高内聚、低耦合的系统。

核心理念是:一个类应该只有一个引起它变化的原因,或者说一个类应该仅有一个职责。换句话说,一个类只负责一个功能领域中的一项职责。

  • 职责单一
    • 一个类只负责一项职责,这样可以使类的实现更加清晰和简洁。
    • 如果一个类承担了过多的职责,它就会变得臃肿且难以维护。
  • 变化的原因
    • 一个类只应该有一个引起它变化的原因。
    • 当某个类承担了多个职责时,这些职责可能会因为不同的原因发生变化,从而导致该类需要频繁修改,增加了出错的可能性。

例子

假设我们有一个类Employee,它负责员工的基本信息管理和薪资计算:

class Employee {
public:
Employee(std::string name, double salary) : m_name(std::move(name)), m_salary(salary) {}
// 负责员工信息管理
void setName(const std::string &name) { m_name = name; }
[[nodiscard]] std::string getName() const { return m_name; }
// 负责薪资计算
void setSalary(double salary) { m_salary = salary; }
double getSalary() const { return m_salary; }
double calculateAnnualSalary() const { return m_salary * 12; }
private:
std::string m_name;
double m_salary;
};

在这个例子中,Employee类同时负责员工信息管理和薪资计算,违反了单一职责原则。如果我们需要修改薪资计算逻辑或修改员工信息管理逻辑,都需要修改这个类,这增加了代码的耦合性和维护难度。

为了遵循单一职责原则,我们可以将Employee类的职责分离为两个类:一个类负责员工信息管理,另一个类负责薪资计算。

class Employee {
public:
Employee(std::string name) : m_name(std::move(name)) {}
void setName(const std::string &name) { m_name = name; }
std::string getName() const { return m_name; }
private:
std::string m_name;
};
class SalaryCalculator {
public:
SalaryCalculator(double salary) : m_salary(salary) {}
void setSalary(double salary) { m_salary = salary; }
double getSalary() const { return m_salary; }
double calculateAnnualSalary() const { return m_salary * 12; }
private:
double m_salary;
};

通过这样的重构,Employee类和SalaryCalculator类各自只负责一个职责,从而遵循了单一职责原则。这使得每个类的变化只会受到一种原因的影响,增加了代码的可维护性和可扩展性。

开放封闭原则

开放封闭原则旨在通过对系统进行扩展而不是修改,来提高代码的可维护性、可扩展性和稳定性。要求软件系统在增加新功能时,不需要修改现有的代码,而是通过增加新代码来实现。

开放-封闭原则的核心理念是:软件实体(如类、模块和函数)应该对扩展开放,对修改封闭。

  • 对扩展开放
    • 软件实体应该可以被扩展以支持新的行为或功能。
    • 这种扩展通常通过继承(继承类)或实现接口(多态)来实现。
  • 对修改封闭
    • 一旦软件实体完成开发并经过测试,就不应该轻易修改它。
    • 避免对现有代码的修改可以减少引入新bug的风险,确保系统的稳定性。

例子 计算图形面积

假设有一个用于计算图形面积的程序,最初只支持矩形。

class Rectangle {
public:
Rectangle(double width, double height) : width(width), height(height) {}
double area() const {
return width * height;
}
private:
double width;
double height;
};
class AreaCalculator {
public:
double calculateArea(const Rectangle& rectangle) {
return rectangle.area();
}
};

现在如果我们想要扩展该程序,使其支持圆形,我们应该如何实现?

按照开放封闭原则,我们不应该修改现有的Rectangle类和AreaCalculator类,而是通过扩展系统来支持新功能。

我们可以通过引入一个抽象基类Shape来表示图形,然后让Rectangle和新引入的Circle类继承Shape,并修改AreaCalculator以支持新的形状类型。

class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0; // 纯虚函数,定义接口
};
class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width(width), height(height) {}
double area() const override {
return width * height;
}
private:
double width;
double height;
};
class Circle : public Shape {
public:
Circle(double radius) : radius(radius) {}
double area() const override {
return 3.14159 * radius * radius;
}
private:
double radius;
};
class AreaCalculator {
public:
double calculateArea(const Shape& shape) {
return shape.area();
}
};

通过上述改动,我们实现了对系统的扩展(添加了对圆形的支持),而没有修改原有的Rectangle类和AreaCalculator类的实现。这样做符合开放封闭原则。

依赖倒转原则

依赖倒转原则强调,高层模块和低层模块都应该依赖于抽象,而不是具体实现。

依赖倒转原则旨在减少高层模块和低层模块之间的耦合,使得系统更容易扩展和维护。

通过引入接口或抽象类,并让具体实现依赖于这些抽象,系统可以更灵活地应对变化和扩展需求。

基本思想:高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

高层模块和低层模块

  • 高层模块通常是指那些实现复杂逻辑或业务规则的模块。可以理解为上层应用、业务层的实现。
  • 低层模块通常是指那些实现基础服务或细节功能的模块。可以理解为底层接口,比如封装好的API、动态库等。

抽象和具体实现

  • 抽象通常是指接口或抽象类,它定义了行为的契约,而不关心具体的实现细节。
  • 具体实现是指实现了这些接口或抽象类的具体类。

例子 一个高层模块依赖低层模块的例子

某项目低层使用的是 MySql 的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层数据的处理。

而后由于某些原因,数据量暴增,因此计划更换为 Oracle 数据库。

高层代码的数据库操作部分直接调用了低层的接口,由于低层的数据库接口变了,因此高层代码也需要进行对应的修改,无法直接复用高层代码,开发者欲哭无泪。

通过上面的例子得知,当依赖的低层模块变了,就会牵一发而动全身。

例子

假设我们有一个简单的日志系统,其中FileLogger类用于将日志信息写入文件,而Application类则是高层模块,负责业务逻辑,并需要使用FileLogger来记录日志。

class FileLogger {
public:
void log(const std::string &message) {
// 代码省略:将日志写入文件
std::cout << "Logging to file: " << message << std::endl;
}
};
class Application {
public:
Application(FileLogger &logger) : m_logger(logger) {}
void run() {
// 业务逻辑
m_logger.log("Application started");
}
private:
FileLogger &m_logger;
};

在这个例子中,Application类依赖于具体的FileLogger类,这违反了依赖倒转原则。如果我们需要更换日志记录方式,比如改为记录到数据库或远程服务器,则需要修改Application类,增加了耦合性和维护难度。

为了遵循依赖倒转原则,我们可以引入一个抽象的日志接口,并让Application依赖这个抽象接口,而不是具体实现。

class ILogger {
public:
virtual ~ILogger() = default;
virtual void log(const std::string& message) = 0;
};
class FileLogger : public ILogger {
public:
void log(const std::string& message) override {
// 代码省略:将日志写入文件
std::cout << "Logging to file: " << message << std::endl;
}
};
class Application {
public:
Application(ILogger& logger) : m_logger(logger) {}
void run() {
// 业务逻辑
m_logger.log("Application started");
}
private:
ILogger& m_logger;
};

通过这种方式,Application类只依赖于ILogger接口,而不依赖于具体的FileLogger类。这样一来,如果我们需要更换日志记录方式,只需创建新的实现类,而无需修改Application类。

里氏代换原则

里氏代换原则要求子类必须能够替换父类,而不会改变程序的正确性和行为。遵守这个原则有助于构建可靠和稳定的继承层次结构,提高代码的可维护性和可扩展性。

核心理念是:如果ST的子类,那么在任何可以使用T的地方都可以使用S而不会因此发生错误。即:子类必须能够替换掉它们的父类。

子类型替换父类型

  • 子类对象可以替换父类对象,程序的行为不会改变。
  • 子类应保证不改变父类的预期行为。

行为一致性

  • 子类必须确保不破坏父类所具有的行为特征。
  • 子类可以扩展父类的行为,但不能改变原有的行为。

契约

  • 子类应遵守父类的“契约”(接口或抽象类中的方法),不能违反这些契约。

例子

假设我们有一个基类Bird,表示鸟类,并有一个子类Penguin,表示企鹅。Bird类有一个fly方法,因为大多数鸟都会飞:

class Bird {
public:
virtual void fly() {
std::cout << "I can fly!" << std::endl;
}
};
class Penguin : public Bird {
public:
void fly() override {
// 企鹅不会飞
std::cout << "I cannot fly!" << std::endl;
}
};

在这个例子中,如果我们在代码的某些地方使用了Bird类,并调用了它的fly方法,我们期望所有鸟类对象都能飞。但是,如果我们用Penguin替换Bird,企鹅不会飞,这就违反了里氏代换原则。

void makeBirdFly(Bird& bird) {
bird.fly();
}
int main() {
Bird bird;
Penguin penguin;
makeBirdFly(bird); // 输出 "I can fly!"
makeBirdFly(penguin); // 输出 "I cannot fly!" 违反了预期
return 0;
}

为了遵守里氏代换原则,我们应该重新设计类层次结构,使得子类不会违背父类的行为。一个可能的解决方案是将飞行行为分离到一个独立的接口中,而不是在基类中定义:

class Bird {
public:
virtual void eat() {
std::cout << "I can eat!" << std::endl;
}
};
class Flyable {
public:
virtual void fly() = 0;
};
class Sparrow : public Bird, public Flyable {
public:
void fly() override {
std::cout << "I can fly!" << std::endl;
}
};
class Penguin : public Bird {
// 企鹅不实现Flyable接口
};

在这种设计中,只有会飞的鸟类才实现Flyable接口,而Penguin类则不会实现该接口:

void makeFly(Flyable& flyable) {
flyable.fly();
}
int main() {
Sparrow sparrow;
Penguin penguin;
makeFly(sparrow); // 输出 "I can fly!"
// makeFly(penguin); // 编译错误,企鹅不会飞
return 0;
}