C++软件设计模式之享元模式(FlyWeight)
享元(Flyweight)模式的动机与意图
动机
享元模式的主要动机是通过共享对象来减少内存使用,从而提高系统的性能。在某些情况下,系统中可能有大量细粒度的对象,这些对象具有共同的部分状态,而这些状态可以共享。如果不进行共享,大量对象会占用大量的内存资源,特别是在对象的数量非常大的时候。享元模式通过共享这些对象的共有部分状态,使得系统只需要保存一份共有的状态,从而显著减少内存占用。
意图
享元模式的意图是通过共享技术来减少内存使用,并提高对象的创建和管理效率。具体来说,享元模式将一个对象的内部状态(可以共享的)和外部状态(不能共享的)分开,使得一个对象可以被多次共享,而每次使用时只需传递外部状态给对象。
适用场合
享元模式适用于以下场合:
-
对象数量庞大:
- 当一个应用程序中需要创建大量的对象,而这些对象占用大量内存时,可以考虑使用享元模式。通过共享这些对象的共有部分状态,可以显著减少内存占用。
-
对象状态可以分离:
- 对象的状态可以被分为内部状态和外部状态,其中内部状态是可以共享的,而外部状态是随上下文变化的。享元模式通过共享内部状态,同时在使用时传递外部状态,来实现对象的高效管理。
-
对象的大多数状态可以被外部化:
- 如果一个对象的大多数状态可以被外部化(即不保存在对象内部),并且这些状态可以在使用对象时传入,那么这个对象就适合使用享元模式。
-
需要缓存对象:
- 当需要缓存对象以提高访问效率时,可以通过享元模式来实现对象的共享和缓存,从而提高系统的性能。
具体示例
假设我们正在开发一个文本编辑器,需要在文档中显示大量的字符。每个字符对象都包含字体、颜色、大小等属性,如果不进行共享,每个字符的这些属性将占用大量的内存。通过享元模式,我们可以共享字符的字体、颜色和大小等共有部分状态,而每个字符的外部状态(如位置)则在使用时传递给对象。
字符类(内部状态)
#include <iostream>
#include <map>
#include <string>
class Character {
private:
char _value;
public:
Character(char value) : _value(value) {}
void display(const std::string& font, const std::string& color, int size) {
std::cout << "显示字符: " << _value << ",字体: " << font << ",颜色: " << color << ",大小: " << size << std::endl;
}
};
class CharacterFactory {
private:
std::map<char, std::shared_ptr<Character>> _characters;
public:
std::shared_ptr<Character> getCharacter(char value) {
if (_characters.find(value) == _characters.end()) {
std::shared_ptr<Character> newChar = std::make_shared<Character>(value);
_characters[value] = newChar;
return newChar;
} else {
return _characters[value];
}
}
};
文档类(外部状态)
class Document {
private:
std::vector<std::shared_ptr<Character>> _characters;
std::string _font;
std::string _color;
int _size;
public:
Document(const std::string& font, const std::string& color, int size) : _font(font), _color(color), _size(size) {}
void addCharacter(char value, CharacterFactory& factory) {
std::shared_ptr<Character> character = factory.getCharacter(value);
_characters.push_back(character);
}
void display() {
for (auto& character : _characters) {
character->display(_font, _color, _size);
}
}
};
客户端代码
int main() {
CharacterFactory factory;
Document doc1("Arial", "Red", 12);
doc1.addCharacter('A', factory);
doc1.addCharacter('B', factory);
doc1.addCharacter('C', factory);
Document doc2("Times New Roman", "Blue", 14);
doc2.addCharacter('A', factory);
doc2.addCharacter('B', factory);
doc2.addCharacter('D', factory);
doc1.display();
doc2.display();
return 0;
}
代码解释
-
Character 类:
- 代表一个字符对象,包含字符的值
_value
。 display
方法用于显示字符,同时接受字体、颜色和大小等外部状态作为参数。
- 代表一个字符对象,包含字符的值
-
CharacterFactory 类:
- 用于创建和管理字符对象的工厂类。
- 通过
getCharacter
方法,工厂类可以返回一个已存在的字符对象(共享对象),或者创建一个新的字符对象并加入到缓存中。
-
Document 类:
- 代表一个文档,包含字符的集合
_characters
以及字体、颜色和大小等外部状态。 addCharacter
方法用于向文档中添加字符,通过CharacterFactory
获取字符对象。display
方法用于显示文档中的所有字符,同时传递字体、颜色和大小等外部状态给字符对象。
- 代表一个文档,包含字符的集合
-
客户端代码:
- 创建
CharacterFactory
对象。 - 创建两个文档对象
doc1
和doc2
,并分别为它们添加字符。 - 显示两个文档中的字符,每个字符对象的内部状态(值)是共享的,而外部状态(字体、颜色、大小)是在使用时传递的。
- 创建
总结
享元模式的主要动机是通过共享对象来减少内存使用,提高系统的性能。其适用场合包括:
- 对象数量庞大:系统中存在大量细粒度的对象,这些对象占用大量内存。
- 对象状态可以分离:对象的状态可以被分为内部状态和外部状态,其中内部状态是可以共享的。
- 对象的大多数状态可以被外部化:对象的大多数状态可以不在对象内部保存,而是在使用对象时传入。
- 需要缓存对象:通过缓存对象来提高访问效率。
通过享元模式,可以有效地管理和共享对象的共有状态,从而减少内存占用,提高系统性能。希望这些解释能帮助你更好地理解享元模式的动机、意图及其适用场合。
享元模式的 UML 类图
享元模式的 UML 类图如下所示:
+------------------------------+ +-----------------------------+
| FlyweightFactory | | Flyweight |
|------------------------------| |-----------------------------|
| +getInstance(key: String): | | +intrinsicState: String |
| Flyweight | | +operation(extrinsicState: |
| | | String): void |
| +flyweights: Map<String, | | |
| Flyweight> | +-----------------------------+
| +getInstance(key: String): | | +-----------------+
| Flyweight | | | ConcreteFlyweight |
| +addFlyweight(key: String, | | |------------------|
| flyweight: Flyweight): void | | | +intrinsicState: String|
+------------------------------+ | | +operation(extrinsicState: |
| | String): void |
+-----------------------------+ |
+---------+
UML 类图解释
-
FlyweightFactory:
- 职责:负责创建和管理享元对象。
- 方法:
getInstance(key: String): Flyweight
:根据给定的键返回一个享元对象。如果享元对象已经存在于缓存中,则返回缓存中的对象;否则,创建一个新的享元对象并将其加入到缓存中,然后返回。addFlyweight(key: String, flyweight: Flyweight): void
:将新的享元对象添加到缓存中。
- 属性:
flyweights: Map<String, Flyweight>
:存储享元对象的缓存,键通常是一个唯一标识符,用于区分不同的享元对象。
-
Flyweight:
- 职责:定义享元对象的接口,该接口可以接受外部状态。
- 方法:
operation(extrinsicState: String): void
:操作方法,接受外部状态作为参数。外部状态是随上下文变化的,而内部状态是共享的。
- 属性:
intrinsicState: String
:内部状态,是可以共享的,通常在创建时设置,并且在对象的生命周期中保持不变。
-
ConcreteFlyweight:
- 职责:实现
Flyweight
接口,定义具体的享元对象。 - 方法:
operation(extrinsicState: String): void
:具体的实现,使用外部状态和内部状态来完成操作。
- 属性:
intrinsicState: String
:共享的内部状态。
- 职责:实现
享元模式的优缺点
优点
- 减少内存占用:通过共享对象的内部状态,减少内存使用,提高系统性能。
- 提高创建和管理效率:享元工厂可以缓存已经创建的享元对象,减少重复创建对象的开销。
- 模块化设计:享元模式将对象的状态分离为内部状态和外部状态,使得系统的模块化设计更加清晰。
缺点
- 增加系统复杂性:引入享元工厂和享元对象会增加系统的复杂性,需要管理内部状态和外部状态的分离。
- 需要外部状态的传递:每次使用享元对象时,都需要传递外部状态,这可能会增加调用的复杂性。
通过享元模式,可以有效地管理和共享对象的共有状态,从而减少内存占用,提高系统的性能。希望这些解释能帮助你更好地理解享元模式的 UML 类图及其具体实现。
享元模式在C++池化技术中的应用
享元模式在池化技术中非常有用,特别是在需要频繁创建和销毁大量相似对象的场景中。 pooling(池化技术)通过预先创建一组对象并重复使用这些对象,来减少对象创建的开销和内存的频繁分配与释放。享元模式可以进一步优化池化技术,通过共享对象的内部状态来减少内存使用。
示例:GUI资源池中的享元模式
假设我们正在开发一个GUI应用程序,需要创建大量的按钮(Button)对象。每个按钮对象都有相同的背景图片,但按钮的文本内容和位置是不同的。我们可以使用享元模式来共享按钮的背景图片,从而减少内存占用。
1. 定义享元接口
#include <iostream>
#include <string>
#include <map>
#include <memory>
class ButtonFlyweight {
public:
virtual void draw(const std::string& text, int x, int y) const = 0;
virtual ~ButtonFlyweight() {}
};
2. 定义具体享元
class ConcreteButtonFlyweight : public ButtonFlyweight {
private:
std::string _backgroundImage; // 共享的内部状态
public:
ConcreteButtonFlyweight(const std::string& backgroundImage) : _backgroundImage(backgroundImage) {}
void draw(const std::string& text, int x, int y) const override {
std::cout << "绘制按钮: " << text << ",位置: (" << x << ", " << y << "), 背景图片: " << _backgroundImage << std::endl;
}
};
3. 定义享元工厂
class ButtonFlyweightFactory {
private:
std::map<std::string, std::shared_ptr<ButtonFlyweight>> _flyweights;
public:
std::shared_ptr<ButtonFlyweight> getButton(const std::string& backgroundImage) {
if (_flyweights.find(backgroundImage) == _flyweights.end()) {
std::shared_ptr<ButtonFlyweight> newButton = std::make_shared<ConcreteButtonFlyweight>(backgroundImage);
_flyweights[backgroundImage] = newButton;
}
return _flyweights[backgroundImage];
}
};
4. 定义GUI组件(使用享元)
class GUIComponent {
private:
std::shared_ptr<ButtonFlyweight> _button;
std::string _text;
int _x;
int _y;
public:
GUIComponent(const std::string& text, int x, int y, ButtonFlyweightFactory& factory, const std::string& backgroundImage) :
_text(text), _x(x), _y(y), _button(factory.getButton(backgroundImage)) {}
void draw() const {
_button->draw(_text, _x, _y);
}
};
5. 客户端代码
int main() {
ButtonFlyweightFactory buttonFactory;
GUIComponent button1("Button 1", 10, 20, buttonFactory, "bg1.png");
GUIComponent button2("Button 2", 30, 40, buttonFactory, "bg1.png");
GUIComponent button3("Button 3", 50, 60, buttonFactory, "bg2.png");
button1.draw();
button2.draw();
button3.draw();
return 0;
}
代码解释
-
ButtonFlyweight 接口:
- 定义了
draw
方法,该方法接受按钮的文本内容和位置(外部状态)作为参数。
- 定义了
-
ConcreteButtonFlyweight 类:
- 实现了
ButtonFlyweight
接口,具体的draw
方法使用传递进来的外部状态(按钮的文本内容和位置)和内部状态(背景图片)来绘制按钮。
- 实现了
-
ButtonFlyweightFactory 类:
- 负责创建和管理
ButtonFlyweight
对象。 getButton
方法根据给定的背景图片返回一个享元对象。如果享元对象已经存在于缓存中,则返回缓存中的对象;否则,创建一个新的享元对象并将其加入到缓存中,然后返回。_flyweights
属性是一个map
,用于存储享元对象,键是背景图片的路径。
- 负责创建和管理
-
GUIComponent 类:
- 代表一个GUI组件,包含按钮的文本内容、位置等外部状态。
GUIComponent
构造函数接受按钮的文本内容、位置、享元工厂和背景图片路径作为参数,通过享元工厂获取享元对象。draw
方法用于绘制按钮,调用享元对象的draw
方法,传递按钮的文本内容和位置作为外部状态。
-
客户端代码:
- 创建
ButtonFlyweightFactory
对象。 - 创建多个
GUIComponent
对象,每个对象使用相同的背景图片时,享元工厂会返回同一个享元对象。 - 调用
draw
方法绘制按钮,每个按钮对象的内部状态(背景图片)是共享的,而外部状态(文本内容和位置)是在使用时传递的。
- 创建
享元模式在GUI资源池中的优势
-
减少内存占用:
- 通过共享按钮的背景图片,可以显著减少内存使用。如果每个按钮都具有相同的背景图片,但不共享,那么每个按钮都会占用额外的内存来存储背景图片数据。
-
提高创建和管理效率:
- 享元工厂可以缓存已经创建的享元对象,减少重复创建对象的开销。这对于频繁创建和销毁按钮对象的场景非常有用。
-
模块化设计:
- 将按钮的内部状态(背景图片)和外部状态(文本内容和位置)分离,使得系统的模块化设计更加清晰。这有助于提高代码的可维护性和可扩展性。
进一步优化
在实际应用中,可以进一步优化享元模式,例如:
- 使用智能指针:使用
std::shared_ptr
来管理享元对象的生命周期,确保对象在不再需要时被自动释放。 - 多线程安全:如果应用是多线程的,可以在享元工厂中使用互斥锁(
std::mutex
)来确保线程安全。 - 外部状态的封装:将外部状态封装在一个结构体或类中,然后传递给享元对象,以提高代码的可读性和可维护性。
通过这些优化,可以进一步提高享元模式在池化技术中的应用效果。希望这些解释和示例代码能帮助你更好地理解享元模式在C++池化技术中的应用。
结合 Composite 模式和 Flyweight 模式实现文档编辑器中的文本分层结构
在文档编辑器中,文本通常可以有层次结构,比如段落、行和字符等。我们可以使用 Composite 模式 来管理这种层次结构,而 Flyweight 模式 可以用来共享文本样式的内部状态,从而减少内存占用。为了进一步优化,我们还可以使用 有向无环图(DAG) 来实现 Flyweight 模式,使得多个对象可以共享复杂的内部状态。
示例代码
1. 定义享元接口
#include <iostream>
#include <string>
#include <map>
#include <memory>
#include <vector>
class TextStyle {
public:
virtual void render(char character) const = 0;
virtual ~TextStyle() {}
};
2. 定义具体享元
class ConcreteTextStyle : public TextStyle {
private:
std::string _font;
std::string _color;
int _size;
public:
ConcreteTextStyle(const std::string& font, const std::string& color, int size)
: _font(font), _color(color), _size(size) {}
void render(char character) const override {
std::cout << "绘制字符: " << character << ",字体: " << _font << ",颜色: " << _color << ",大小: " << _size << std::endl;
}
};
3. 定义享元工厂
class TextStyleFactory {
private:
std::map<std::string, std::shared_ptr<TextStyle>> _styles;
public:
std::shared_ptr<TextStyle> getTextStyle(const std::string& font, const std::string& color, int size) {
std::string key = font + "," + color + "," + std::to_string(size);
if (_styles.find(key) == _styles.end()) {
std::shared_ptr<TextStyle> newStyle = std::make_shared<ConcreteTextStyle>(font, color, size);
_styles[key] = newStyle;
}
return _styles[key];
}
};
4. 定义 Composite 模式中的组件接口
class TextComponent {
public:
virtual void display() const = 0;
virtual ~TextComponent() {}
};
5. 定义叶子节点(字符)
class TextCharacter : public TextComponent {
private:
char _character;
std::shared_ptr<TextStyle> _style;
public:
TextCharacter(char character, const std::shared_ptr<TextStyle>& style)
: _character(character), _style(style) {}
void display() const override {
_style->render(_character);
}
};
6. 定义组合节点(段落)
class Paragraph : public TextComponent {
private:
std::vector<std::shared_ptr<TextComponent>> _children;
public:
void addChild(const std::shared_ptr<TextComponent>& child) {
_children.push_back(child);
}
void display() const override {
std::cout << "段落:" << std::endl;
for (const auto& child : _children) {
child->display();
}
}
};
7. 定义组合节点(行)
class Line : public TextComponent {
private:
std::vector<std::shared_ptr<TextComponent>> _children;
public:
void addChild(const std::shared_ptr<TextComponent>& child) {
_children.push_back(child);
}
void display() const override {
std::cout << "行:" << std::endl;
for (const auto& child : _children) {
child->display();
}
}
};
8. 客户端代码
int main() {
TextStyleFactory styleFactory;
// 创建文本样式
std::shared_ptr<TextStyle> style1 = styleFactory.getTextStyle("Arial", "Red", 12);
std::shared_ptr<TextStyle> style2 = styleFactory.getTextStyle("Times New Roman", "Blue", 14);
// 创建字符
std::shared_ptr<TextComponent> charA = std::make_shared<TextCharacter>('A', style1);
std::shared_ptr<TextComponent> charB = std::make_shared<TextCharacter>('B', style1);
std::shared_ptr<TextComponent> charC = std::make_shared<TextCharacter>('C', style1);
std::shared_ptr<TextComponent> charD = std::make_shared<TextCharacter>('D', style2);
std::shared_ptr<TextComponent> charE = std::make_shared<TextCharacter>('E', style2);
// 创建行
std::shared_ptr<TextComponent> line1 = std::make_shared<Line>();
line1->addChild(charA);
line1->addChild(charB);
line1->addChild(charC);
std::shared_ptr<TextComponent> line2 = std::make_shared<Line>();
line2->addChild(charD);
line2->addChild(charE);
// 创建段落
std::shared_ptr<TextComponent> paragraph = std::make_shared<Paragraph>();
paragraph->addChild(line1);
paragraph->addChild(line2);
// 显示文档
paragraph->display();
return 0;
}
代码解释
-
TextStyle 接口:
- 定义了一个
render
方法,该方法接受一个字符作为参数,并负责绘制该字符时使用特定的文本样式。
- 定义了一个
-
ConcreteTextStyle 类:
- 实现了
TextStyle
接口,具体的render
方法使用传递进来的字符和内部状态(字体、颜色、大小)来绘制字符。
- 实现了
-
TextStyleFactory 类:
- 负责创建和管理
TextStyle
对象。 getTextStyle
方法根据给定的字体、颜色和大小生成一个唯一的键,并根据该键返回一个享元对象。如果享元对象已经存在于缓存中,则返回缓存中的对象;否则,创建一个新的享元对象并将其加入到缓存中,然后返回。_styles
属性是一个map
,用于存储享元对象,键是字体、颜色和大小的组合字符串。
- 负责创建和管理
-
TextComponent 接口:
- 定义了一个
display
方法,该方法用于显示文本组件。
- 定义了一个
-
TextCharacter 类:
- 代表文档中的单个字符,继承自
TextComponent
接口。 display
方法调用享元对象的render
方法来绘制字符。
- 代表文档中的单个字符,继承自
-
Line 类:
- 代表文档中的一行,继承自
TextComponent
接口。 addChild
方法用于向行中添加字符或其他文本组件。display
方法遍历并显示所有子组件。
- 代表文档中的一行,继承自
-
Paragraph 类:
- 代表文档中的段落,继承自
TextComponent
接口。 addChild
方法用于向段落中添加行或其他文本组件。display
方法遍历并显示所有子组件。
- 代表文档中的段落,继承自
-
客户端代码:
- 创建
TextStyleFactory
对象。 - 创建多个
ConcreteTextStyle
对象,并通过享元工厂获取它们。 - 创建多个字符对象
TextCharacter
,并指定其样式。 - 创建行对象
Line
,并向其中添加字符。 - 创建段落对象
Paragraph
,并向其中添加行。 - 调用
display
方法显示整个段落的层次结构。
- 创建
享元模式结合 Composite 模式的优势
-
减少内存占用:
- 通过共享文本样式的内部状态(字体、颜色、大小),可以显著减少内存使用。特别是在文档中大量字符使用相同的样式时,效果尤为明显。
-
提高创建和管理效率:
- 享元工厂可以缓存已经创建的文本样式对象,减少重复创建对象的开销。这对于频繁应用相同样式的场景非常有用。
-
层次结构的管理:
- 使用 Composite 模式可以方便地管理文档的层次结构,如段落、行和字符。通过组合节点和叶子节点,可以灵活地构建复杂的文本结构。
-
模块化设计:
- 将文本样式的内部状态和外部状态(字符)分离,使得系统的模块化设计更加清晰。这有助于提高代码的可维护性和可扩展性。
进一步优化
在实际应用中,可以进一步优化享元模式和 Composite 模式,例如:
- 使用智能指针:使用
std::shared_ptr
来管理享元对象和文本组件的生命周期,确保对象在不再需要时被自动释放。 - 多线程安全:如果应用是多线程的,可以在享元工厂中使用互斥锁(
std::mutex
)来确保线程安全。 - 外部状态的封装:将外部状态(如字符位置)封装在一个结构体或类中,然后传递给享元对象,以提高代码的可读性和可维护性。
通过这些优化,可以进一步提高享元模式在文档编辑器中文本样式中的应用效果。希望这些解释和示例代码能帮助你更好地理解如何结合 Composite 模式和 Flyweight 模式来实现文档编辑器中的文本分层结构。