C++设计模式之——享元模式详解和代码案例

C++中实现享元模式通常涉及以下几个关键部分:

享元模式(Flyweight Pattern)是一种用于性能优化的设计模式,它通过共享对象来有效支持大量的细粒度对象,以减少内存消耗。这种模式适用于那些对象中大部分状态可以外部化,而只有少量内部状态(不随环境改变而改变的状态)的对象。在C++中实现享元模式通常涉及以下几个关键部分:

  1. 抽象享元(Flyweight Interface)
    定义了所有具体享元类共有的接口,通常包含内部状态和外部状态的操作方法。内部状态是可以共享的,而外部状态由客户端在每次调用时传入。
class IFlyweight {
public:
    virtual ~IFlyweight() {}
    // 内部状态不变的操作
    virtual void operation(const Context& context) const = 0;
};
  1. 具体享元(Concrete Flyweight)
    实现了抽象享元接口,并存储内部状态。具体享元类的实例是可共享的。
class ConcreteFlyweight : public IFlyweight {
private:
    std::string intrinsicState; // 内部状态
public:
    ConcreteFlyweight(const std::string& state) : intrinsicState(state) {}
    void operation(const Context& context) const override {
        // 使用内部状态和外部状态进行操作
    }
};
  1. 享元工厂(Flyweight Factory)
    负责创建和管理享元对象。它确保当请求的是相同的内部状态时,不会创建多个具有相同内部状态的享元对象。
class FlyweightFactory {
private:
    std::map<std::string, std::shared_ptr<IFlyweight>> flyweights;
public:
    std::shared_ptr<IFlyweight> getFlyweight(const std::string& key) {
        if (flyweights.find(key) == flyweights.end()) {
            flyweights[key] = std::make_shared<ConcreteFlyweight>(key);
        }
        return flyweights[key];
    }
};

一个简单的C++代码片段示例

下面是一个简单的C++代码片段示例,展示了如何使用享元模式:

#include <iostream>
#include <string>
#include <map>
#include <memory>

// 抽象享元
class IFlyweight {
public:
    virtual ~IFlyweight() {}
    virtual void operation(const std::string& extrinsicState) const = 0;
};

// 具体享元
class ConcreteFlyweight : public IFlyweight {
private:
    std::string intrinsicState;
public:
    ConcreteFlyweight(const std::string& state) : intrinsicState(state) {}
    void operation(const std::string& extrinsicState) const override {
        std::cout << "Concrete Flyweight: Internal State = " << intrinsicState
                  << ", Extrinsic State = " << extrinsicState << std::endl;
    }
};

// 享元工厂
class FlyweightFactory {
private:
    std::map<std::string, std::shared_ptr<IFlyweight>> flyweights;
public:
    std::shared_ptr<IFlyweight> getFlyweight(const std::string& key) {
        if (flyweights.count(key) == 0) {
            flyweights[key] = std::make_shared<ConcreteFlyweight>(key);
        }
        return flyweights[key];
    }
};

int main() {
    FlyweightFactory factory;

    std::vector<std::string> extrinsicStates = {"state1", "state2", "state2"};

    for (const auto& state : extrinsicStates) {
        auto flyweight = factory.getFlyweight("sharedState");
        flyweight->operation(state);
    }

    return 0;
}

在这个例子中,每当客户端请求一个享元对象时,工厂会检查是否已经存在具有相同内部状态的对象。如果存在,则返回已有的对象;如果不存在,则创建新的具体享元对象并存储起来供后续请求使用。这样,多个带有不同外部状态的对象就可以共享同一个具有固定内部状态的具体享元对象,从而节约内存。

让我们深入探讨一下上面代码示例的实际意义和应用场景。在上述例子中,ConcreteFlyweight 类的内部状态是字符串 "sharedState",它是可共享的。而外部状态则是传递给 operation() 方法的参数 extrinsicState,每次调用时可能不同。

例如,假设我们正在创建一个文本渲染引擎,其中有许多字符需要绘制,但许多字符共享同一张图片资源。在这种情况下,字符的形状(字体样式、颜色等)可以视为内部状态,这部分是固定的且可以共享;而字符的位置、旋转角度、缩放比例等则可以视为外部状态,这些属性会随着字符在不同上下文中的使用而变化。

class CharacterFlyweight : public IFlyweight {
private:
    Texture texture; // 内部状态,表示字符图像纹理
public:
    CharacterFlyweight(const Texture& tex) : texture(tex) {}
    void operation(const RenderingContext& context) const override {
        // 使用共享的纹理资源,根据context中的位置、旋转角度等绘制字符
    }
};

class CharacterFactory {
private:
    std::map<CharacterStyle, std::shared_ptr<IFlyweight>> characters;
public:
    std::shared_ptr<IFlyweight> getCharacter(const CharacterStyle& style) {
        if (characters.find(style) == characters.end()) {
            Texture tex = loadTexture(style.getFontFile()); // 加载图片资源
            characters[style] = std::make_shared<CharacterFlyweight>(tex);
        }
        return characters[style];
    }
};

struct RenderingContext {
    Point position;
    double rotation;
    float scale;
    // 其他外部状态...
};

int main() {
    CharacterFactory factory;

    // 渲染不同位置的同一种字符
    CharacterStyle sharedStyle("Arial.ttf");
    RenderingContext ctx1{Point{10, 20}, 0.0, 1.0};
    RenderingContext ctx2{Point{30, 40}, 0.0, 1.0};
    auto character = factory.getCharacter(sharedStyle);
    character->operation(ctx1);
    character->operation(ctx2);

    return 0;
}

在这个例子中,CharacterFlyweight 类是具体的享元类,存储了字符的纹理(内部状态)。CharacterFactory 类作为享元工厂,负责创建和管理字符享元对象,保证相同的字符样式只加载一次图片资源。每次需要渲染字符时,客户端获取对应字符享元对象并传入上下文(即外部状态),从而有效地减少了重复加载资源造成的内存消耗。

享元模式的进一步说明

进一步说明,享元模式(Flyweight Pattern)的核心目标是通过共享技术有效支持大量细粒度的对象,从而节省系统资源。在上述文本渲染引擎的例子中:

  • CharacterFlyweight 是享元类,它封装了字符的基本视觉表现——也就是共享的纹理资源(内部状态),并且提供了 operation() 方法来执行实际的渲染操作,该方法接受 RenderingContext 对象,包含了外部状态如位置、旋转角度和缩放比例。

  • CharacterFactory 负责管理和创建享元对象,确保相同字符样式只创建一个实例。当请求某个样式的字符时,如果该样式对应的享元对象尚不存在,则加载相应的纹理资源并创建新的 CharacterFlyweight 实例;若已经存在,则直接返回已有的实例。

  • RenderingContext 表示每个字符实例的特定环境或配置,这是外部状态的具体体现,不被多个字符实例共享。每渲染一个字符时,都会根据当前的渲染上下文调整字符的表现形式。

在实际应用中,通过这样的设计,即便文档中有成千上万个同类型的字符需要渲染,也只需要保存一份共享的纹理资源,大大降低了系统的内存占用。同时,由于外部状态的变化不影响内部状态的复用,使得系统能灵活应对各种不同的渲染需求。

除此之外,享元模式还有助于减少系统中对象的数量,进而降低系统运行时的内存消耗和CPU开销。特别是在大型系统中,合理运用享元模式能够显著提升性能和响应速度。不过需要注意的是,不是所有的系统或场景都适用享元模式,应根据具体情况判断是否满足以下条件:

  1. 对象数量庞大且内部状态大部分可以共享:如果系统中存在大量相似对象,且这些对象之间的大部分状态是相同的,那么就可以考虑使用享元模式。

  2. 内部状态较少且相对稳定:内部状态是指那些不会随着环境改变而改变的状态,外部状态则是与具体使用环境相关的状态。享元对象应该尽量少地持有内部状态,并通过参数传递外部状态。

  3. 对象的创建成本高:如果对象的创建过程比较耗时或耗费资源,通过享元模式复用已有对象可以显著减少这些成本。

总结起来,享元模式在游戏开发、图形渲染、数据库连接池、缓存系统等领域有广泛应用。在实现时需权衡好内存消耗和程序逻辑复杂度的关系,确保模式的有效性和易维护性。

C++享元模式代码案例——咖啡店订单系统

以下是一个简单的C++享元模式代码案例,该案例模拟了一个咖啡店订单系统,其中咖啡口味被视为享元对象,可以被多个订单共享:

#include <iostream>
#include <map>
#include <memory>

// 抽象享元接口
class CoffeeFlavor {
public:
    virtual ~CoffeeFlavor() {}
    virtual void serve() const = 0;
};

// 具体享元类
class ConcreteCoffeeFlavor : public CoffeeFlavor {
public:
    explicit ConcreteCoffeeFlavor(const std::string& flavorName)
        : flavorName_(flavorName) {}

    void serve() const override {
        std::cout << "Serving coffee flavor: " << flavorName_ << std::endl;
    }

private:
    std::string flavorName_;
};

// 享元工厂
class CoffeeFlavorFactory {
private:
    std::map<std::string, std::shared_ptr<CoffeeFlavor>> flavors;

public:
    std::shared_ptr<CoffeeFlavor> getOrder(const std::string& flavor) {
        if (flavors.find(flavor) == flavors.end()) {
            flavors[flavor] = std::make_shared<ConcreteCoffeeFlavor>(flavor);
        }
        return flavors[flavor];
    }
};

int main() {
    CoffeeFlavorFactory factory;

    // 创建几个不同的订单,但某些口味会被共享
    std::vector<std::string> orders = {"Espresso", "Latte", "Espresso", "Cappuccino", "Espresso"};
    for (const auto& order : orders) {
        auto flavor = factory.getOrder(order);
        flavor->serve();
    }

    return 0;
}

在这个例子中:

  • CoffeeFlavor 是抽象享元类,定义了所有咖啡口味的基本行为,即服务(serve)咖啡。
  • ConcreteCoffeeFlavor 是具体享元类,实现了咖啡口味的具体行为,并存储了口味名称这一内部状态。
  • CoffeeFlavorFactory 是享元工厂,它维护了一个储存所有咖啡口味实例的映射表。当客户请求某个口味的咖啡时,工厂会检查是否已经创建过该口味的实例,如果没有则创建一个新的实例,否则返回已有的实例,从而实现了口味的共享。

运行此代码,可以看到虽然订单列表中有多个"Espresso",但在打印结果中只会看到一次"Serving coffee flavor: Espresso",这是因为享元模式让多次请求相同口味的咖啡共享了同一个实例。

实际上,上述咖啡口味享元模式的案例并没有体现出享元模式对外部状态的处理。在一个更全面的示例中,我们可能还会遇到咖啡订单具有外部状态,如客户的偏好(加糖、加奶)、杯子大小等。这时,我们可以对示例稍作修改,将外部状态从享元对象中分离出来:

#include <iostream>
#include <map>
#include <memory>

// 抽象享元接口
class CoffeeFlavor {
public:
    virtual ~CoffeeFlavor() {}
    virtual void serve(const std::string& extras, const std::string& size) const = 0;
};

// 具体享元类
class ConcreteCoffeeFlavor : public CoffeeFlavor {
public:
    explicit ConcreteCoffeeFlavor(const std::string& flavorName)
        : flavorName_(flavorName) {}

    void serve(const std::string& extras, const std::string& size) const override {
        std::cout << "Serving " << size << " cup of " << flavorName_
                  << " with extras: " << extras << std::endl;
    }

private:
    std::string flavorName_;
};

// 享元工厂
class CoffeeFlavorFactory {
private:
    std::map<std::string, std::shared_ptr<CoffeeFlavor>> flavors;

public:
    std::shared_ptr<CoffeeFlavor> getOrder(const std::string& flavor) {
        if (flavors.find(flavor) == flavors.end()) {
            flavors[flavor] = std::make_shared<ConcreteCoffeeFlavor>(flavor);
        }
        return flavors[flavor];
    }
};

// 订单类,包含外部状态
class CoffeeOrder {
public:
    CoffeeOrder(std::shared_ptr<CoffeeFlavor> flavor, const std::string& extras, const std::string& size)
        : flavor_(flavor), extras_(extras), size_(size) {}

    void serve() const {
        flavor_->serve(extras_, size_);
    }

private:
    std::shared_ptr<CoffeeFlavor> flavor_;
    std::string extras_;
    std::string size_;
};

int main() {
    CoffeeFlavorFactory factory;

    // 创建几个不同的订单,但某些口味会被共享
    std::vector<CoffeeOrder> orders = {
        {factory.getOrder("Espresso"), "no sugar", "small"},
        {factory.getOrder("Latte"), "extra foam", "medium"},
        {factory.getOrder("Espresso"), "double sugar", "large"},
        {factory.getOrder("Cappuccino"), "cinnamon", "medium"},
        {factory.getOrder("Espresso"), "single sugar", "small"}
    };

    for (const auto& order : orders) {
        order.serve();
    }

    return 0;
}

在这个修改后的示例中,我们创建了一个CoffeeOrder类,它包含了外部状态(额外配料和杯子大小),并在serve()方法中将这些外部状态传递给享元对象。即使多个订单选择了相同的咖啡口味,由于外部状态的不同,每个订单都能得到个性化的服务。

享元模式在现实世界的应用场景

讨论享元模式在现实世界的应用场景,除了前面提到的咖啡订单系统以外,还有很多其他例子可以借鉴:

  1. 字体渲染:在图形用户界面或者文字处理软件中,同一字体的不同实例可以共享字体文件数据,字体的尺寸、颜色、阴影等效果可以作为外部状态传递给字体享元对象。

  2. 图形渲染:在游戏开发中,大量的小颗粒物(如草地上的草叶、森林里的树叶等)可以共享同样的纹理和模型数据,而位置、旋转角度、缩放比例等作为外部状态传递。

  3. 数据库连接池:在Web服务器中,数据库连接是非常宝贵的资源,通过享元模式可以复用已建立的数据库连接,避免频繁创建和销毁连接带来的性能损耗。这里的连接对象就是享元对象,连接参数(数据库地址、用户名、密码等)则是外部状态。

  4. HTTP 请求缓存:在Web服务中,针对相同的URL发起的GET请求可以复用之前请求的结果,而不是每次都重新发送请求。这里HTTP请求结果可以看作享元对象,请求参数和URL作为外部状态。

在上述各个场景中,享元模式通过共享内部状态(那些不随环境变化而变化的部分)的实例,有效地节省了系统资源,提升了整体性能。同时,它通过分离内部状态和外部状态,使得系统能够灵活地应对各种复杂的业务场景。
python推荐学习汇总连接:
50个开发必备的Python经典脚本(1-10)

50个开发必备的Python经典脚本(11-20)

50个开发必备的Python经典脚本(21-30)

50个开发必备的Python经典脚本(31-40)

50个开发必备的Python经典脚本(41-50)
————————————————

​最后我们放松一下眼睛
在这里插入图片描述

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

极致人生-010

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值