设计模式-面向对象的设计原则
变化是复用的天地。面向对象设计最大的优势在于抵御变化。
重新认识面向对象。
理解隔离变化
从宏观层面来看,面向对象的构建方式更能适应软件的变化。将变化所带来的影响减为最小。
各司其职
从微观层面来看面,面向对象的方式更强调这个类的责任。由于需求变化导致的增类型不应该影响原来类型的实现
设计原则大于设计模式。可以通过设计原则发明设计模式.
依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP) 是面向对象编程和软件设计领域中的一项重要原则,它是SOLID原则(单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则和依赖倒置原则)中的一部分。这个原则的主要目标是减少代码之间的耦合性,提高系统的可维护性和可扩展性。
依赖倒置原则的主要思想有以下几点:
高层模块不应该依赖于低层模块,二者都应该依赖于抽象:这里的"高层"和"低层"是逻辑上的概念,不是物理或架构上的。高层模块通常指的是调用其他模块或服务的模块,而低层模块则是被调用的模块或服务。这个原则要求我们在编写代码时,应该尽量依赖于接口或抽象类,而不是具体的实现类。
抽象不应该依赖于细节,细节应该依赖于抽象:这意味着我们在设计系统时,应该首先定义出抽象接口或类,然后再根据这些接口或类去编写具体的实现。这样做的好处是,我们可以随时更换具体的实现,而不会影响到高层模块。
依赖倒置原则的实现方式主要有以下几种:
使用接口或抽象类:在Java、C#等语言中,我们可以使用接口或抽象类来定义出系统的抽象部分,然后再根据这些接口或抽象类去编写具体的实现类。
通过配置文件、注解等方式注入依赖:这种方式常见于一些框架中,如Spring、Hibernate等。这些框架通过读取配置文件或注解来动态地注入依赖,从而实现了高层模块和低层模块之间的解耦。
使用依赖注入框架:依赖注入框架(如Google Guice、Dagger等)可以自动地管理对象之间的依赖关系,从而减少了我们手动编写依赖代码的工作量。
总之,依赖倒置原则是一种非常重要的编程思想,它可以帮助我们编写出更加灵活、可维护、可扩展的代码。在实际开发中,我们应该尽量遵循这个原则,以提高系统的质量和效率。
开闭封闭原则
开放封闭原则(Open-Closed Principle,OCP)是面向对象设计中的一项基本原则。该原则的核心思想是:软件实体(如类、模块和函数等)应该对扩展开放,而对修改封闭。这意味着在不修改现有代码的情况下,可以通过添加新代码来改变模块的行为。
下面用一个简单的C++例子来解释开放封闭原则。
假设我们有一个形状(Shape)类及其子类矩形(Rectangle)和圆形(Circle),并且我们想要计算这些形状的面积。如果不遵循开放封闭原则,可能会写成这样:
cpp
#include <iostream>
#include <vector>
#include <cmath>
enum ShapeType {
Rectangle,
Circle
};
class Shape {
public:
ShapeType type;
double width;
double height;
double radius;
};
double calculateArea(Shape shape) {
if (shape.type == Rectangle) {
return shape.width * shape.height;
} else if (shape.type == Circle) {
return M_PI * shape.radius * shape.radius;
}
return 0;
}
int main() {
Shape rectangle;
rectangle.type = Rectangle;
rectangle.width = 5;
rectangle.height = 10;
Shape circle;
circle.type = Circle;
circle.radius = 7;
std::vector<Shape> shapes = {rectangle, circle};
for (const auto& shape : shapes) {
std::cout << "Area: " << calculateArea(shape) << std::endl;
}
return 0;
}
在这种设计中,如果我们要添加一个新形状,例如三角形(Triangle),我们需要修改calculateArea
函数,这违反了开放封闭原则。为了遵循开放封闭原则,我们可以将形状设计成多态类,通过继承和虚函数来实现扩展:
cpp
#include <iostream>
#include <vector>
#include <cmath>
// 基类Shape
class Shape {
public:
virtual double calculateArea() const = 0; // 纯虚函数
virtual ~Shape() = default; // 虚析构函数
};
// 派生类Rectangle
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() const override {
return width * height;
}
private:
double width;
double height;
};
// 派生类Circle
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
double calculateArea() const override {
return M_PI * radius * radius;
}
private:
double radius;
};
// 新增的派生类Triangle
class Triangle : public Shape {
public:
Triangle(double b, double h) : base(b), height(h) {}
double calculateArea() const override {
return 0.5 * base * height;
}
private:
double base;
double height;
};
int main() {
// 创建各种形状对象
Rectangle rectangle(5, 10);
Circle circle(7);
Triangle triangle(6, 8);
// 存储到Shape指针的向量中
std::vector<Shape*> shapes = {&rectangle, &circle, &triangle};
for (const auto& shape : shapes) {
std::cout << "Area: " << shape->calculateArea() << std::endl;
}
return 0;
}
在这个设计中,Shape
类是一个抽象基类,定义了一个纯虚函数calculateArea
。Rectangle
、Circle
和Triangle
类分别继承自Shape
并实现了calculateArea
函数。如果要添加新的形状,只需要创建一个新的派生类并实现calculateArea
函数,而不需要修改现有的代码,这样就遵循了开放封闭原则。
单一职责原则
单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中的一项基本原则。该原则的核心思想是:一个类应该只有一个引起它变化的原因,换句话说,一个类应该仅有一个职责(责任)。如果一个类承担了多个职责,这些职责就会耦合在一起,导致变化的影响面扩大。
下面通过一个简单的C++例子来解释单一职责原则。
假设我们有一个报告(Report)类,它包含生成报告和保存报告的功能。如果不遵循单一职责原则,可能会写成这样:
cpp
#include <iostream>
#include <string>
#include <fstream>
class Report {
public:
Report(const std::string& content) : content(content) {}
void generate() {
std::cout << "Generating report: " << content << std::endl;
}
void save(const std::string& filename) {
std::ofstream file(filename);
if (file.is_open()) {
file << content;
file.close();
std::cout << "Report saved to " << filename << std::endl;
} else {
std::cerr << "Failed to open file " << filename << std::endl;
}
}
private:
std::string content;
};
int main() {
Report report("This is the report content");
report.generate();
report.save("report.txt");
return 0;
}
在这个设计中,Report
类承担了两个职责:生成报告和保存报告。如果以后需要修改保存报告的方式,就需要修改Report
类,违反了单一职责原则。
为了遵循单一职责原则,我们可以将生成报告和保存报告的职责分离到不同的类中:
cpp
#include <iostream>
#include <string>
#include <fstream>
// Report类,只负责生成报告
class Report {
public:
Report(const std::string& content) : content(content) {}
void generate() const {
std::cout << "Generating report: " << content << std::endl;
}
const std::string& getContent() const {
return content;
}
private:
std::string content;
};
// ReportSaver类,只负责保存报告
class ReportSaver {
public:
void save(const Report& report, const std::string& filename) const {
std::ofstream file(filename);
if (file.is_open()) {
file << report.getContent();
file.close();
std::cout << "Report saved to " << filename << std::endl;
} else {
std::cerr << "Failed to open file " << filename << std::endl;
}
}
};
int main() {
Report report("This is the report content");
report.generate();
ReportSaver saver;
saver.save(report, "report.txt");
return 0;
}
在这个设计中,Report
类只负责生成报告,而ReportSaver
类负责保存报告。这种方式下,每个类只有一个职责,如果需要修改保存报告的方式,只需要修改ReportSaver
类即可,而不需要修改Report
类,从而遵循了单一职责原则。
Liskov替换原则
Liskov替换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一项基本原则。该原则的核心思想是:如果 S 是 T 的一个子类型,那么类型为 T 的对象可以被替换为类型为 S 的对象,而不会改变程序的正确性。这意味着子类应该可以替换其基类而不影响程序的行为。
为了更好地理解这一原则,我们通过一个C++的例子来说明。
假设我们有一个基类 Rectangle
和一个子类 Square
。如果 Square
继承 Rectangle
但未遵循 Liskov 替换原则,则可能会出现问题。
cpp
#include <iostream>
class Rectangle {
public:
virtual void setWidth(double w) {
width = w;
}
virtual void setHeight(double h) {
height = h;
}
virtual double getWidth() const {
return width;
}
virtual double getHeight() const {
return height;
}
double getArea() const {
return width * height;
}
protected:
double width = 0;
double height = 0;
};
class Square : public Rectangle {
public:
void setWidth(double w) override {
width = w;
height = w; // 保证宽度等于高度
}
void setHeight(double h) override {
height = h;
width = h; // 保证宽度等于高度
}
};
void processRectangle(Rectangle& r) {
r.setWidth(5);
r.setHeight(10);
std::cout << "Expected area: 50, Actual area: " << r.getArea() << std::endl;
}
int main() {
Rectangle rect;
Square square;
processRectangle(rect);
processRectangle(square); // 这里会导致错误输出
return 0;
}
在这个例子中,processRectangle
函数期望传入一个 Rectangle
对象,并将宽度设为5,高度设为10,从而期望面积为50。然而,当我们传入 Square
对象时,由于 Square
的 setWidth
和 setHeight
方法强制宽度和高度相等,这会导致 getArea
返回 100,而不是预期的 50。这违反了 Liskov 替换原则,因为 Square
对象不能替代 Rectangle
对象而不改变程序的行为。
为了遵循 Liskov 替换原则,我们需要确保子类 Square
完全遵循基类 Rectangle
的行为契约。一个更好的设计是不要让 Square
继承 Rectangle
,而是将它们设计为独立的类,或者使用组合而不是继承:
cpp
#include <iostream>
class Shape {
public:
virtual double getArea() const = 0;
virtual ~Shape() = default;
};
class Rectangle : public Shape {
public:
void setWidth(double w) {
width = w;
}
void setHeight(double h) {
height = h;
}
double getWidth() const {
return width;
}
double getHeight() const {
return height;
}
double getArea() const override {
return width * height;
}
private:
double width = 0;
double height = 0;
};
class Square : public Shape {
public:
void setSide(double s) {
side = s;
}
double getSide() const {
return side;
}
double getArea() const override {
return side * side;
}
private:
double side = 0;
};
void processShape(Shape& shape) {
std::cout << "Area: " << shape.getArea() << std::endl;
}
int main() {
Rectangle rect;
rect.setWidth(5);
rect.setHeight(10);
Square square;
square.setSide(7);
processShape(rect); // 处理矩形
processShape(square); // 处理正方形
return 0;
}
在这个设计中,Rectangle
和 Square
都继承自抽象基类 Shape
,并且都实现了 getArea
方法。这样,Rectangle
和 Square
是独立的类,且它们的行为契约各自独立,不会互相影响,从而遵循了 Liskov 替换原则。
接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计中的一项基本原则。该原则的核心思想是:不应强迫客户依赖它们不使用的方法。也就是说,一个类对另一个类的依赖应该建立在最小接口之上。
为了更好地理解这一原则,我们通过一个C++的例子来说明。
假设我们有一个打印机(Printer)接口,它包含打印(print)、扫描(scan)和传真(fax)功能。如果一个具体的打印机类不支持传真功能,但它仍然需要实现这个接口,那么这违反了接口隔离原则。
cpp
#include <iostream>
// 打印机接口
class IPrinter {
public:
virtual void print() = 0;
virtual void scan() = 0;
virtual void fax() = 0; // 这个方法并不是所有打印机都需要
virtual ~IPrinter() = default;
};
// 普通打印机类
class SimplePrinter : public IPrinter {
public:
void print() override {
std::cout << "Printing document..." << std::endl;
}
void scan() override {
std::cout << "Scanning document..." << std::endl;
}
void fax() override {
// 这个打印机不支持传真功能,但仍需要实现这个方法
std::cout << "Fax not supported." << std::endl;
}
};
void processPrinter(IPrinter& printer) {
printer.print();
printer.scan();
printer.fax();
}
int main() {
SimplePrinter simplePrinter;
processPrinter(simplePrinter);
return 0;
}
在这个设计中,SimplePrinter
类不支持传真功能,但它仍然需要实现 IPrinter
接口的 fax
方法。这违反了接口隔离原则,因为 SimplePrinter
被强制实现了它不需要的方法。
为了遵循接口隔离原则,我们可以将 IPrinter
接口拆分为多个更小的接口,每个接口只包含一个功能。这样,具体的类只需要实现它们实际需要的接口。
cpp
#include <iostream>
// 打印接口
class IPrint {
public:
virtual void print() = 0;
virtual ~IPrint() = default;
};
// 扫描接口
class IScan {
public:
virtual void scan() = 0;
virtual ~IScan() = default;
};
// 传真接口
class IFax {
public:
virtual void fax() = 0;
virtual ~IFax() = default;
};
// 普通打印机类,只实现打印和扫描接口
class SimplePrinter : public IPrint, public IScan {
public:
void print() override {
std::cout << "Printing document..." << std::endl;
}
void scan() override {
std::cout << "Scanning document..." << std::endl;
}
};
// 多功能打印机类,实现所有接口
class MultiFunctionPrinter : public IPrint, public IScan, public IFax {
public:
void print() override {
std::cout << "Printing document..." << std::endl;
}
void scan() override {
std::cout << "Scanning document..." << std::endl;
}
void fax() override {
std::cout << "Sending fax..." << std::endl;
}
};
void processPrint(IPrint& printer) {
printer.print();
}
void processScan(IScan& scanner) {
scanner.scan();
}
void processFax(IFax& faxer) {
faxer.fax();
}
int main() {
SimplePrinter simplePrinter;
MultiFunctionPrinter mfp;
processPrint(simplePrinter);
processScan(simplePrinter);
// processFax(simplePrinter); // 这行编译时会报错,因为SimplePrinter没有实现IFax接口
processPrint(mfp);
processScan(mfp);
processFax(mfp);
return 0;
}
在这个设计中,IPrint
、IScan
和 IFax
接口被拆分成更小的接口,具体的类只实现它们实际需要的接口。SimplePrinter
类只实现 IPrint
和 IScan
接口,而 MultiFunctionPrinter
类实现所有三个接口。这样,具体的类只需要依赖它们实际需要的方法,从而遵循了接口隔离原则
面向对象优先使用对象组合,而不是类继承。
面向对象设计中,优先使用对象组合而不是类继承是为了提高代码的灵活性和可维护性。组合通过包含其他对象来实现其功能,而不是通过继承父类的方法和属性。组合比继承更能应对需求变化,因为它允许在运行时动态地改变对象的行为。
下面通过一个C++例子来说明这一原则。
假设我们需要设计一个具有不同显示方式的文本处理系统,可以通过继承方式来实现:
cpp
#include <iostream>
#include <string>
// 基类
class Text {
public:
Text(const std::string& content) : content(content) {}
virtual void display() const {
std::cout << content << std::endl;
}
protected:
std::string content;
};
// 派生类:HTML显示
class HtmlText : public Text {
public:
HtmlText(const std::string& content) : Text(content) {}
void display() const override {
std::cout << "<html>" << content << "</html>" << std::endl;
}
};
// 派生类:Markdown显示
class MarkdownText : public Text {
public:
MarkdownText(const std::string& content) : Text(content) {}
void display() const override {
std::cout << "**" << content << "**" << std::endl;
}
};
int main() {
HtmlText htmlText("Hello, World!");
MarkdownText markdownText("Hello, World!");
htmlText.display();
markdownText.display();
return 0;
}
在上述例子中,通过继承来实现不同显示方式的文本。但是,如果我们需要添加更多的显示方式或者改变现有的显示方式,这种方法会导致类数量迅速增加,并且每次都需要创建新的子类。
为了解决这个问题,我们可以使用组合而不是继承。通过组合,我们可以将显示方式分离到独立的策略类中,这样可以更灵活地组合不同的行为。
cpp
#include <iostream>
#include <string>
#include <memory>
// 显示策略接口
class DisplayStrategy {
public:
virtual void display(const std::string& content) const = 0;
virtual ~DisplayStrategy() = default;
};
// HTML显示策略
class HtmlDisplay : public DisplayStrategy {
public:
void display(const std::string& content) const override {
std::cout << "<html>" << content << "</html>" << std::endl;
}
};
// Markdown显示策略
class MarkdownDisplay : public DisplayStrategy {
public:
void display(const std::string& content) const override {
std::cout << "**" << content << "**" << std::endl;
}
};
// 普通文本显示策略
class PlainTextDisplay : public DisplayStrategy {
public:
void display(const std::string& content) const override {
std::cout << content << std::endl;
}
};
// 文本类
class Text {
public:
Text(const std::string& content, std::shared_ptr<DisplayStrategy> strategy)
: content(content), strategy(strategy) {}
void setDisplayStrategy(std::shared_ptr<DisplayStrategy> newStrategy) {
strategy = newStrategy;
}
void display() const {
strategy->display(content);
}
private:
std::string content;
std::shared_ptr<DisplayStrategy> strategy;
};
int main() {
auto plainText = std::make_shared<PlainTextDisplay>();
auto htmlText = std::make_shared<HtmlDisplay>();
auto markdownText = std::make_shared<MarkdownDisplay>();
Text text("Hello, World!", plainText);
text.display();
text.setDisplayStrategy(htmlText);
text.display();
text.setDisplayStrategy(markdownText);
text.display();
return 0;
}
在这个设计中,DisplayStrategy
接口和它的实现类(HtmlDisplay
、MarkdownDisplay
和 PlainTextDisplay
)独立于 Text
类。通过组合,Text
类持有一个 DisplayStrategy
对象,可以在运行时动态地改变显示策略。这种设计提高了代码的灵活性和可维护性,符合优先使用对象组合而不是类继承的原则。
封装变化点
封装变化点是面向对象设计中的一个重要原则,它强调将可能变化的部分与稳定的部分隔离开来,从而使系统更具弹性和可维护性。通过封装变化点,可以在系统的其他部分不受影响的情况下进行修改和扩展。
下面通过一个C++例子来说明封装变化点的原则。假设我们需要设计一个支付系统,支持多种支付方式(例如信用卡支付和现金支付)。如果未来需要添加新的支付方式,我们希望对现有系统的修改尽可能小。
首先,让我们看看一个没有封装变化点的设计:
cpp
#include <iostream>
#include <string>
class PaymentProcessor {
public:
void processPayment(const std::string& method, double amount) {
if (method == "credit_card") {
std::cout << "Processing credit card payment of $" << amount << std::endl;
// 信用卡支付逻辑
} else if (method == "cash") {
std::cout << "Processing cash payment of $" << amount << std::endl;
// 现金支付逻辑
} else {
std::cout << "Unknown payment method" << std::endl;
}
}
};
int main() {
PaymentProcessor processor;
processor.processPayment("credit_card", 100.0);
processor.processPayment("cash", 50.0);
return 0;
}
在这个设计中,如果我们需要添加新的支付方式(例如移动支付),我们必须修改 PaymentProcessor
类的 processPayment
方法。这违反了封装变化点的原则。
为了封装变化点,我们可以使用策略模式将支付方式的变化封装到独立的类中:
cpp
#include <iostream>
#include <memory>
#include <string>
// 支付策略接口
class PaymentStrategy {
public:
virtual void pay(double amount) const = 0;
virtual ~PaymentStrategy() = default;
};
// 信用卡支付策略
class CreditCardPayment : public PaymentStrategy {
public:
void pay(double amount) const override {
std::cout << "Processing credit card payment of $" << amount << std::endl;
// 信用卡支付逻辑
}
};
// 现金支付策略
class CashPayment : public PaymentStrategy {
public:
void pay(double amount) const override {
std::cout << "Processing cash payment of $" << amount << std::endl;
// 现金支付逻辑
}
};
// 移动支付策略
class MobilePayment : public PaymentStrategy {
public:
void pay(double amount) const override {
std::cout << "Processing mobile payment of $" << amount << std::endl;
// 移动支付逻辑
}
};
// 支付处理器类
class PaymentProcessor {
public:
void setPaymentStrategy(std::shared_ptr<PaymentStrategy> strategy) {
this->strategy = strategy;
}
void processPayment(double amount) const {
if (strategy) {
strategy->pay(amount);
} else {
std::cout << "Payment strategy not set" << std::endl;
}
}
private:
std::shared_ptr<PaymentStrategy> strategy;
};
int main() {
PaymentProcessor processor;
auto creditCardPayment = std::make_shared<CreditCardPayment>();
auto cashPayment = std::make_shared<CashPayment>();
auto mobilePayment = std::make_shared<MobilePayment>();
processor.setPaymentStrategy(creditCardPayment);
processor.processPayment(100.0);
processor.setPaymentStrategy(cashPayment);
processor.processPayment(50.0);
processor.setPaymentStrategy(mobilePayment);
processor.processPayment(75.0);
return 0;
}
在这个设计中,PaymentStrategy
接口和具体的支付策略类(CreditCardPayment
、CashPayment
和 MobilePayment
)封装了支付方式的变化。PaymentProcessor
类通过组合的方式使用这些策略类,可以在运行时动态地改变支付策略。
这种设计遵循了封装变化点的原则,使得添加新的支付方式变得简单,不需要修改现有的 PaymentProcessor
类,只需创建新的策略类并在需要时进行设置即可。这大大提高了系统的灵活性和可维护性。
针对接口编程,而不是针对实现编程
针对接口编程而不是针对实现编程是面向对象设计中的一个重要原则,它强调程序应该依赖于抽象接口而不是具体实现。这种方式可以提高代码的灵活性和可扩展性,使得系统更容易适应变化。
在C++中,可以通过抽象基类(接口类)和多态来实现针对接口编程的设计。以下是一个简单的例子:
cpp
#include <iostream>
#include <memory> // 使用智能指针需要包含头文件
// 抽象接口类
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
// 具体实现类:圆形
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Circle::draw()" << std::endl;
}
};
// 具体实现类:矩形
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Rectangle::draw()" << std::endl;
}
};
// 客户端代码,针对接口编程
void drawShapes(const std::shared_ptr<Shape>& shape) {
shape->draw();
}
int main() {
// 使用圆形
std::shared_ptr<Shape> circle = std::make_shared<Circle>();
drawShapes(circle);
// 使用矩形
std::shared_ptr<Shape> rectangle = std::make_shared<Rectangle>();
drawShapes(rectangle);
return 0;
}
在上述例子中,Shape
是一个抽象基类,定义了一个纯虚函数 draw()
,表示所有形状类都必须实现绘制操作。Circle
和 Rectangle
是具体的实现类,分别实现了 Shape
接口。在 main
函数中,我们使用智能指针 std::shared_ptr<Shape>
来管理 Circle
和 Rectangle
的实例,并通过 drawShapes
函数调用它们的 draw()
方法。
这种设计方式强调了程序依赖于抽象接口 Shape
,而不是具体的 Circle
或 Rectangle
类。如果以后需要添加新的形状,只需创建新的类并继承自 Shape
,实现 draw()
方法即可,而不需要修改现有的 drawShapes
函数或客户端代码。这种灵活性和可扩展性是通过针对接口编程而实现的。