设计模式(十)
享元模式(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);}// 默认返回falsereturn 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解析。
-
需要对不同类型元素进行差异化操作:
- 如文档格式化、图形渲染。
-
跨层次操作元素:
- 如统计文档中不同元素的数量。
-
避免大量条件判断:
- 将类型判断逻辑封装在访问者中。
六、注意事项
-
破坏封装性:
- 访问者可能需要访问元素的私有状态,需谨慎设计。
-
元素类型固定:
- 新增元素类型需修改所有访问者,违反开闭原则。
-
复杂度增加:
- 模式引入多个类,可能增加系统复杂度。
-
性能开销:
- 双重分派可能带来一定性能损耗。
访问者模式通过分离对象结构和操作,提供了一种灵活的方式来扩展系统功能。在需要对稳定对象结构定义多种操作的场景中,该模式尤为有效。