设计模式(十)

发布于:2025-07-05 ⋅ 阅读:(21) ⋅ 点赞:(0)

享元模式(Flyweight Pattern)详解

一、核心概念

享元模式通过共享技术复用相同或相似的细粒度对象,以减少内存占用和提高性能。该模式将对象状态分为内部状态(可共享的不变部分)和外部状态(需外部传入的可变部分),通过共享内部状态降低对象数量。

核心组件

  1. 抽象享元(Flyweight):定义共享对象的接口,声明处理外部状态的方法。
  2. 具体享元(Concrete Flyweight):实现抽象享元接口,包含内部状态并处理外部状态。
  3. 享元工厂(Flyweight Factory):创建和管理享元对象,确保合理共享。
  4. 客户端(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;
}
三、享元模式的优势
  1. 减少内存占用

    • 通过共享相同内部状态的对象,大幅降低内存消耗。
  2. 提高性能

    • 减少对象创建和垃圾回收的开销。
  3. 集中管理状态

    • 内部状态由享元对象统一维护,外部状态由客户端管理。
  4. 符合开闭原则

    • 新增享元类型无需修改现有代码,易于扩展。
四、实现变种
  1. 单纯享元 vs 复合享元

    • 复合享元将多个单纯享元组合成更复杂的对象,仍保持共享特性。
  2. 静态工厂 vs 动态工厂

    • 静态工厂在初始化时创建所有享元;动态工厂按需创建。
  3. 弱引用缓存

    • 使用std::weak_ptr管理享元对象,允许不再使用的对象被垃圾回收。
五、适用场景
  1. 系统中存在大量相似对象

    • 如游戏中的粒子效果、文本处理中的字符。
  2. 对象状态可分为内部/外部状态

    • 内部状态可共享,外部状态需动态传入。
  3. 对象创建成本高

    • 如数据库连接、网络连接等资源密集型对象。
  4. 需要缓存对象

    • 如缓存配置信息、模板对象等。
六、注意事项
  1. 状态划分复杂性

    • 正确区分内部状态和外部状态需要仔细设计,避免逻辑混乱。
  2. 线程安全

    • 共享对象可能被多线程访问,需确保线程安全。
  3. 性能权衡

    • 享元模式引入工厂和缓存逻辑,需权衡其带来的额外开销。
  4. 与其他模式结合

    • 常与工厂模式结合创建享元对象,与组合模式构建复合享元。

享元模式通过共享技术优化大量细粒度对象的内存占用,是一种典型的“以时间换空间”的优化策略。在需要处理大量相似对象的场景中,该模式能显著提升系统性能。

解释器模式(Interpreter Pattern)详解

一、核心概念

解释器模式用于定义语言的文法表示,并创建解释器来解析该语言中的句子。它将语言中的每个语法规则映射为一个类,使语法规则的实现和使用分离,适合简单的领域特定语言(DSL)。

核心组件

  1. 抽象表达式(Abstract Expression):定义解释操作的接口。
  2. 终结符表达式(Terminal Expression):实现与文法中的终结符相关的解释操作(如变量、常量)。
  3. 非终结符表达式(Non-terminal Expression):实现文法中的非终结符操作(如运算符、语句结构)。
  4. 上下文(Context):包含解释器需要的全局信息。
  5. 客户端(Client):构建抽象语法树并调用解释器。

“解释器模式有什么好处呢?​”
“用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写[DP]​。​”

二、代码示例:简单的布尔表达式解释器

场景:实现一个简单的布尔表达式解释器,支持变量(如xy)、逻辑与(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;
}
三、解释器模式的优势
  1. 可扩展性

    • 新增语法规则只需添加新的表达式类,符合开闭原则。
  2. 简化语法实现

    • 将复杂语法分解为多个简单表达式,易于维护。
  3. 灵活性

    • 相同语法树可通过不同上下文实现不同解释。
  4. 直观表示

    • 语法规则以类的形式表示,清晰直观。
四、实现变种
  1. 解析器生成器

    • 使用工具(如ANTLR)自动生成解释器,而非手动实现。
  2. 解释器与编译器结合

    • 先将语言编译为中间表示,再由解释器执行。
  3. 上下文优化

    • 使用共享上下文或线程局部上下文提高性能。
五、适用场景
  1. 领域特定语言(DSL)

    • 如正则表达式、SQL查询、配置文件解析。
  2. 简单语法解析

    • 如数学表达式计算、模板引擎。
  3. 重复出现的问题

    • 如日志过滤规则、权限表达式。
  4. 教育场景

    • 教学编译器原理或语言解释机制。
六、注意事项
  1. 性能限制

    • 解释执行效率通常低于编译执行,不适合复杂大规模语言。
  2. 复杂度控制

    • 过多语法规则会导致类数量激增,需合理组织。
  3. 安全性

    • 解释用户输入的表达式可能引入安全风险(如代码注入)。
  4. 替代方案

    • 复杂场景可考虑使用解析器生成工具(如Lex/Yacc)或直接编译为字节码。

解释器模式通过将语言语法规则映射为对象层次结构,提供了一种优雅的方式来解析和执行简单语言。在需要自定义DSL或处理特定语法的场景中,该模式能有效简化实现。

访问者模式(Visitor Pattern)详解

一、核心概念

访问者模式允许在不改变对象结构的前提下,定义作用于这些对象的新操作。它将算法与对象结构分离,通过双分派(Double Dispatch)实现对不同类型元素的差异化处理。

核心组件

  1. 抽象访问者(Visitor):定义对每个具体元素的访问操作接口。
  2. 具体访问者(Concrete Visitor):实现抽象访问者接口,处理特定操作。
  3. 抽象元素(Element):定义接受访问者的接口(accept方法)。
  4. 具体元素(Concrete Element):实现接受访问者的逻辑,通常调用访问者的对应方法。
  5. 对象结构(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 << "![" << image.getUrl() << "](" << image.getUrl() << ")" << 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;
}
三、访问者模式的优势
  1. 开闭原则

    • 新增操作只需添加新的访问者,无需修改元素类。
  2. 操作集中化

    • 相关操作集中在一个访问者中,便于维护。
  3. 双分派机制

    • 通过acceptvisit方法的双重调用,动态确定元素类型和操作。
  4. 分离关注点

    • 元素的业务逻辑与操作解耦,提高代码可维护性。
四、实现变种
  1. 对象结构的实现

    • 可以是列表、树、图等任何数据结构。
  2. 访问者重载

    • 支持不同参数或返回值的访问方法。
  3. 访问者链

    • 多个访问者按顺序处理元素。
  4. 懒加载访问者

    • 在需要时动态创建访问者。
五、适用场景
  1. 对象结构稳定但操作多变

    • 如编译器的AST节点处理、XML解析。
  2. 需要对不同类型元素进行差异化操作

    • 如文档格式化、图形渲染。
  3. 跨层次操作元素

    • 如统计文档中不同元素的数量。
  4. 避免大量条件判断

    • 将类型判断逻辑封装在访问者中。
六、注意事项
  1. 破坏封装性

    • 访问者可能需要访问元素的私有状态,需谨慎设计。
  2. 元素类型固定

    • 新增元素类型需修改所有访问者,违反开闭原则。
  3. 复杂度增加

    • 模式引入多个类,可能增加系统复杂度。
  4. 性能开销

    • 双重分派可能带来一定性能损耗。

访问者模式通过分离对象结构和操作,提供了一种灵活的方式来扩展系统功能。在需要对稳定对象结构定义多种操作的场景中,该模式尤为有效。


网站公告

今日签到

点亮在社区的每一天
去签到