Skip to the content.

Flyweight (享元) 模式

享元模式,作用于会产生大量对象的场景,其实现原理是使用缓存,避免创建过多的、重复的对象并提升程序性能。

简单来说,当我们需要一个对象时,一般都是使用 new 来创建一个对象。但是实际情况却是,如果在一段时间内有大量的请求的话,就会产生大量的对象,可是这些对象的内容都大同小异,如果反复的创建、销毁对象,必将影响程序的性能。而享元模式的作用就是用来解决这个问题的。

其实在这里关于缓存的实现可以理解为对“池”的实现,我们都知道,在 String 类中有常量池,在线程中有线程池,它们的作用都是避免创建过多的对象以达到提升性能的目的。所以,在享元模式中,我们将实现一个对象池。一个经典的实现就是使用键值对( Map )。我们以对象之间相同的部分(内在状态)作为键,以不同的部分(外在状态)作为值,当我们从持有缓存的享元工厂中获取享元对象的时候,先判断时候存在我们需要的键,若没有则创建一个新的对象放入缓存中,若存在则使用缓存的对象,以此来达到避免创建过多对象的目的。

Flyweight (享元) 模式的 UML 类图

flyweight pattern

简单的看一下实现:

首先是定义一个享元对象接口:Flyweight

public interface Flyweight {

    public void operation(String extrinsicState);

}

这里要传入的参数就是该享元对象的外部状态 (Extrinsic State),即享元对象间根据情况的不同而会变化的部分

接着是实现了接口的具体的享元对象:ConcreteFlyweight

public class ConcreteFlyweight implements Flyweight{

    private String intrinsicState;

    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void operation(String extrinsicState) {
        System.out.println(intrinsicState + " : " + extrinsicState);
    }
}

这里的全局变量就是该享元对象的内部状态 (Intrinsic State),即享元对象间不会随着使用情况的不同而变化的部分

最后就是实现享元工厂:FlyweightFactory

public class FlyweightFactory {

    private static Map<String, Flyweight> map = new ConcurrentHashMap<>();

    public static Flyweight getFlyweight(String key) {
        if (map.containsKey(key)) {
            System.out.println("use cache:");
            return map.get(key);
        } else {
            System.out.println("create new:");
            Flyweight flyweight = new ConcreteFlyweight(key);
            map.put(key, flyweight);
            return flyweight;
        }
    }

}

在这一块使用 Map 集合来缓存对象,在获取享元对象的时候会判断是否存在该对象的缓存,若存在则直接获取,否则就创建一个新的对象。这里为了显示这两部分的效果,输出了中间结果

那么接下来就是测试一下

Flyweight flyweight1 = FlyweightFactory.getFlyweight("key1");
flyweight1.operation("value1");
Flyweight flyweight2 = FlyweightFactory.getFlyweight("key1");
flyweight2.operation("value2");
Flyweight flyweight3 = FlyweightFactory.getFlyweight("key1");
flyweight3.operation("value3");

测试结果如下

create new:
key1 : value1
use cache:
key1 : value2
use cache:
key1 : value3

可见,在使用除了第一次使用需要创建对象之外,接下来获取该 key 的对象的时候都不需要创建对象

享元模式的简单实现下

正如上面所说,享元模式适用于可能存在大量重复对象的场景,所以,诸如订票系统这类例子。因为同一类型的票会产生很多份,那么对应的重复对象的创建和销毁就会产生,这将导致程序性能下降和内存占用率的上升

接下来就以订购电影票为例子。简单的分析一下,一张电影票大概含有一下几个元素:电影名,时间,座位和价格,这其中电影名是重复的,座位和时间根据情况的不同是不一样的。

首先就是关于票的接口:Ticket

public interface Ticket {

    public void printTicket(String time, String seat);

}

printTicket() 方法是根据时间和作为打印出相应的电影票

然后就是具体的电影票:MovieTicket

public class MovieTicket implements Ticket {

    private String movieName;
    private String price;

    public MovieTicket(String movieName) {
        this.movieName = movieName;
        price = "Price " + new Random().nextInt(100);
    }

    @Override
    public void printTicket(String time, String seat) {
        System.out.println("+-------------------+");
        System.out.printf("| %-12s    |\n", movieName);
        System.out.println("|                   |");
        System.out.printf("|       %-12s|\n", time);
        System.out.printf("|       %-12s|\n", seat);
        System.out.printf("|       %-12s|\n", price);
        System.out.println("|                   |");
        System.out.println("+-------------------+");
    }
}

在创建这电影票的对象时,需要根据电影名来创建,也就是说电影名是作为享元对象的键。这里为了效果,在 printTicket() 方法中,格式化打印输出了一张电影票大概的样子。

那么享元工厂跟之前写的差不了多少:TicketFactory

public class TicketFactory {

    private static Map<String, Ticket> map = new ConcurrentHashMap<>();

    public static Ticket getTicket(String movieName) {
        if (map.containsKey(movieName)) {
            return map.get(movieName);
        } else {
            Ticket ticket = new MovieTicket(movieName);
            map.put(movieName, ticket);
            return ticket;
        }
    }

}

接下来就可以测试一下了

MovieTicket movieTicket1 = (MovieTicket) TicketFactory.getTicket("Transformers 5");
movieTicket1.printTicket("14:00-16:30", "Seat  D-5");
MovieTicket movieTicket2 = (MovieTicket) TicketFactory.getTicket("Transformers 5");
movieTicket2.printTicket("14:00-16:30", "Seat  F-6");
MovieTicket movieTicket3 = (MovieTicket) TicketFactory.getTicket("Transformers 5");
movieTicket3.printTicket("18:00-22:30", "Seat  A-2");

相同的电影名,但是根据不同的时间和座位号打印出不同的票

+-------------------+
| Transformers 5    |
|                   |
|       14:00-16:30 |
|       Seat  D-5   |
|       Price 4     |
|                   |
+-------------------+
+-------------------+
| Transformers 5    |
|                   |
|       14:00-16:30 |
|       Seat  F-6   |
|       Price 30    |
|                   |
+-------------------+
+-------------------+
| Transformers 5    |
|                   |
|       18:00-22:30 |
|       Seat  A-2   |
|       Price 29    |
|                   |
+-------------------+

总结

虽然享元模式的实现非常简单,但是它的应用场景十分重要。

END.