享元模式(Flyweight Pattern)详解
一、核心概念
享元模式通过共享技术复用相同或相似的细粒度对象,以减少内存占用和提高性能。该模式将对象状态分为内部状态(可共享的不变部分)和外部状态(需外部传入的可变部分),通过共享内部状态降低对象数量。
核心组件:
- 抽象享元(Flyweight):定义共享对象的接口,声明处理外部状态的方法。
- 具体享元(Concrete Flyweight):实现抽象享元接口,包含内部状态并处理外部状态。
- 享元工厂(Flyweight Factory):创建和管理享元对象,确保合理共享。
- 客户端(Client):通过工厂获取享元对象,并传入外部状态。
在享元对象内部并且不会随环境改变而改变的共享部分,可以称为是享元对象的内部状态,而随环境改变而改变的、不可以共享的状态就是外部状态了。事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。也就是说,享元模式Flyweight执行时所需的状态是有内部的也可能有外部的,内部状态存储于ConcreteFlyweight对象之中,而外部对象则应该考虑由客户端对象存储或计算,当调用Flyweight对象的操作时,将该状态传递给它。” ——《大话设计模式》
“大鸟,你通过这个例子来讲解享元模式虽然我是理解了,但在现实中什么时候才应该考虑使用享元模式呢?”
“就知道你会问这样的问题,如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。”
二、代码示例:文本编辑器中的字符共享
场景:文本编辑器中,每个字符的字体、颜色等属性可共享,位置和内容为外部状态。
#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>
// 外部状态:字符位置
struct CharacterPosition {
int x, y;
};
// 抽象享元:字符
class Character {
protected:
char symbol; // 内部状态:字符符号
std::string font; // 内部状态:字体
int size; // 内部状态:字号
public:
Character(char symbol, const std::string& font, int size)
: symbol(symbol), font(font), size(size) {}
virtual void display(const CharacterPosition& pos) = 0;
virtual ~Character() = default;
};
// 具体享元:ASCII字符
class AsciiCharacter : public Character {
public:
using Character::Character; // 继承构造函数
void display(const CharacterPosition& pos) override {
std::cout << "字符: " << symbol
<< ", 字体: " << font
<< ", 字号: " << size
<< ", 位置: (" << pos.x << ", " << pos.y << ")" << std::endl;
}
};
// 享元工厂:字符工厂
class CharacterFactory {
private:
std::unordered_map<std::string, std::shared_ptr<Character>> characters;
public:
std::shared_ptr<Character> getCharacter(char symbol, const std::string& font, int size) {
std::string key = std::string(1, symbol) + "-" + font + "-" + std::to_string(size);
if (characters.find(key) == characters.end()) {
// 若不存在,则创建新的享元对象
characters[key] = std::make_shared<AsciiCharacter>(symbol, font, size);
std::cout << "创建新字符: " << key << std::endl;
} else {
std::cout << "复用已存在字符: " << key << std::endl;
}
return characters[key];
}
size_t getCharacterCount() const {
return characters.size();
}
};
// 客户端代码
int main() {
CharacterFactory factory;
// 创建并显示多个字符(部分共享)
factory.getCharacter('A', "Arial", 12)->display({10, 20});
factory.getCharacter('B', "Arial", 12)->display({30, 20});
factory.getCharacter('A', "Arial", 12)->display({50, 20}); // 复用'A'
factory.getCharacter('A', "Times New Roman", 14)->display({70, 20}); // 新字符
std::cout << "总共创建的字符对象数: " << factory.getCharacterCount() << std::endl;
return 0;
}
三、享元模式的优势
减少内存占用:
- 通过共享相同内部状态的对象,大幅降低内存消耗。
提高性能:
- 减少对象创建和垃圾回收的开销。
集中管理状态:
- 内部状态由享元对象统一维护,外部状态由客户端管理。
符合开闭原则:
- 新增享元类型无需修改现有代码,易于扩展。
四、实现变种
单纯享元 vs 复合享元:
- 复合享元将多个单纯享元组合成更复杂的对象,仍保持共享特性。
静态工厂 vs 动态工厂:
- 静态工厂在初始化时创建所有享元;动态工厂按需创建。
弱引用缓存:
- 使用
std::weak_ptr
管理享元对象,允许不再使用的对象被垃圾回收。
- 使用
五、适用场景
系统中存在大量相似对象:
- 如游戏中的粒子效果、文本处理中的字符。
对象状态可分为内部/外部状态:
- 内部状态可共享,外部状态需动态传入。
对象创建成本高:
- 如数据库连接、网络连接等资源密集型对象。
需要缓存对象:
- 如缓存配置信息、模板对象等。
六、注意事项
状态划分复杂性:
- 正确区分内部状态和外部状态需要仔细设计,避免逻辑混乱。
线程安全:
- 共享对象可能被多线程访问,需确保线程安全。
性能权衡:
- 享元模式引入工厂和缓存逻辑,需权衡其带来的额外开销。
与其他模式结合:
- 常与工厂模式结合创建享元对象,与组合模式构建复合享元。
享元模式通过共享技术优化大量细粒度对象的内存占用,是一种典型的“以时间换空间”的优化策略。在需要处理大量相似对象的场景中,该模式能显著提升系统性能。
解释器模式(Interpreter Pattern)详解
一、核心概念
解释器模式用于定义语言的文法表示,并创建解释器来解析该语言中的句子。它将语言中的每个语法规则映射为一个类,使语法规则的实现和使用分离,适合简单的领域特定语言(DSL)。
核心组件:
- 抽象表达式(Abstract Expression):定义解释操作的接口。
- 终结符表达式(Terminal Expression):实现与文法中的终结符相关的解释操作(如变量、常量)。
- 非终结符表达式(Non-terminal Expression):实现文法中的非终结符操作(如运算符、语句结构)。
- 上下文(Context):包含解释器需要的全局信息。
- 客户端(Client):构建抽象语法树并调用解释器。
“解释器模式有什么好处呢?”
“用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写[DP]。”
二、代码示例:简单的布尔表达式解释器
场景:实现一个简单的布尔表达式解释器,支持变量(如x
、y
)、逻辑与(AND
)、逻辑非(NOT
)。
#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>
#include <vector>
// 上下文:存储变量值
class Context {
private:
std::unordered_map<std::string, bool> variables;
public:
void setVariable(const std::string& name, bool value) {
variables[name] = value;
}
bool getVariable(const std::string& name) const {
auto it = variables.find(name);
return it != variables.end() ? it->second : false;
}
};
// 抽象表达式
class Expression {
public:
virtual bool interpret(const Context& context) const = 0;
virtual ~Expression() = default;
};
// 终结符表达式:变量
class VariableExpression : public Expression {
private:
std::string name;
public:
explicit VariableExpression(const std::string& name) : name(name) {}
bool interpret(const Context& context) const override {
return context.getVariable(name);
}
};
// 终结符表达式:常量
class ConstantExpression : public Expression {
private:
bool value;
public:
explicit ConstantExpression(bool value) : value(value) {}
bool interpret(const Context& context) const override {
return value;
}
};
// 非终结符表达式:逻辑与
class AndExpression : public Expression {
private:
std::shared_ptr<Expression> left;
std::shared_ptr<Expression> right;
public:
AndExpression(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right)
: left(left), right(right) {}
bool interpret(const Context& context) const override {
return left->interpret(context) && right->interpret(context);
}
};
// 非终结符表达式:逻辑非
class NotExpression : public Expression {
private:
std::shared_ptr<Expression> operand;
public:
explicit NotExpression(std::shared_ptr<Expression> operand) : operand(operand) {}
bool interpret(const Context& context) const override {
return !operand->interpret(context);
}
};
// 表达式解析器(简化版)
class Parser {
public:
static std::shared_ptr<Expression> parse(const std::string& input) {
// 实际应用中需实现完整的词法和语法分析
// 此处简化为直接构建表达式
if (input == "true") {
return std::make_shared<ConstantExpression>(true);
} else if (input == "false") {
return std::make_shared<ConstantExpression>(false);
} else if (input == "NOT x") {
auto x = std::make_shared<VariableExpression>("x");
return std::make_shared<NotExpression>(x);
} else if (input == "x AND y") {
auto x = std::make_shared<VariableExpression>("x");
auto y = std::make_shared<VariableExpression>("y");
return std::make_shared<AndExpression>(x, y);
}
// 默认返回false
return std::make_shared<ConstantExpression>(false);
}
};
// 客户端代码
int main() {
Context context;
context.setVariable("x", true);
context.setVariable("y", false);
// 解析并解释表达式
auto expr1 = Parser::parse("x AND y");
std::cout << "表达式 \"x AND y\" 的结果: "
<< (expr1->interpret(context) ? "true" : "false") << std::endl;
auto expr2 = Parser::parse("NOT x");
std::cout << "表达式 \"NOT x\" 的结果: "
<< (expr2->interpret(context) ? "true" : "false") << std::endl;
return 0;
}
三、解释器模式的优势
可扩展性:
- 新增语法规则只需添加新的表达式类,符合开闭原则。
简化语法实现:
- 将复杂语法分解为多个简单表达式,易于维护。
灵活性:
- 相同语法树可通过不同上下文实现不同解释。
直观表示:
- 语法规则以类的形式表示,清晰直观。
四、实现变种
解析器生成器:
- 使用工具(如ANTLR)自动生成解释器,而非手动实现。
解释器与编译器结合:
- 先将语言编译为中间表示,再由解释器执行。
上下文优化:
- 使用共享上下文或线程局部上下文提高性能。
五、适用场景
领域特定语言(DSL):
- 如正则表达式、SQL查询、配置文件解析。
简单语法解析:
- 如数学表达式计算、模板引擎。
重复出现的问题:
- 如日志过滤规则、权限表达式。
教育场景:
- 教学编译器原理或语言解释机制。
六、注意事项
性能限制:
- 解释执行效率通常低于编译执行,不适合复杂大规模语言。
复杂度控制:
- 过多语法规则会导致类数量激增,需合理组织。
安全性:
- 解释用户输入的表达式可能引入安全风险(如代码注入)。
替代方案:
- 复杂场景可考虑使用解析器生成工具(如Lex/Yacc)或直接编译为字节码。
解释器模式通过将语言语法规则映射为对象层次结构,提供了一种优雅的方式来解析和执行简单语言。在需要自定义DSL或处理特定语法的场景中,该模式能有效简化实现。
访问者模式(Visitor Pattern)详解
一、核心概念
访问者模式允许在不改变对象结构的前提下,定义作用于这些对象的新操作。它将算法与对象结构分离,通过双分派(Double Dispatch)实现对不同类型元素的差异化处理。
核心组件:
- 抽象访问者(Visitor):定义对每个具体元素的访问操作接口。
- 具体访问者(Concrete Visitor):实现抽象访问者接口,处理特定操作。
- 抽象元素(Element):定义接受访问者的接口(
accept
方法)。 - 具体元素(Concrete Element):实现接受访问者的逻辑,通常调用访问者的对应方法。
- 对象结构(Object Structure):管理元素集合,提供遍历元素的方式。
二、代码示例:文档元素的格式化处理
场景:文档包含文本、图片等元素,需支持不同格式的导出(如HTML、Markdown)。
#include <iostream>
#include <string>
#include <memory>
#include <vector>
// 前向声明
class TextElement;
class ImageElement;
// 抽象访问者
class Visitor {
public:
virtual void visitText(const TextElement& text) = 0;
virtual void visitImage(const ImageElement& image) = 0;
virtual ~Visitor() = default;
};
// 抽象元素
class Element {
public:
virtual void accept(Visitor& visitor) const = 0;
virtual ~Element() = default;
};
// 具体元素:文本
class TextElement : public Element {
private:
std::string content;
public:
explicit TextElement(const std::string& content) : content(content) {}
void accept(Visitor& visitor) const override {
visitor.visitText(*this);
}
std::string getContent() const { return content; }
};
// 具体元素:图片
class ImageElement : public Element {
private:
std::string url;
public:
explicit ImageElement(const std::string& url) : url(url) {}
void accept(Visitor& visitor) const override {
visitor.visitImage(*this);
}
std::string getUrl() const { return url; }
};
// 具体访问者:HTML导出器
class HtmlExportVisitor : public Visitor {
public:
void visitText(const TextElement& text) override {
std::cout << "<p>" << text.getContent() << "</p>" << std::endl;
}
void visitImage(const ImageElement& image) override {
std::cout << "<img src=\"" << image.getUrl() << "\" alt=\"图片\" />" << std::endl;
}
};
// 具体访问者:Markdown导出器
class MarkdownExportVisitor : public Visitor {
public:
void visitText(const TextElement& text) override {
std::cout << text.getContent() << std::endl << std::endl;
}
void visitImage(const ImageElement& image) override {
std::cout << " << ")" << std::endl;
}
};
// 对象结构:文档
class Document {
private:
std::vector<std::shared_ptr<Element>> elements;
public:
void addElement(std::shared_ptr<Element> element) {
elements.push_back(element);
}
void accept(Visitor& visitor) const {
for (const auto& element : elements) {
element->accept(visitor);
}
}
};
// 客户端代码
int main() {
Document doc;
doc.addElement(std::make_shared<TextElement>("欢迎使用访问者模式"));
doc.addElement(std::make_shared<ImageElement>("https://example.com/logo.png"));
doc.addElement(std::make_shared<TextElement>("这是一个示例文档"));
std::cout << "=== HTML 导出 ===" << std::endl;
HtmlExportVisitor htmlVisitor;
doc.accept(htmlVisitor);
std::cout << "\n=== Markdown 导出 ===" << std::endl;
MarkdownExportVisitor markdownVisitor;
doc.accept(markdownVisitor);
return 0;
}
三、访问者模式的优势
开闭原则:
- 新增操作只需添加新的访问者,无需修改元素类。
操作集中化:
- 相关操作集中在一个访问者中,便于维护。
双分派机制:
- 通过
accept
和visit
方法的双重调用,动态确定元素类型和操作。
- 通过
分离关注点:
- 元素的业务逻辑与操作解耦,提高代码可维护性。
四、实现变种
对象结构的实现:
- 可以是列表、树、图等任何数据结构。
访问者重载:
- 支持不同参数或返回值的访问方法。
访问者链:
- 多个访问者按顺序处理元素。
懒加载访问者:
- 在需要时动态创建访问者。
五、适用场景
对象结构稳定但操作多变:
- 如编译器的AST节点处理、XML解析。
需要对不同类型元素进行差异化操作:
- 如文档格式化、图形渲染。
跨层次操作元素:
- 如统计文档中不同元素的数量。
避免大量条件判断:
- 将类型判断逻辑封装在访问者中。
六、注意事项
破坏封装性:
- 访问者可能需要访问元素的私有状态,需谨慎设计。
元素类型固定:
- 新增元素类型需修改所有访问者,违反开闭原则。
复杂度增加:
- 模式引入多个类,可能增加系统复杂度。
性能开销:
- 双重分派可能带来一定性能损耗。
访问者模式通过分离对象结构和操作,提供了一种灵活的方式来扩展系统功能。在需要对稳定对象结构定义多种操作的场景中,该模式尤为有效。