设计模式与性能优化:享元模式减少内存,代理模式延迟加载,策略模式避免条件分支
性能优化,说白了就是跟资源较劲。内存、CPU、加载时间,哪个不是我们天天要打交道的?我做了这么多年架构,发现一个有意思的现象:很多性能问题,其实不是算法不够快,而是设计上埋了坑。今天聊的三个模式——享元、代理、策略,恰好对应了三种最常见的性能痛点。
享元模式:别让对象撑爆你的内存
先说说享元模式。这个模式的名字挺文艺,但本质很朴素:共享细粒度对象,避免重复创建。
我记得有一次接手一个游戏后端项目,玩家创建角色时,系统要为每个角色生成一套装备属性模板。几万个玩家同时在线,内存直接飙到 2GB 多。一查才发现,同样的装备属性被反复实例化——每个玩家手里的「铁剑」都是独立对象。
享元模式怎么解决?把对象拆成两部分:
- 内部状态(Intrinsic State):可共享的、不变的部分。比如装备的ID、基础攻击力、图标路径。
- 外部状态(Extrinsic State):依赖上下文的、可变的部分。比如装备的当前耐久度、强化等级。
看个简化代码:
// 享元对象:装备模板
class EquipmentFlyweight {
public:
EquipmentFlyweight(int id, const string& name, int baseAtk)
: id_(id), name_(name), baseAtk_(baseAtk) {}
void display(int durability, int level) const {
// 内部状态 + 外部状态组合输出
cout << name_ << " 攻击力:" << baseAtk_
<< " 耐久:" << durability
<< " 强化:" << level << endl;
}
private:
int id_;
string name_;
int baseAtk_;
};
// 享元工厂
class EquipmentFactory {
public:
shared_ptr<EquipmentFlyweight> getFlyweight(int id) {
auto it = pool_.find(id);
if (it != pool_.end()) {
return it->second;
}
// 实际项目中从配置表加载
auto fw = make_shared<EquipmentFlyweight>(id, "铁剑", 10);
pool_[id] = fw;
return fw;
}
private:
unordered_map<int, shared_ptr<EquipmentFlyweight>> pool_;
};
核心要点:享元模式适合大量相似对象的场景。但别滥用——如果对象本身差异很大,共享反而增加复杂度。
我的习惯:在写工厂类时,我会先问自己:「这个对象有多少属性是全局不变的?」如果超过一半,就值得用享元。
代理模式:别让大对象拖慢启动速度
代理模式,很多人只记得它做访问控制。其实它在性能优化上有个杀手级应用:延迟加载(Lazy Loading)。
我曾经维护过一个报表系统,每次启动都要加载所有数据源配置。有些数据源是历史归档,一个月才用一次。结果每次重启服务都要等 40 秒。后来用代理模式改造,只有真正查询某个数据源时,才去加载它的配置。
代理模式的结构很简单:
// 抽象主题
class DataSource {
public:
virtual ~DataSource() = default;
virtual void query(const string& sql) = 0;
};
// 真实主题:加载代价高
class RealDataSource : public DataSource {
public:
RealDataSource(const string& config) {
// 模拟耗时加载
this_thread::sleep_for(chrono::seconds(3));
cout << "数据源加载完成" << endl;
}
void query(const string& sql) override {
cout << "执行查询: " << sql << endl;
}
};
// 代理:延迟加载
class DataSourceProxy : public DataSource {
public:
DataSourceProxy(const string& config) : config_(config), real_(nullptr) {}
void query(const string& sql) override {
// 第一次使用时才真正加载
if (!real_) {
real_ = make_unique<RealDataSource>(config_);
}
real_->query(sql);
}
private:
string config_;
unique_ptr<RealDataSource> real_;
};
我曾经踩过的坑:代理模式如果用在多线程环境,一定要加锁或使用 double-checked locking。否则两个线程同时触发加载,会创建两个真实对象。
你想想看,代理模式本质上是在「做不做」和「什么时候做」之间做权衡。它不减少总工作量,但把工作推迟到真正需要的时候。这对启动性能、响应速度的提升非常明显。
策略模式:干掉又臭又长的 if-else
策略模式跟性能有什么关系?关系大了。条件分支越多,CPU 分支预测失败的代价就越高。而且代码一长,维护成本直线上升。
我见过最夸张的一段代码,一个函数里嵌套了 7 层 if-else,处理 20 多种支付方式。每次加新支付渠道,都要改这个函数,改完还要全量回归测试。
策略模式的做法:把每个分支封装成独立的策略类,通过多态来消除条件判断。
// 策略接口
class PaymentStrategy {
public:
virtual ~PaymentStrategy() = default;
virtual void pay(double amount) = 0;
};
// 具体策略
class AlipayStrategy : public PaymentStrategy {
public:
void pay(double amount) override {
cout << "支付宝支付: " << amount << endl;
}
};
class WechatStrategy : public PaymentStrategy {
public:
void pay(double amount) override {
cout << "微信支付: " << amount << endl;
}
};
// 上下文
class PaymentContext {
public:
void setStrategy(unique_ptr<PaymentStrategy> strategy) {
strategy_ = move(strategy);
}
void executePayment(double amount) {
if (strategy_) {
strategy_->pay(amount);
}
}
private:
unique_ptr<PaymentStrategy> strategy_;
};
性能对比:策略模式用虚函数调用替代了条件分支。虚函数调用有微小开销(一次间接跳转),但相比分支预测失败带来的流水线冲刷,这点代价几乎可以忽略。更重要的是,代码的可维护性提升了几个量级。
嗯,这里要注意:策略模式不是让你把所有 if-else 都干掉。如果只有两三个分支,而且不会频繁变化,用 if-else 反而更直接。我个人的判断标准是:当分支数量超过 5 个,或者每个分支的逻辑超过 10 行时,就该考虑策略模式了。
三种模式的协同
这三种模式不是孤立的。我在一个项目中同时用过它们:
- 享元模式:缓存策略对象的元数据,避免重复创建
- 代理模式:延迟加载策略对象,只在第一次使用时才实例化
- 策略模式:根据运行时条件选择不同的算法
说白了,性能优化不是堆技巧,而是理解资源的本质。享元管内存,代理管时间,策略管复杂度。把这三个维度管好了,你的架构自然就优雅了。
避坑指南:我曾经在享元模式里把外部状态也塞进了共享对象,结果导致一个玩家修改装备耐久,所有玩家的装备都变了。记住:内部状态必须不可变,外部状态由客户端管理。