设计模式与性能优化:享元模式减少对象、代理模式延迟加载、装饰器模式避免继承爆炸

性能优化这件事,说白了就是跟资源较劲。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组件

实际项目中,这三种模式经常配合使用。比如一个图片查看器:用享元模式共享缩略图缓存,用代理模式延迟加载高清图,用装饰器模式给图片添加滤镜效果。

我的建议:不要为了用模式而用模式。先分析性能瓶颈在哪,再选择合适的模式。有时候一个简单的缓存就能解决问题,没必要上享元。

知识体系图

下面这张图展示了三种模式在性能优化中的定位和关系:

设计模式与性能优化 享元模式 代理模式 装饰器模式 共享内部状态 延迟加载 组合代替继承 减少对象数量 降低启动时间 避免类爆炸 字符渲染、粒子系统 大图加载、配置管理 IO流、UI组件

这三种模式,说白了就是三种不同的优化思路。享元模式省内存,代理模式省时间,装饰器模式省代码。实际项目中,我经常把它们组合使用。比如一个大型文档系统:用享元共享样式定义,用代理延迟加载图片,用装饰器动态添加批注功能。

嗯,最后提醒一句:性能优化要量化。别凭感觉说「这个模式能提升性能」,先跑个基准测试,看看瓶颈到底在哪。我曾经见过有人为了用享元模式重构了整个系统,结果性能反而下降了——因为共享对象的访问冲突成了新瓶颈。

选对模式,用对地方,才是真正的性能优化之道。

公众号:蓝海资料掘金营,微信deep3321