最近在一场面试中被问到面向对象的原则,结果笔者张口就来了面向对象的特性,没有get到面试官的意思和对代码规范这一块的期望┭┮﹏┭┮
世人以不得第为耻,吾以不得第动心为耻。问题不大,我们斗罢艰险又出发~
面向对象的核心特性:
- 封装(Encapsulation):封装是指将对象的属性和行为(方法)封装在类内部,外部无法直接访问内部的属性,而是通过公开的方法进行操作。这样可以保护数据,防止外部代码对其进行随意修改。
- 继承(Inheritance):继承允许一个类从另一个类继承属性和方法,形成类与类之间的层次结构。子类不仅可以拥有父类的功能,还可以扩展或重写父类的行为。
- 多态(Polymorphism):多态允许同一个方法在不同对象上表现出不同的行为。多态通常通过方法重载或重写实现,使得程序能够灵活应对不同的对象类型。
- 抽象(Abstraction):抽象是指将对象的共性提取出来,定义为类。抽象允许程序员忽略对象的具体实现,只关心对象的功能。
学习面向对象的设计原则有助于提升代码的可维护性 、可扩展性 和可复用性。这些原则帮助开发者在设计系统时减少类与类之间的耦合、增强模块的独立性,使代码更容易适应需求变化,同时降低了修改时引入错误的风险。通过遵循这些设计原则,开发者能够编写更清晰、灵活、稳定的代码,有效应对复杂的业务场景和长期的软件维护。
现在常说的面向对象的设计原则是经典的SOLID五大原则 + LC(迪米特法则、组合/聚合复用原则)。
1. 单一职责原则(Single Responsibility Principle)
单一职责原则(SRP)的核心思想是一个类应该只有一个职责 ,即它只负责一件事情,或是它应该只有一个引起它变化的原因。如果一个类承担了多种职责,修改其中一个职责的需求可能会影响到其他职责,导致类变得难以维护和扩展。
※ 反例
假设我们正在开发一个简单的订单处理系统,初始设计中,我们将订单的处理和保存放在了同一个类中:
cpp
#include <iostream>
#include <string>
using namespace std;
// 初始设计:一个类同时负责订单的处理和保存
class Order {
public:
void processOrder() {
// 处理订单
cout << "Processing the order..." << endl;
}
void saveOrderToDatabase() {
// 将订单保存到数据库
cout << "Saving order to database..." << endl;
}
};
int main() {
Order order;
order.processOrder();
order.saveOrderToDatabase();
return 0;
}
※ 问题
在这个设计中,Order
类既负责订单的处理(processOrder
),又负责将订单保存到数据库(saveOrderToDatabase
)。如果数据库的逻辑发生了变化,我们可能需要修改Order
类,这导致该类的职责过多,增加了代码维护的复杂性。
※ 正例
我们可以将订单的处理和保存职责分离成两个不同的类,一个负责订单的处理,另一个负责保存订单数据。
cpp
#include <iostream>
#include <string>
using namespace std;
// 订单处理类,专注于订单处理逻辑
class OrderProcessor {
public:
void processOrder() {
// 处理订单
cout << "Processing the order..." << endl;
}
};
// 订单保存类,专注于保存订单到数据库
class OrderRepository {
public:
void saveOrderToDatabase() {
// 将订单保存到数据库
cout << "Saving order to database..." << endl;
}
};
int main() {
OrderProcessor orderProcessor;
OrderRepository orderRepository;
orderProcessor.processOrder();
orderRepository.saveOrderToDatabase();
return 0;
}
※ 改进后的优点
- 职责分离:
OrderProcessor
类只负责订单的处理,OrderRepository
类只负责订单的保存。它们各自只负责一项职责,遵循了单一职责原则。 - 提高可维护性:如果将来需要改变保存订单的方式(例如从数据库改为保存到文件),我们只需要修改
OrderRepository
类,不需要触碰OrderProcessor
。 - 提高可扩展性:这种设计允许我们灵活扩展处理订单或保存订单的逻辑,而不必担心它们相互影响。
通过遵循单一职责原则,可以让代码更加清晰、易于维护和扩展,减少不必要的耦合。
2. 开放封闭原则(Open-Closed Principle)
开放封闭原则(OCP)要求软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当我们需要为系统添加新功能时,应该通过扩展已有的代码,而不是修改现有的代码,从而减少引入新错误的风险,保持系统的稳定性。
※ 反例
假设我们有一个简单的系统,用于计算不同形状的面积。最初的设计中,我们有一个 Shape
类和 Rectangle
类,Shape
类中有一个 calculateArea
函数来计算不同形状的面积。
cpp
#include <iostream>
#include <string>
using namespace std;
// Shape类负责计算形状的面积
class Shape {
public:
// 根据形状类型计算面积
double calculateArea(const string& shapeType, double width, double height = 0) {
if (shapeType == "rectangle") {
return width * height;
}
// 未来可能添加更多形状
return 0.0;
}
};
int main() {
Shape shape;
double rectangleArea = shape.calculateArea("rectangle", 5.0, 10.0);
cout << "Rectangle Area: " << rectangleArea << endl;
return 0;
}
※ 问题
如果我们想要添加一个新的形状(例如圆形),就必须修改 Shape
类中的 calculateArea
函数,增加新的分支。这违反了开放封闭原则,因为我们需要修改现有的代码来添加新功能。
※ 正例
为了解决这个问题,我们可以使用多态性 来实现。通过定义一个基类 Shape
,并为每种形状创建一个子类,实现基类中的 calculateArea
方法。当需要添加新的形状时,只需扩展一个新的类,而不需要修改原有的代码。
cpp
#include <iostream>
#include <cmath>
using namespace std;
// 抽象基类 Shape,定义接口
class Shape {
public:
virtual double calculateArea() const = 0; // 纯虚函数,定义面积计算接口
virtual ~Shape() {} // 虚析构函数
};
// Rectangle 类,负责矩形面积计算
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() const override {
return width * height;
}
};
// Circle 类,负责圆形面积计算
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double calculateArea() const override {
return M_PI * radius * radius;
}
};
int main() {
Shape* rectangle = new Rectangle(5.0, 10.0);
Shape* circle = new Circle(7.0);
cout << "Rectangle Area: " << rectangle->calculateArea() << endl;
cout << "Circle Area: " << circle->calculateArea() << endl;
delete rectangle;
delete circle;
return 0;
}
※ 改进后的优点
-
符合开放封闭原则 :当我们需要添加新的形状(如圆形、三角形等)时,只需创建一个新的类(例如
Circle
或Triangle
),实现Shape
的calculateArea
方法即可。无需修改Shape
类或其他已有的代码。 -
增强可扩展性:新功能通过扩展类来实现,代码更加灵活且易于维护。以后要增加更多形状,只需扩展新的类,而不需要修改已有类的代码,降低了修改引入错误的风险。
-
代码的模块化和清晰性:每个形状都有自己的类,职责清晰。每个类只负责计算自己对应的形状面积,增强了代码的可读性和可维护性。
这种方式完美体现了开放封闭原则,即通过扩展(增加新类)而不是修改现有代码来添加新功能。
3. 里氏替换原则(Liskov Substitution Principle)
里氏替换原则(LSP)要求在软件设计中,所有引用基类的地方都可以透明地使用其子类的对象,而不影响程序的正确性 。简单来说,**子类必须能够替换父类,且程序行为保持一致。**如果子类破坏了父类的行为约定,那么就违反了里氏替换原则。
- 如果S是T的子类型,则所有T类型的对象可以被替换为S类型的对象,而不会影响程序的正确性。
※ 反例
假设我们设计了一个 Rectangle
(矩形)类,并继承了一个 Square
(正方形)类。我们可以初步实现它们的类层次结构,展示违反里氏替换原则的情况。
cpp
#include <iostream>
using namespace std;
// 矩形类
class Rectangle {
protected:
double width, height;
public:
void setWidth(double w) { width = w; }
void setHeight(double h) { height = h; }
virtual double getArea() const { return width * height; }
};
// 正方形类,继承自矩形
class Square : public Rectangle {
public:
// 正方形的宽和高应该始终相等
void setWidth(double w) override {
width = w;
height = w; // 正方形的高度和宽度必须相等
}
void setHeight(double h) override {
height = h;
width = h; // 正方形的宽度和高度必须相等
}
};
void printArea(Rectangle& rect) {
rect.setWidth(5);
rect.setHeight(10);
cout << "Area: " << rect.getArea() << endl;
}
int main() {
Rectangle rect;
Square sq;
// 打印矩形的面积
printArea(rect); // 正确输出:面积为50
// 打印正方形的面积
printArea(sq); // 错误输出:面积为100,违反预期
return 0;
}
※ 问题
在这个例子中,Square
继承了 Rectangle
,但它破坏了矩形的行为。调用 printArea
函数时,期望是通过设置宽度和高度来计算面积。但对于 Square
类,它强制宽和高相等,导致面积计算结果错误(应为 50,但返回了 100)。
这违反了里氏替换原则,因为我们期望 Square
能像 Rectangle
一样使用,但它却没有正确遵循矩形的行为约定。
※ 正例
为了满足里氏替换原则,Square
不应该继承自 Rectangle
,因为正方形并不是一种特殊的矩形,它有自己的约束条件。因此,可以将 Square
和 Rectangle
分开处理,而不是通过继承来实现。
cpp
#include <iostream>
using namespace std;
// 矩形类
class Rectangle {
protected:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void setWidth(double w) { width = w; }
void setHeight(double h) { height = h; }
virtual double getArea() const { return width * height; }
};
// 正方形类,独立于矩形类
class Square {
private:
double side;
public:
Square(double s) : side(s) {}
void setSide(double s) { side = s; }
double getArea() const { return side * side; }
};
void printRectangleArea(Rectangle& rect) {
rect.setWidth(5);
rect.setHeight(10);
cout << "Rectangle Area: " << rect.getArea() << endl;
}
void printSquareArea(Square& sq) {
sq.setSide(5);
cout << "Square Area: " << sq.getArea() << endl;
}
int main() {
Rectangle rect(5, 10);
Square sq(5);
// 打印矩形的面积
printRectangleArea(rect); // 正确输出:面积为50
// 打印正方形的面积
printSquareArea(sq); // 正确输出:面积为25
return 0;
}
※ 改进后的优点
- 遵守里氏替换原则 :
Square
和Rectangle
不再有继承关系,因此不会出现Square
不能替换Rectangle
的问题。各自有独立的逻辑,且不干扰对方的行为。 - 清晰的设计 :通过分离
Square
和Rectangle
,我们确保了每个类的行为与它的定义一致,避免了意外的行为修改。
在面向对象设计中,确保子类能替代父类,且不会破坏程序的逻辑,是遵守里氏替换原则的关键。通过设计独立的类而非不合理的继承,可以避免违背这一原则的情况,确保代码的健壮性和可维护性。
4. 接口隔离原则(Interface Segregation Principle)
接口隔离原则(ISP)要求不应该强迫一个类依赖它不需要的接口,即接口应该尽可能小和具体,避免一个接口承担过多的职责。类不应该实现那些它不会使用的方法,从而减少代码的复杂性和实现的负担。
- 将大型接口拆分成更小的、特定功能的接口,让类实现它们真正需要的接口。
- 避免使用"肥接口"(Fat Interfaces),即包含大量无关方法的接口
※ 反例
假设我们在设计一个图形处理程序,不同类型的图形需要实现不同的操作。有些图形只需要绘制,而有些图形可能还需要执行其他操作(如保存或加载)。为了演示接口隔离原则的作用,首先我们来看看一个违反接口隔离原则的设计。
cpp
#include <iostream>
using namespace std;
// 定义一个大型的图形操作接口,包含多个方法
class IShape {
public:
virtual void draw() = 0; // 绘制图形
virtual void saveToFile() = 0; // 将图形保存到文件
virtual void loadFromFile() = 0; // 从文件加载图形
};
// 实现圆形类
class Circle : public IShape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
void saveToFile() override {
// 圆形不需要保存到文件,但必须实现此方法
cout << "Circle: save to file (not applicable)" << endl;
}
void loadFromFile() override {
// 圆形不需要从文件加载,但必须实现此方法
cout << "Circle: load from file (not applicable)" << endl;
}
};
// 实现图像类
class Image : public IShape {
public:
void draw() override {
cout << "Drawing an image" << endl;
}
void saveToFile() override {
cout << "Saving image to file" << endl;
}
void loadFromFile() override {
cout << "Loading image from file" << endl;
}
};
int main() {
Circle circle;
Image image;
circle.draw();
circle.saveToFile(); // 圆形没有保存功能,但仍必须实现该方法
image.draw();
image.saveToFile(); // 图像保存到文件
return 0;
}
※ 问题
在这个设计中,IShape
接口包含了所有图形必须实现的方法,如 draw()
、saveToFile()
和 loadFromFile()
。然而,某些图形(如 Circle
)不需要保存和加载文件,但因为 IShape
接口要求实现这些方法,圆形类被迫实现了与它无关的功能。这违反了接口隔离原则,因为 Circle
类依赖了它不需要的接口方法。
※ 正例
为了改进设计,我们可以将一个大的接口分解成多个小的、专注于具体职责的接口。这样,每个类只需实现它真正需要的功能。
cpp
#include <iostream>
using namespace std;
// 定义专门的绘制接口
class IDrawable {
public:
virtual void draw() = 0; // 绘制图形
};
// 定义专门的可保存和加载的接口
class IStorable {
public:
virtual void saveToFile() = 0; // 保存到文件
virtual void loadFromFile() = 0; // 从文件加载
};
// 实现圆形类,只需要绘制功能
class Circle : public IDrawable {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
// 实现图像类,需要绘制和文件操作功能
class Image : public IDrawable, public IStorable {
public:
void draw() override {
cout << "Drawing an image" << endl;
}
void saveToFile() override {
cout << "Saving image to file" << endl;
}
void loadFromFile() override {
cout << "Loading image from file" << endl;
}
};
int main() {
Circle circle;
Image image;
// 绘制图形
circle.draw();
image.draw();
// 图像的保存和加载功能
image.saveToFile();
image.loadFromFile();
return 0;
}
※ 改进后的优点
-
遵守接口隔离原则 :我们将一个大的
IShape
接口分解成了两个更小的接口------IDrawable
负责绘制图形,IStorable
负责保存和加载功能。Circle
类只需要绘制功能,因此只实现了IDrawable
接口,不需要实现与其无关的IStorable
接口。Image
类需要绘制和保存功能,因此实现了两个接口。 -
降低类的实现复杂性:每个类只实现它真正需要的接口方法,避免了类被迫实现不必要的方法,从而降低了代码的复杂性和维护成本。
-
增强系统的灵活性 :通过接口的分离,系统可以更灵活地添加新的功能。例如,如果将来需要实现一种新的存储方式,可以轻松扩展
IStorable
接口,而不会影响IDrawable
接口及其实现类。
接口隔离原则提倡使用小而专注的接口,避免类依赖不需要的功能。通过将接口职责分离到更小、更专一的接口中,系统的灵活性和可维护性都得到了提升。
5. 依赖倒置原则(Dependency Inversion Principle)
依赖倒置原则(DIP)核心思想是高层模块不应该依赖于低层模块,两者都应该依赖于抽象(接口或抽象类)。抽象不应该依赖于具体实现,而是具体实现应该依赖于抽象。
依赖倒置原则的好处是将依赖关系从具体的实现类转移到抽象接口上,从而提高系统的灵活性和可扩展性。可以轻松地替换底层模块,而不需要修改高层模块的代码。
※ 反例
假设我们正在设计一个简单的消息发送系统,最初设计中,高层模块直接依赖于具体的消息发送类(例如Email或SMS)。这种设计违反了依赖倒置原则,因为高层模块直接依赖于低层实现。
cpp
#include <iostream>
#include <string>
using namespace std;
// 具体的Email发送类
class EmailSender {
public:
void sendEmail(const string& message) {
cout << "Sending email: " << message << endl;
}
};
// 业务逻辑类,依赖于具体的EmailSender类
class Notification {
EmailSender emailSender;
public:
void send(const string& message) {
emailSender.sendEmail(message);
}
};
int main() {
Notification notification;
notification.send("Hello, world!");
return 0;
}
※ 问题
在这个设计中,Notification
类直接依赖 EmailSender
,即高层模块(Notification
)依赖于低层模块(EmailSender
)。如果将来我们想使用其他发送方式(如短信SMS发送),我们需要修改 Notification
类的代码,违反了依赖倒置原则。
※ 正例
为了遵守依赖倒置原则,我们可以引入一个抽象接口,让高层模块依赖于接口,而不是依赖于具体实现。通过这种方式,我们可以轻松替换底层模块,而无需修改高层模块的代码。
cpp
#include <iostream>
#include <string>
using namespace std;
// 定义抽象的消息发送接口
class IMessageSender {
public:
virtual void sendMessage(const string& message) = 0; // 纯虚函数
virtual ~IMessageSender() {} // 虚析构函数
};
// Email发送类,实现IMessageSender接口
class EmailSender : public IMessageSender {
public:
void sendMessage(const string& message) override {
cout << "Sending email: " << message << endl;
}
};
// SMS发送类,实现IMessageSender接口
class SmsSender : public IMessageSender {
public:
void sendMessage(const string& message) override {
cout << "Sending SMS: " << message << endl;
}
};
// Notification类依赖于抽象的IMessageSender接口,而不是具体的发送实现类
class Notification {
IMessageSender* sender; // 使用接口类型,而不是具体实现类型
public:
Notification(IMessageSender* sender) : sender(sender) {}
void send(const string& message) {
sender->sendMessage(message); // 通过接口调用具体的发送方法
}
};
int main() {
// 使用EmailSender发送消息
IMessageSender* emailSender = new EmailSender();
Notification emailNotification(emailSender);
emailNotification.send("Hello, email world!");
// 使用SmsSender发送消息
IMessageSender* smsSender = new SmsSender();
Notification smsNotification(smsSender);
smsNotification.send("Hello, SMS world!");
delete emailSender;
delete smsSender;
return 0;
}
※ 改进后的优点
-
符合依赖倒置原则 :
Notification
类现在依赖于抽象的IMessageSender
接口,而不是具体的EmailSender
或SmsSender
实现。这使得高层模块与低层实现解耦,可以轻松地替换不同的消息发送方式,而不需要修改高层代码。 -
增强系统的灵活性 :我们可以通过实现不同的消息发送类(如
EmailSender
、SmsSender
)来扩展功能,而不必改变Notification
类的实现。可以轻松添加新类型的发送方式,比如通过社交媒体发送,而不影响现有代码。 -
降低耦合性 :高层模块(如
Notification
)不再依赖具体的低层实现类,而是依赖于一个通用的接口。这减少了代码的耦合性,提高了代码的可维护性和可扩展性。 -
测试性更强:由于依赖于抽象接口,我们可以轻松地为系统编写单元测试,使用模拟对象来替代实际的消息发送对象,从而提高测试的效率和质量。
依赖倒置原则通过让高层模块依赖于抽象,而不是具体的实现类,减少了系统的耦合性,提高了扩展性和可维护性。通过遵循这一原则,系统可以更容易地进行功能扩展和模块替换,而不会影响高层逻辑。
6. 迪米特法则(Law of Demeter)
迪米特法则(LoD),又称为最少知道原则,其核心思想是一个对象应当尽可能少地了解其他对象的内部细节。具体来说,一个对象只能与直接相关的对象进行通信,不应该与它不直接相关的对象打交道,也不应该通过一个对象去调用另一个对象的方法。这样可以降低类之间的耦合性,使得系统更易维护和扩展。
迪米特法则的规则:
- 一个对象应该只与它的直接朋友通信。
- 一个对象的"朋友"包括:
- 它自己(self)
- 它的成员变量
- 参数
- 它的返回值
- 它的父类
- 它的子类
- 不应该通过朋友去访问朋友的朋友,即避免链式调用。
※ 反例
我们假设有一个设计,用户类 User
通过一个订单类 Order
来获取订单细节,同时订单又依赖于另一个类 Address
来获取配送地址。这里,User
直接访问了 Order
对象的 Address
,这违反了迪米特法则,因为 User
不应该直接操控 Address
。
cpp
#include <iostream>
#include <string>
using namespace std;
// 地址类
class Address {
public:
string getFullAddress() const {
return "123 Main Street";
}
};
// 订单类
class Order {
Address address;
public:
Address& getAddress() {
return address;
}
};
// 用户类
class User {
public:
void printShippingAddress(Order& order) {
// 直接访问Order的Address对象,违反了迪米特法则
cout << "Shipping address: " << order.getAddress().getFullAddress() << endl;
}
};
int main() {
User user;
Order order;
user.printShippingAddress(order); // 输出订单的配送地址
return 0;
}
※ 问题
在这个例子中,User
直接通过 Order
调用了 Address
的方法。这意味着 User
类知道了 Order
的内部结构(Order
包含一个 Address
对象)。如果未来 Order
的实现发生变化,例如 Address
的实现更改或移除,这将导致 User
类必须修改,这显然违反了迪米特法则。
※ 正例
为了遵守迪米特法则,我们应该让 User
只与 Order
类直接交互,而不要让它知道 Order
的内部细节,即 Address
类。可以通过在 Order
类中添加一个获取地址的方法,将 Address
的细节隐藏起来。
cpp
#include <iostream>
#include <string>
using namespace std;
// 地址类
class Address {
public:
string getFullAddress() const {
return "123 Main Street";
}
};
// 订单类
class Order {
Address address;
public:
// 提供一个获取完整地址的接口,隐藏Address的细节
string getShippingAddress() const {
return address.getFullAddress();
}
};
// 用户类
class User {
public:
void printShippingAddress(const Order& order) {
// User只与Order交互,不需要了解Address的存在
cout << "Shipping address: " << order.getShippingAddress() << endl;
}
};
int main() {
User user;
Order order;
user.printShippingAddress(order); // 输出订单的配送地址
return 0;
}
※ 改进后的优点
-
符合迪米特法则 :
User
类不再直接访问Address
类,而是通过Order
提供的接口getShippingAddress()
来获取配送地址。这使得User
类与Order
类直接交互,降低了类之间的耦合性。 -
隐藏细节,增强封装性 :
Order
类内部的Address
实现细节被隐藏起来。将来即使Order
类的实现发生变化(比如Address
的具体实现改变),只要getShippingAddress
方法的接口不变,User
类就不需要修改。 -
减少类之间的耦合 :通过减少类之间的直接依赖,我们可以让代码更加灵活,易于维护和扩展。如果未来添加新的订单类型或配送地址类型,不需要修改
User
类的代码。
迪米特法则的核心是通过减少类之间的耦合,使得系统更加稳定和易于维护。通过让类只与它直接相关的类交互,避免对其他类的内部结构产生依赖,代码的扩展性和维护性大大增强。这一设计原则可以有效减少系统中的联动修改,保持代码的简洁性和模块化。
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle)
组合/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)的核心思想是,尽量使用对象的组合或聚合,而不是通过继承来实现代码的复用。通过组合或聚合,可以在运行时动态地将功能组合在一起,灵活性更高,而继承则是静态的,且容易引入紧密耦合。
组合与聚合的区别
组合:是一种强关联形式,表示部分和整体的生命周期紧密相连。部分对象无法独立于整体对象存在,当整体对象被销毁时,部分对象也会被销毁。例如,车轮是汽车的一部分,没有汽车就没有车轮的概念。
聚合:是一种弱关联形式,表示部分和整体的生命周期可以独立。部分对象可以属于多个整体对象,也可以独立于整体对象存在。例如,员工可以属于多个项目,即使项目结束,员工依然存在。
※ 反例
我们设计一个图形处理系统,最初的设计中通过继承实现了不同图形的颜色设置。但这种设计增加了类之间的耦合,容易导致继承层次复杂化。
cpp
#include <iostream>
#include <string>
using namespace std;
// 图形基类
class Shape {
public:
virtual void draw() const = 0;
};
// 继承自Shape的圆形类
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing a circle" << endl;
}
};
// 继承自Circle类的有颜色的圆形类
class ColoredCircle : public Circle {
private:
string color;
public:
ColoredCircle(const string& color) : color(color) {}
void draw() const override {
cout << "Drawing a " << color << " circle" << endl;
}
};
int main() {
ColoredCircle redCircle("red");
redCircle.draw(); // 输出:Drawing a red circle
return 0;
}
※ 问题
在这个例子中,我们通过继承 Circle
类来实现 ColoredCircle
类,使其能够绘制带颜色的圆。然而,如果需要给更多形状(例如矩形、三角形)添加颜色,就需要分别创建 ColoredRectangle
、ColoredTriangle
等类。这不仅增加了继承层次的复杂度,还导致代码重复,降低了系统的灵活性和可维护性。
※ 正例
为了遵守组合/聚合复用原则,我们可以将颜色与形状的职责分离,通过组合的方式将颜色应用到各种形状上,而不是使用继承。这使得颜色可以独立于形状复用,提升了代码的可扩展性。
cpp
#include <iostream>
#include <string>
using namespace std;
// 颜色类,负责处理颜色相关功能
class Color {
private:
string color;
public:
Color(const string& color) : color(color) {}
string getColor() const {
return color;
}
};
// 图形基类
class Shape {
public:
virtual void draw() const = 0;
};
// 圆形类,不依赖颜色
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing a circle" << endl;
}
};
// Rectangle 类,不依赖颜色
class Rectangle : public Shape {
public:
void draw() const override {
cout << "Drawing a rectangle" << endl;
}
};
// 组合颜色和形状的类,使用合成来扩展功能
class ColoredShape : public Shape {
private:
Shape* shape;
Color* color;
public:
ColoredShape(Shape* shape, Color* color) : shape(shape), color(color) {}
void draw() const override {
cout << "Drawing a " << color->getColor() << " ";
shape->draw(); // 调用具体形状的绘制方法
}
};
int main() {
Circle circle;
Rectangle rectangle;
Color red("red");
Color blue("blue");
// 通过组合为不同形状添加颜色
ColoredShape redCircle(&circle, &red);
ColoredShape blueRectangle(&rectangle, &blue);
redCircle.draw(); // 输出:Drawing a red circle
blueRectangle.draw(); // 输出:Drawing a blue rectangle
return 0;
}
※ 改进后的优点
-
符合组合/聚合复用原则 :我们使用组合的方式,将
Shape
和Color
结合在一起,而不是通过继承来扩展功能。ColoredShape
通过组合Shape
和Color
来实现形状与颜色的灵活结合,不需要创建专门的ColoredCircle
、ColoredRectangle
等类。 -
提高了代码的灵活性:通过组合,我们可以将任意形状与颜色动态结合在一起,而无需通过继承扩展每种形状和颜色的组合。这大大减少了代码的重复,提升了系统的扩展性。
-
减少了耦合 :形状和颜色的职责被分离,
Shape
只负责图形的绘制,Color
只负责颜色管理。每个类的职责更加明确,修改某个类不会影响到其他类。 -
提高了代码的可复用性 :颜色和形状的复用性提高了,
Color
类可以应用到任何Shape
子类上,而不需要为每个形状单独创建有颜色的子类。
组合/聚合复用原则提倡通过对象组合来实现功能扩展,而不是通过继承。通过使用组合,我们可以创建更加灵活、可扩展的系统,减少类之间的耦合,增强代码的可复用性和可维护性。