设计模式2-面向对象设计原则

设计模式-面向对象的设计原则

  • 依赖倒置原则
  • 开闭封闭原则
  • 单一职责原则
  • Liskov替换原则
  • 接口隔离原则
  • 面向对象优先使用对象组合,而不是类继承。
  • 封装变化点
  • 针对接口编程,而不是针对实现编程

变化是复用的天地。面向对象设计最大的优势在于抵御变化。

重新认识面向对象。
理解隔离变化
从宏观层面来看,面向对象的构建方式更能适应软件的变化。将变化所带来的影响减为最小。

各司其职
从微观层面来看面,面向对象的方式更强调这个类的责任。由于需求变化导致的增类型不应该影响原来类型的实现

设计原则大于设计模式。可以通过设计原则发明设计模式.

依赖倒置原则

依赖倒置原则(Dependency Inversion Principle, DIP) 是面向对象编程和软件设计领域中的一项重要原则,它是SOLID原则(单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则和依赖倒置原则)中的一部分。这个原则的主要目标是减少代码之间的耦合性,提高系统的可维护性和可扩展性。

依赖倒置原则的主要思想有以下几点:

高层模块不应该依赖于低层模块,二者都应该依赖于抽象:这里的“高层”和“低层”是逻辑上的概念,不是物理或架构上的。高层模块通常指的是调用其他模块或服务的模块,而低层模块则是被调用的模块或服务。这个原则要求我们在编写代码时,应该尽量依赖于接口或抽象类,而不是具体的实现类。

抽象不应该依赖于细节,细节应该依赖于抽象:这意味着我们在设计系统时,应该首先定义出抽象接口或类,然后再根据这些接口或类去编写具体的实现。这样做的好处是,我们可以随时更换具体的实现,而不会影响到高层模块。

依赖倒置原则的实现方式主要有以下几种:
使用接口或抽象类:在Java、C#等语言中,我们可以使用接口或抽象类来定义出系统的抽象部分,然后再根据这些接口或抽象类去编写具体的实现类。
通过配置文件、注解等方式注入依赖:这种方式常见于一些框架中,如Spring、Hibernate等。这些框架通过读取配置文件或注解来动态地注入依赖,从而实现了高层模块和低层模块之间的解耦。
使用依赖注入框架:依赖注入框架(如Google Guice、Dagger等)可以自动地管理对象之间的依赖关系,从而减少了我们手动编写依赖代码的工作量。
总之,依赖倒置原则是一种非常重要的编程思想,它可以帮助我们编写出更加灵活、可维护、可扩展的代码。在实际开发中,我们应该尽量遵循这个原则,以提高系统的质量和效率。

下面是一个简单的C++例子来展示依赖倒置原则的应用。

问题描述
假设我们有一个简单的应用程序,需要发送消息。我们有两种消息发送方式:通过电子邮件和通过短信。传统设计中,高层模块直接依赖于具体的消息发送类,这违反了依赖倒置原则。

违反DIP的设计

#include <iostream>
#include <string>// 具体的电子邮件发送类
class EmailSender {
public:void sendEmail(const std::string& message) {std::cout << "Sending email: " << message << std::endl;}
};// 具体的短信发送类
class SMSSender {
public:void sendSMS(const std::string& message) {std::cout << "Sending SMS: " << message << std::endl;}
};// 高层模块,直接依赖于具体的消息发送类
class Notification {EmailSender emailSender;SMSSender smsSender;public:void notify(const std::string& message) {emailSender.sendEmail(message);smsSender.sendSMS(message);}
};int main() {Notification notification;notification.notify("Hello, World!");return 0;
}

在上述设计中,Notification 类直接依赖于具体的 EmailSenderSMSSender 类。如果我们需要增加新的消息发送方式,必须修改 Notification 类,这违反了开放封闭原则(Open/Closed Principle)。

遵循DIP的设计

#include <iostream>
#include <string>
#include <memory>
#include <vector>// 抽象的消息发送接口
class IMessageSender {
public:virtual ~IMessageSender() = default;virtual void sendMessage(const std::string& message) = 0;
};// 具体的电子邮件发送类,实现了抽象接口
class EmailSender : public IMessageSender {
public:void sendMessage(const std::string& message) override {std::cout << "Sending email: " << message << std::endl;}
};// 具体的短信发送类,实现了抽象接口
class SMSSender : public IMessageSender {
public:void sendMessage(const std::string& message) override {std::cout << "Sending SMS: " << message << std::endl;}
};// 高层模块,依赖于抽象的消息发送接口
class Notification {std::vector<std::shared_ptr<IMessageSender>> senders;public:void addSender(std::shared_ptr<IMessageSender> sender) {senders.push_back(sender);}void notify(const std::string& message) {for (auto& sender : senders) {sender->sendMessage(message);}}
};int main() {Notification notification;notification.addSender(std::make_shared<EmailSender>());notification.addSender(std::make_shared<SMSSender>());notification.notify("Hello, World!");return 0;
}

解释

  1. 抽象接口:我们定义了一个抽象接口 IMessageSender,它有一个纯虚函数 sendMessage。具体的消息发送类(EmailSenderSMSSender)都实现了这个接口。

  2. 高层模块依赖于抽象Notification 类依赖于 IMessageSender 抽象接口,而不是具体的实现类。它通过 addSender 方法动态添加消息发送方式。

  3. 灵活扩展:如果将来需要增加新的消息发送方式,只需创建一个新的实现 IMessageSender 接口的类,并在 main 函数中将其添加到 Notification 类中,而不需要修改 Notification 类的代码。

通过这种方式,我们遵循了依赖倒置原则,使得高层模块不依赖于低层模块的具体实现,而是依赖于抽象接口。这种设计更具灵活性和可维护性。

开闭封闭原则

开放封闭原则(Open-Closed Principle,OCP)是面向对象设计中的一项基本原则。该原则的核心思想是:软件实体(如类、模块和函数等)应该对扩展开放,而对修改封闭。这意味着在不修改现有代码的情况下,可以通过添加新代码来改变模块的行为。

下面用一个简单的C++例子来解释开放封闭原则。

假设我们有一个形状(Shape)类及其子类矩形(Rectangle)和圆形(Circle),并且我们想要计算这些形状的面积。如果不遵循开放封闭原则,可能会写成这样:

#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函数,这违反了开放封闭原则。为了遵循开放封闭原则,我们可以将形状设计成多态类,通过继承和虚函数来实现扩展:

#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类是一个抽象基类,定义了一个纯虚函数calculateAreaRectangleCircleTriangle类分别继承自Shape并实现了calculateArea函数。如果要添加新的形状,只需要创建一个新的派生类并实现calculateArea函数,而不需要修改现有的代码,这样就遵循了开放封闭原则。

单一职责原则

单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中的一项基本原则。该原则的核心思想是:一个类应该只有一个引起它变化的原因,换句话说,一个类应该仅有一个职责(责任)。如果一个类承担了多个职责,这些职责就会耦合在一起,导致变化的影响面扩大。

下面通过一个简单的C++例子来解释单一职责原则。

假设我们有一个报告(Report)类,它包含生成报告和保存报告的功能。如果不遵循单一职责原则,可能会写成这样:

#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类,违反了单一职责原则。

为了遵循单一职责原则,我们可以将生成报告和保存报告的职责分离到不同的类中:

#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 替换原则,则可能会出现问题。

#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 对象时,由于 SquaresetWidthsetHeight 方法强制宽度和高度相等,这会导致 getArea 返回 100,而不是预期的 50。这违反了 Liskov 替换原则,因为 Square 对象不能替代 Rectangle 对象而不改变程序的行为。

为了遵循 Liskov 替换原则,我们需要确保子类 Square 完全遵循基类 Rectangle 的行为契约。一个更好的设计是不要让 Square 继承 Rectangle,而是将它们设计为独立的类,或者使用组合而不是继承:

#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;
}

在这个设计中,RectangleSquare 都继承自抽象基类 Shape,并且都实现了 getArea 方法。这样,RectangleSquare 是独立的类,且它们的行为契约各自独立,不会互相影响,从而遵循了 Liskov 替换原则。

接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计中的一项基本原则。该原则的核心思想是:不应强迫客户依赖它们不使用的方法。也就是说,一个类对另一个类的依赖应该建立在最小接口之上。

为了更好地理解这一原则,我们通过一个C++的例子来说明。

假设我们有一个打印机(Printer)接口,它包含打印(print)、扫描(scan)和传真(fax)功能。如果一个具体的打印机类不支持传真功能,但它仍然需要实现这个接口,那么这违反了接口隔离原则。

#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 接口拆分为多个更小的接口,每个接口只包含一个功能。这样,具体的类只需要实现它们实际需要的接口。

#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;
}

在这个设计中,IPrintIScanIFax 接口被拆分成更小的接口,具体的类只实现它们实际需要的接口。SimplePrinter 类只实现 IPrintIScan 接口,而 MultiFunctionPrinter 类实现所有三个接口。这样,具体的类只需要依赖它们实际需要的方法,从而遵循了接口隔离原则

面向对象优先使用对象组合,而不是类继承。

面向对象设计中,优先使用对象组合而不是类继承是为了提高代码的灵活性和可维护性。组合通过包含其他对象来实现其功能,而不是通过继承父类的方法和属性。组合比继承更能应对需求变化,因为它允许在运行时动态地改变对象的行为。

下面通过一个C++例子来说明这一原则。

假设我们需要设计一个具有不同显示方式的文本处理系统,可以通过继承方式来实现:

#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;
}

在上述例子中,通过继承来实现不同显示方式的文本。但是,如果我们需要添加更多的显示方式或者改变现有的显示方式,这种方法会导致类数量迅速增加,并且每次都需要创建新的子类。

为了解决这个问题,我们可以使用组合而不是继承。通过组合,我们可以将显示方式分离到独立的策略类中,这样可以更灵活地组合不同的行为。

#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 接口和它的实现类(HtmlDisplayMarkdownDisplayPlainTextDisplay)独立于 Text 类。通过组合,Text 类持有一个 DisplayStrategy 对象,可以在运行时动态地改变显示策略。这种设计提高了代码的灵活性和可维护性,符合优先使用对象组合而不是类继承的原则。

封装变化点

封装变化点是面向对象设计中的一个重要原则,它强调将可能变化的部分与稳定的部分隔离开来,从而使系统更具弹性和可维护性。通过封装变化点,可以在系统的其他部分不受影响的情况下进行修改和扩展。

下面通过一个C++例子来说明封装变化点的原则。假设我们需要设计一个支付系统,支持多种支付方式(例如信用卡支付和现金支付)。如果未来需要添加新的支付方式,我们希望对现有系统的修改尽可能小。

首先,让我们看看一个没有封装变化点的设计:

#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 方法。这违反了封装变化点的原则。

为了封装变化点,我们可以使用策略模式将支付方式的变化封装到独立的类中:

#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 接口和具体的支付策略类(CreditCardPaymentCashPaymentMobilePayment)封装了支付方式的变化。PaymentProcessor 类通过组合的方式使用这些策略类,可以在运行时动态地改变支付策略。

这种设计遵循了封装变化点的原则,使得添加新的支付方式变得简单,不需要修改现有的 PaymentProcessor 类,只需创建新的策略类并在需要时进行设置即可。这大大提高了系统的灵活性和可维护性。

针对接口编程,而不是针对实现编程

针对接口编程而不是针对实现编程是面向对象设计中的一个重要原则,它强调程序应该依赖于抽象接口而不是具体实现。这种方式可以提高代码的灵活性和可扩展性,使得系统更容易适应变化。

在C++中,可以通过抽象基类(接口类)和多态来实现针对接口编程的设计。以下是一个简单的例子:

#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(),表示所有形状类都必须实现绘制操作。CircleRectangle 是具体的实现类,分别实现了 Shape 接口。在 main 函数中,我们使用智能指针 std::shared_ptr<Shape> 来管理 CircleRectangle 的实例,并通过 drawShapes 函数调用它们的 draw() 方法。

这种设计方式强调了程序依赖于抽象接口 Shape,而不是具体的 CircleRectangle 类。如果以后需要添加新的形状,只需创建新的类并继承自 Shape,实现 draw() 方法即可,而不需要修改现有的 drawShapes 函数或客户端代码。这种灵活性和可扩展性是通过针对接口编程而实现的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/31950.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

odoo 翻译字段sql查询语句

字段写法&#xff1a; name->>en_US 任务&#xff1a; 查询name字段中&#xff0c;包含ring的数据 SQL模糊查询 SELECT * FROM product_public_category WHERE name->>en_US LIKE %ring%; SQL精准查询 SELECT * FROM product_public_category WHERE name->…

深入解析MVC架构(Model-View-Controller Architecture)

目录 前言1. MVC架构概述1.1 模型&#xff08;Model&#xff09;1.1.1 数据管理1.1.2 业务逻辑 1.2 视图&#xff08;View&#xff09;1.2.1 数据展示1.2.2 用户界面设计 1.3 控制器&#xff08;Controller&#xff09;1.3.1 用户输入处理1.3.2 更新模型和视图 2. MVC架构的优缺…

主干网络篇 | YOLOv5/v7 更换主干网络之 ResNet50/ResNet101 | 对比实验必备

主干网络篇 | YOLOv5/v7 更换主干网络之 ResNet50/ResNet101 | 对比实验必备 1. 简介 ResNet 是近年来最受欢迎的深度卷积神经网络架构之一&#xff0c;它以其优异的性能和鲁棒性而著称。ResNet50 和 ResNet101 是 ResNet 家族中最常用的两个模型&#xff0c;它们分别具有 50…

【深度学习】stable-diffusion-webui AUTOMATIC1111 的参数解释翻译

https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Command-Line-Arguments-and-Settings 参数命令值默认值描述-h, --helpNoneFalse显示帮助信息并退出–exit安装后终止–data-dirDATA_DIR./存储所有用户数据的基本路径–configCONFIGconfigs/stable-diffusion/…

易管理工厂设备日志采集工具

免费试用下载: Gitee下载 最新版本 优势: A. 开箱即用. 解压直接运行.不需额外安装. B. 批管理设备. 设备配置均在后台管理. C. 无人值守 客户端自启动,自更新. D. 稳定安全. 架构简单,内存占用小,通过授权访问.

通过python脚本采集网络流量

#!/usr/bin/python # -*- coding:utf-8 -*- psutil模块是一个跨平台的获取进程和系统应用情况&#xff08;CPU&#xff0c;内存&#xff0c;磁盘&#xff0c;网络&#xff0c;传感器&#xff09;的库。 该模块用于系统监控、限制进程资源和运行进程的管理等方面。 网络信息 ps…

反激开关电源输出假负载

1、为何需要假负载&#xff1f; 开关电源芯片的占空比最小不可能做到0%&#xff0c;都有一个最小导通时间&#xff0c;不过最小导通时间&#xff0c;在规格书中&#xff0c;不一定给出来 注意&#xff1a;如果没有最小导通时间&#xff0c;就相当于芯片都停止输出了&#xff…

29-Linux--守护进程

一.基础概念 1.守护进程&#xff1a;精灵进程&#xff0c;在后台为用户提高服务&#xff0c;是一个生存周期长&#xff0c;通常独立于控制终端并且周期性的执行任务火处理事件发生 2.ps axj&#xff1a;查看守护进程 3.进程组&#xff1a;多个进程的集合&#xff0c;由于管理…

Flask之模板

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、模板的基本用法 1.1、创建模板 1.2、模板语法 1.3、渲染模板 二、模板辅助工具 2.1、上下文 2.2、全局对象 2.3、过滤器 2.4、测试…

小米测开二面—80min中核

小米测开二面—80min中核 3.28 无自我介绍直接开问&#xff01;你的第一份实习是一个开发工作你的第二实习为什么又跑到测试了你的第一份实习遇到了哪些挑战你的逆向开发的开发目标是什么&#xff0c;使用了什么工具你最终开发落地是用在了什么方面上&#xff0c;比如机器人路…

什么是云主机?

云主机是新一代的主机租借服务&#xff0c;它整合了高性能服务器与优质网络带宽&#xff0c;有用处理了传统主机租借价格偏高、服务品良莠不齐等缺陷&#xff0c;可全面满意中小企业、个人站长用户对主机租借服务低本钱&#xff0c;高牢靠&#xff0c;易办理的需求。   关于大…

C语言入门系列:可迁移的数据类型

文章目录 1&#xff0c;精确宽度类型(exact-width integer type)2&#xff0c;最小宽度类型&#xff08;minimum width type&#xff09;3&#xff0c;最快的最小宽度类型&#xff08;fast minimum width type&#xff09;4&#xff0c;可以保存指针的整数类型。5&#xff0c; …

编译 CanMV 固件

前言 上一章节中已经搭建好了基于 CanMV 的 C 开发环境&#xff0c;这么一来便可以进行基于 C 语言和 FreeRTOS 的应用开发或者编译基于 MicroPython 语法的应用开发方式所需的 CanMV 固件&#xff0c;本 章就将带领读者体验一下 CanMV 固件的编译流程。 本章分为如下几个小节&…

为什么print语句被Python3遗弃?

在开发和维护python项目的时候发现经常有print语句报错&#xff0c;原因是python3放弃了print语句 print 语句 早就被列在了不可靠的语言特性列表中&#xff0c;例如 Guido 的“Python 之悔”&#xff08;Python Regrets&#xff09;演讲【1】&#xff0c;并计划在 Python 300…

Python期末复习:基础+数据结构

合法的标识符定义规则 以字母或下划线开头&#xff1a; 标识符必须以字母&#xff08;大写或小写&#xff09;或下划线 _ 开头。 后续字符可以是字母、数字或下划线&#xff1a; 后续字符可以是字母&#xff08;大写或小写&#xff09;、数字&#xff08;0-9&#xff09…

Unity 从0开始编写一个技能编辑器_02_Buff系统的Handler

BuffHandler可以是用于处理角色身上buff的Mono类&#xff0c;任何具备跟Buff交互的角色&#xff0c;都要携带这个BuffHandler脚本。如果你的Buff有额外的处理机制&#xff0c;比如互斥Buff&#xff08;如&#xff1a;免疫负面效果的霸体&#xff09;&#xff0c;需要在AddBuff方…

Anthropic AI模型Claude 3.5 Sonnet在Amazon Bedrock上正式可用

Claude 3.5 Sonnet是Anthropic最先进的Claude系列AI模型的新成员&#xff0c;比Claude 3 Opus更智能且价格只有其五分之一 北京——2024年6月21日 亚马逊云科技宣布&#xff0c;Anthropic最新、最强大的模型Claude 3.5 Sonnet现已在Amazon Bedrock上正式可用&#xff0c;该模型…

增强-MIGO物料消耗需要将物料描述写到会计凭证的摘要里面

财务比较闲提的需求&#xff0c;有些物料消耗需要将物料描述写到会计凭证的摘要里面&#xff0c; 找了一下增强点&#xff0c;随便搞了一下&#xff0c;可以了。

20240622 每日AI必读资讯

&#x1f916;力压GPT-4o&#xff01;新王Claude 3.5 Sonnet来了&#xff0c;直接免费可用 - 新模型在推理、知识和编码能力评估方面超越了以前的版本和竞争对手GPT 4o模型&#xff0c;同时其运行速度是Claude 3 Opus的两倍。 - 该模型可在http://Claude.ai和Claude iOS应用上…

Spring Bean 生命周期详解

Spring Bean 生命周期详解 在 Spring 框架中&#xff0c;Bean 的生命周期由 Spring 容器全权管理。了解和掌握 Bean 的生命周期对于使用 Spring 开发稳定且高效的应用程序至关重要。本文将详细介绍 Spring Bean 生命周期的五个主要阶段&#xff1a;实例化、属性注入、初始化、…