设计模式与性能优化:享元模式减少对象、代理模式延迟加载、装饰器模式避免继承爆炸
性能优化这件事,说白了就是跟资源较劲。CPU、内存、IO,哪个都是稀缺货。我做了十几年系统架构,见过太多因为对象太多把内存撑爆的案例,也见过继承层次深到没人敢改的代码。今天聊的三个模式——享元、代理、装饰器,恰好从三个不同角度解决性能问题。
享元模式:别重复造轮子,共享才是王道
先讲享元模式。这个模式的核心思想很简单:能共享的就别新建。
我记得有个项目,需要渲染几十万个字符。每个字符都创建一个对象?内存直接炸了。后来我用享元模式,把字符的字体、大小、颜色这些内部状态提取出来共享,只保留位置这种外部状态。结果对象数量从几十万降到了几十个。
来看个实际例子。假设我们要做一个文本编辑器,每个字符都是一个对象:
// Java 实现
// 享元接口
interface Character {
void display(int x, int y);
}
// 具体享元类
class ConcreteCharacter implements Character {
private char symbol; // 内部状态
private String font; // 内部状态
private int size; // 内部状态
public ConcreteCharacter(char symbol, String font, int size) {
this.symbol = symbol;
this.font = font;
this.size = size;
}
@Override
public void display(int x, int y) {
System.out.println("渲染字符 '" + symbol + "' 在 (" + x + "," + y + ")");
}
}
// 享元工厂
class CharacterFactory {
private Map<String, Character> pool = new HashMap<>();
public Character getCharacter(char symbol, String font, int size) {
String key = symbol + ":" + font + ":" + size;
Character character = pool.get(key);
if (character == null) {
character = new ConcreteCharacter(symbol, font, size);
pool.put(key, character);
}
return character;
}
}
你想想看,如果没有享元模式,10万个字符就要创建10万个对象。用了享元,相同字体和大小的字符只存一份。内存占用直接降到原来的千分之一。
代理模式:能拖就拖,延迟加载的艺术
代理模式解决的是另一个问题:别急着加载,用到再说。
我曾经接手过一个项目,启动时要加载几十个配置文件。实际上,大部分配置根本用不到。启动时间长达3分钟,用户都快疯了。后来我用代理模式做延迟加载,启动时间降到了10秒。
代理模式的核心就是给真实对象找个替身。客户端跟替身打交道,替身只在真正需要时才创建真实对象。
// C++ 实现
class Image {
public:
virtual void display() = 0;
virtual ~Image() {}
};
class RealImage : public Image {
private:
std::string filename;
void loadFromDisk() {
std::cout << "从磁盘加载图片: " << filename << std::endl;
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
}
public:
RealImage(const std::string& file) : filename(file) {
loadFromDisk();
}
void display() override {
std::cout << "显示图片: " << filename << std::endl;
}
};
class ProxyImage : public Image {
private:
RealImage* realImage;
std::string filename;
public:
ProxyImage(const std::string& file) : filename(file), realImage(nullptr) {}
~ProxyImage() {
delete realImage;
}
void display() override {
// 延迟加载:只在第一次调用时创建真实对象
if (realImage == nullptr) {
realImage = new RealImage(filename);
}
realImage->display();
}
};
这里的关键是:ProxyImage 在构造时并不加载图片,只有调用 display() 时才真正加载。如果用户从来不看某张图片,那它就永远不会被加载。
装饰器模式:组合优于继承,别让类爆炸
最后说装饰器模式。这个模式解决的是继承爆炸问题。
我见过最夸张的代码:一个咖啡类,有加糖、加奶、加巧克力、加奶油……各种组合。结果呢?类数量呈指数级增长。加糖咖啡、加奶咖啡、加糖加奶咖啡、加糖加巧克力咖啡……光命名就让人崩溃。
装饰器模式的做法是:用组合代替继承。把功能拆成一个个装饰器,像搭积木一样组合起来。
// Java 实现
// 抽象组件
interface Coffee {
double cost();
String getDescription();
}
// 具体组件
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 10.0;
}
@Override
public String getDescription() {
return "普通咖啡";
}
}
// 抽象装饰器
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 3.0;
}
@Override
public String getDescription() {
return super.getDescription() + " + 牛奶";
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 2.0;
}
@Override
public String getDescription() {
return super.getDescription() + " + 糖";
}
}
// 使用示例
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee); // 加牛奶
coffee = new SugarDecorator(coffee); // 加糖
System.out.println(coffee.getDescription() + " 价格: " + coffee.cost());
// 输出: 普通咖啡 + 牛奶 + 糖 价格: 15.0
你看,只需要3个类(咖啡、牛奶装饰器、糖装饰器),就能组合出任意口味。如果用继承,3种配料就需要8个类。配料越多,差距越大。
三种模式的对比与选择
这三种模式虽然都跟性能有关,但解决的问题不同。我整理了一个对比表:
| 模式 | 核心思想 | 性能收益 | 适用场景 |
|---|---|---|---|
| 享元模式 | 共享内部状态 | 减少对象数量,降低内存 | 大量细粒度对象,如字符渲染、粒子系统 |
| 代理模式 | 延迟加载 | 减少启动时间,按需加载 | 资源密集型对象,如大图、配置文件 |
| 装饰器模式 | 组合代替继承 | 避免类爆炸,降低维护成本 | 功能动态组合,如IO流、UI组件 |
实际项目中,这三种模式经常配合使用。比如一个图片查看器:用享元模式共享缩略图缓存,用代理模式延迟加载高清图,用装饰器模式给图片添加滤镜效果。
知识体系图
下面这张图展示了三种模式在性能优化中的定位和关系:
这三种模式,说白了就是三种不同的优化思路。享元模式省内存,代理模式省时间,装饰器模式省代码。实际项目中,我经常把它们组合使用。比如一个大型文档系统:用享元共享样式定义,用代理延迟加载图片,用装饰器动态添加批注功能。
嗯,最后提醒一句:性能优化要量化。别凭感觉说「这个模式能提升性能」,先跑个基准测试,看看瓶颈到底在哪。我曾经见过有人为了用享元模式重构了整个系统,结果性能反而下降了——因为共享对象的访问冲突成了新瓶颈。
选对模式,用对地方,才是真正的性能优化之道。