设计模式之享元模式
一、意图
在面向对象系统的设计和实现中,创建对象是最为常见的操作。这里面就有一个问题:如果一个应用程序使用了太多的对象,就会造成很大的存储开销。特别是对于大量轻量级(细粒度)的对象,比如在文档编辑器的设计过程中,我们如果为每个字母创建一个对象的话,系统可能会因为大量的对象而造成存储开销的浪费。
例如一个字母“a”在文档中出现了10000次,而实际上我们可以让这一万个字母“a”共享一个对象,当然因为在不同的位置可能字母“a”有不同的显示效果(例如字体和大小等设置不同),在这种情况下我们可以将对象的状态分为“外部状态”和“内部状态”,将可以被共享(不会变化)的状态作为内部状态存储在对象中,而外部状态(例如上面提到的字体、大小等)我们可以在适当的时候将外部对象作为参数传递给对象(例如在显示的时候,将字体、大小等信息传递给对象)。
- 享元模式(Flyweight Pattern) 也叫蝇量模式,是一种结构型模式,“享”就表示共享,“元”表示对象。运用共享技术有效地支持大量细粒度的对象,享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时,不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率。
- 解决思路:复用对象最简单的方式是,用一个 HashMap 来存放每次新生成的对象。每次需要一个对象的时候,先到 HashMap 中看看有没有,如果没有,再生成新的对象,然后将这个对象放入 HashMap 中。
二、角色职责
-
Flyweight(抽象享元角色):享元对象的抽象基类或者接口,声明具体享元角色需要实现的方法,这些方法可以向外界提供对象的内部状态的操作,设置外部状态。
- 内部状态(intrinsic):是
享元对象可共享的属性
,存储在享元对象内部并且不会随环境改变而改变。 - 外部状态(extrinsic):是
对象得以依赖的一个标记
,是随环境改变而改变的、不可以共享的状态。
- 内部状态(intrinsic):是
-
ConcreteFlyweight( 具体享元角色):
Flyweight的具体实现
,实现抽象角色定义的方法。 为内部状态提供成员变量进行存储。 -
UnsharedConcreteFlyweigh( 非共享的享元角色):
不能被共享的子类
,可以设计为非共享享元类。 -
FlyweightFactory(享元工厂角色): 负责管理享元对象池和创建享元对象,就是构造一个池容器,同时提供从池中获得对象的方法,
- 当请求获取一个享元对象时,享元工厂判断是否存在该享元对象,如果存在则返回;如果不存在的话,则创建一个新的享元对象,保存到池容器中,然后返回给请求者。
- 享元池一般设计成键值对。
- 享元模式一般都是和工厂模式一起出现
三、类图
四、代码实现
场景:现有一外包公司,帮客户A做了一个产品展示网站,网站做好后更多客户觉得效果不错,也希望做个类似网站,但不同的是有客户要求以网页形式发布、有客户要求以微信公众号形式发布、有客户希望以博客形式发布。合理设计达到代码复用,灵活易维护扩展。
一般解法
直接复制粘贴一份,然后再根据客户不同要求,进行定制修改,给每一个网站租用了一个空间。
示意图如下:
问题分析:
首先需要的网站结构相似度很高(设普通网站,非高访问大并发),如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,造成服务器的资源浪费。
解决思路:
全部整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源。对于代码来说,由于是一份实例,维护和扩展都更加容易=》享元模式。
- 抽象类:
public abstract class WebSite {
public abstract void use(User user);
}
- 抽象实现子类
public class ConcreteWebSite extends WebSite{
//内部状态
private String type="";//网站的发布形式
//创建网站的构造器
public ConcreteWebSite(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println("网站的发布形式为:"+type+",使用者为:"+user.getName());
}
}
- 享元工厂类
import java.util.HashMap;
public class WebSiteFactory {
//集合,充当池容器的作用
private HashMap<String,ConcreteWebSite> pool = new HashMap<>();
//根据网站的类型,返回一个网站,如果没有就创建一个网站放入池中,并返回
public WebSite getWebSiteCategory(String type){
if(!pool.containsKey(type)){
pool.put(type,new ConcreteWebSite(type));
}
return pool.get(type);
}
//获取网站分类的总数 (池中有多少个网站类型)
public int getWebSiteCount(){
return pool.size();
}
}
- 不可共享类
@Data
public class User {//外部状态
private String name;
public User(String name) {
this.name = name;
}
}
- 客户端调用类
public class Client {
public static void main(String[] args) {
//创建一个工厂实例
WebSiteFactory webSiteFactory = new WebSiteFactory();
WebSite webSite1 = webSiteFactory.getWebSiteCategory("网页");
webSite1.user(new User("客户A"));
WebSite webSite2 = webSiteFactory.getWebSiteCategory("微信公众号");
webSite2.user(new User("客户B"));
WebSite webSite3 = webSiteFactory.getWebSiteCategory("微信公众号");
webSite3.user(new User("客户C"));
WebSite webSite4 = webSiteFactory.getWebSiteCategory("微信公众号");
webSite4.user(new User("客户D"));
System.out.println("网站的分类共:"+webSiteFactory.getWebSiteCount()+"种");
System.out.println("webSite1-"+webSite1.hashCode());
System.out.println("webSite2-"+webSite2.hashCode());
System.out.println("webSite3-"+webSite3.hashCode());
System.out.println("webSite4-"+webSite4.hashCode());
}
五、享元模式的应用场景
有大量相同或者相似的对象
,造成内存的大量耗费- 对象的
大部分状态都可以外部化
,可以将这些外部状态传入对象中。 - 需要缓冲池的场景
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
六、总结
享元模式就是使相同的对象共享使用。
(一)优点
-
极大
减少内存中对象的数量
,相同或相似对象内存中只存一份,降低程序内存的占用,增强程序的性能。 -
外部状态相对独立,不影响内部状态
(二)缺点
- 提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态不应该随内部状态改变而改变,否则导致系统的逻辑混乱。