设计之禅——享元模式

引言

之前已经写了17篇关于设计模式的文章,而这些设计模式大都是为了降低代码之间的耦合,避免违反开闭原则,但它们大都有同样的一个缺点,产生更多的类和对象,如果数量达到一定程度,就会导致系统性能降低,而今天要讲的这个模式就是为了解决这样的一个问题,它就是享元模式。

正文

定义

享元模式是为了尽可能地划分出细粒度对象并复用,减少内存资源的占用,因而它是一种优化系统性能的模式。

为什么是细粒度的对象复用呢?因为对象越大,对象的复用率并不高(在系统运行时你不可能总是会使用一些很大的对象),如果将其缓存下来,对系统而言反而是负担。并且有可能那些特大的对象是我们自己因为方便而创造出来的,承担了很多的责任,而你常用的可能只是其中的一部分功能,所以我们需要尽可能的划分出细粒度对象并缓存起来。因其轻量级特性,享元模式又称为蝇量模式

好吧,那照你这么说,我只需要缓存对象就行了,难道这就是享元模式么?

当然不是。将对象细粒度划分后,有其固有不变的属性,也有些属性可能会随着环境的改变而改变,如果将这些属性都放到一个对象里,那还怎么复用这个对象呢?享元模式则是将属性进行划分,固有属性放在对象内部,称为内在属性,而会变化的部分则放在对象的外部,称为外在属性,并通过方法参数传递进去,这样我们就能复用这个对象了。

你这么说我好像懂了,但还是很模糊。

是的,概念总是抽象的,我举个例子。比如你有一张图片,需要显示在电脑桌面的不同位置,那么只需要将坐标位置属性抽离出来,根据你的设置动态显示就行了,这里坐标就是外部会变化的属性。

这样就很清楚了,那我在网上看到很多博客都说Java中的字符串就是享元模式的应用,你觉得呢?

我也看到了,但个人不太认同这样的观点,String只是利用了缓存,并不能说明使用了享元模式,并且字符串的固有属性和外部属性是什么呢?字符串本身内容吗?那使用享元模式实现就有些多此一举了。不过不用太纠结这一点,我们的重点是学习享元模式。

那应该如何实现享元模式呢?

我们先来看看其类图:
设计之禅——享元模式_第1张图片
从类图上看享元模式具有三个角色:

  • 抽象享元接口或抽象类
  • 具体享元类(需要共享的对象)
  • 享元工厂

享元模式常常需要配合工厂模式使用,使用工厂是为了维护一个共享对象池,将对象缓存到该池中,该池一般使用类似Map的键值对来实现,并提供一个方法供外部获取池中对象,如果池中没有该对象,就新建一个并放入池中。

说了这么多,快让我看代码吧!

好的,当然没问题。我想大多数人都知道CS这款游戏,曾经火遍全球。想想看如果每个角色都新建一个对象是不是很浪费内存呢?毕竟它们基本上一样,只是需要经常切换武器装备啊。这就可以使用享元来达到节省内存的目的了啊,就像下面这样:

Coding

public abstract class AbstractPlayer {

    private String weapon;
    protected String mission;


    public void assignWeapon(String weapon) {
        System.out.println("使用武器:" + weapon);
        this.weapon = weapon;
    }

    public void execute() {
        System.out.println("execute mission: " + mission);
    }
}

public class Police extends AbstractPlayer {

    public Police() {
        this.mission = "kill terrorist!";
    }

}

public class Terrorist extends AbstractPlayer {

    public Terrorist() {
        this.mission = "kill police!";
    }
}

首先我创建了一个抽象的玩家角色类,并实现了土匪和警察两个具体的类,对于这两类玩家而言,任务是不会改变的:土匪杀警察,警察杀土匪。而对所有角色而言,武器是随时都会更换的,因此抽离到外部并通过参数传入进来。而对象的创建工厂如下:

public class PlayerFactory {

    private static Map<String, AbstractPlayer> pool = new HashMap<>();

    public static AbstractPlayer getPlayer(String type) throws Exception {
        AbstractPlayer player = pool.get(type);
        if (player == null) {
            switch (type) {
                case "P":
                    System.out.println("Create police: ");
                    player = new Police();
                    break;
                case "T":
                    System.out.println("Create terrorist: ");
                    player = new Terrorist();
                    break;
                default:
                    throw new Exception("无此类型的玩家!");
            }
        }
        return player;
    }

}

很简单,玩家调用getPlayer方法并传入对应的标识创建警察或是土匪。

public class CS {

    // 角色表示
    private static String[] players = {"T", "P"};
    // 武器类型
    private static String[] weapons = {"AK-47", "Knife", "Sniper"};

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            AbstractPlayer player = PlayerFactory.getPlayer(getPlayer(i));
            player.assignWeapon(getWeapon(i));

            player.execute();
        }
    }

    /**
     * 随机创建土匪或是警察
     */
    private static String getPlayer(int i) {
        return players[i % players.length];
    }

    /**
     * 随机获取武器
     */
    private static String getWeapon(int i) {
        return weapons[i % weapons.length];
    }

}

这里看起来实现非常简单,但在实际开发中,要精确把控区分可共享域和不可共享域是非常难的,并且这里只见到了单纯享元模式,另外还有复合享元模式,笔者不再打算展开讲述,感兴趣的可自行查阅资料。

总结

享元模式在提高对象复用性,节省内存方面非常有用,但在复用度不高时,并不建议使用享元模式,因为享元工厂本身需要维护一个共享池,浪费资源。并且,需要将变化属性外部化,使得程序逻辑变得复杂难以理解,同时,外部属性也会导致运行时间变长。

参考

  • GeeksForGeeks
  • 享元模式

你可能感兴趣的:(设计之禅——享元模式)