1.享元模式概念
1.1定义
享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
- 享元模式(Flyweight Pattern)也叫绳量模式:运用共享技术有效地支持大量细粒度的对象
- 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的,就可以直接拿来使用,避免重复创建,如果没有需要的就创建一个。
- 享元模式能够解决重复对象的内存浪费问题,当系统中有大量的相似对象,需要缓冲池时,不需要总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率。
- 享元模式经典的应用场景就是池技术了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。
1.2内部状态与外部状态
享元模式以共享的方式高效地支持大量细粒度对象的重用,能做到共享的关键是区分了内部状态以及外部状态。
- 内部状态:存储在享元对象内部并且不会随环境改变而改变,内部状态可以共享,例如字符的内容,字符a永远是字符a,不会变为字符b
- 外部状态:能够随环境改变而改变,不可以共享的状态,通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。外部状态之间通常是相互独立的,比如字符的颜色,字号,字体等,可以独立变化,没有影响,客户端在使用时将外部状态注入到享元对象中
正因为区分了内部状态以及外部状态,可以将具有相同内部状态的对象存储在享元池中,享元池的对象是可以实现共享的,需要的时候从中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象实际上只存储一份。
1.3原理类图
角色说明:
- Flyweights(抽象享元类):通常是一个接口或者抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
- ConcreteFlyweight(具体享元类):实现/继承抽象共享类,实例称为共享对象,在具体享元类中为内部状态提供了存储空间,通常可以结合单例模式来设计具体享元类
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元子类都需要被共享,不能被共享的子类可设计为非共享具体享元类,当需要一个非具体享元对象时可以直接实例化创建
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,针对抽象享元类编程,将具体享元对象存储于享元池中。一般使用键值对集合(比如Java中的HashMap)作为享元池,当客户端获取享元对象时,首先判断是否存在,存在则从集合中取出并返回,不存在则创建新具体享元的实例,存储于享元池中并返回新实例
1.4分类
- 单纯享元模式:在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。
- 复合享元模式:将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享
1.5.典型实现
在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
典型的享元工厂类的代码如下:
class FlyweightFactory {
//定义一个HashMap用于存储享元对象,实现享元池
private HashMap<String,Flyweight> flyweights = new HashMap<>();
public static Flyweight getFlyweight(String key){
//如果对象存在,则直接从享元池获取
if(flyweights.containsKey(key)){
return(Flyweight)flyweights.get(key);
}
//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回
else {
Flyweight fw = newConcreteFlyweight();
flyweights.put(key,fw);
return fw;
}
}
}
享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中。
典型的享元类代码如下所示:
1 public abstract class Flyweight {
2
3 //内部状态
4 public String intrinsic;
5 //外部状态
6 protected final String extrinsic;
7
8 //要求享元角色必须接受外部状态
9 public Flyweight(String extrinsic) {
10 this.extrinsic = extrinsic;
11 }
12
13 //定义业务操作
14 public abstract void operate(int extrinsic);
15
16 public String getIntrinsic() {
17 return intrinsic;
18 }
19
20 public void setIntrinsic(String intrinsic) {
21 this.intrinsic = intrinsic;
22 }
23
24 }
ConcreteFlyweight类继承Flyweight超类或实现Flyweight接口,并为其内部状态增加存储空间。
1 public class ConcreteFlyweight extends Flyweight {
2
3 //接受外部状态
4 public ConcreteFlyweight(String extrinsic) {
5 super(extrinsic);
6 }
7
8 //根据外部状态进行逻辑处理
9 @Override
10 public void operate(int extrinsic) {
11 System.out.println("具体Flyweight:" + extrinsic);
12 }
13
14 }
UnsharedConcreteFlyweight类:指那些不需要共享的Flyweight子类。
1 public class UnsharedConcreteFlyweight extends Flyweight {
2
3 public UnsharedConcreteFlyweight(String extrinsic) {
4 super(extrinsic);
5 }
6
7 @Override
8 public void operate(int extrinsic) {
9 System.out.println("不共享的具体Flyweight:" + extrinsic);
10 }
11
12 }
Client客户端
1 public class Client {
2
3 public static void main(String[] args) {
4 int extrinsic = 22;
5
6 Flyweight flyweightX = FlyweightFactory.getFlyweight("X");
7 flyweightX.operate(++ extrinsic);
8
9 Flyweight flyweightY = FlyweightFactory.getFlyweight("Y");
10 flyweightY.operate(++ extrinsic);
11
12 Flyweight flyweightZ = FlyweightFactory.getFlyweight("Z");
13 flyweightZ.operate(++ extrinsic);
14
15 Flyweight flyweightReX = FlyweightFactory.getFlyweight("X");
16 flyweightReX.operate(++ extrinsic);
17
18 Flyweight unsharedFlyweight = new UnsharedConcreteFlyweight("X");
19 unsharedFlyweight.operate(++ extrinsic);
20 }
21
22 }
2.享元模式的实现
比如接了我一个小型的外包项目,是做一个产品展示网站,后来他的朋友们也希望做这样的网站,但要求都有些不同,我们当然不能直接复制粘贴再来一份,有任希望是新闻发布形式的,有人希望是博客形式的等等,而且因为经费原因不能每个网站租用一个空间。
其实这里他们需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费。如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源;而对于代码,由于是一份实例,维护和扩展都更加容易。
那么此时就可以用到享元模式了。类图如下:
网站抽象类
public abstract class WebSite {
public abstract void user(User user);
}
外部状态
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
具体网站类
//具体网站
public class ConcreteWebSite extends WebSite{
//共享的部分 内部状态
private String type=""; //发布的类型
public ConcreteWebSite(String type) {
this.type = type;
}
@Override
public void user(User user) {
System.out.println("网站的发布形式为:"+type+",使用者是:"+user.getName());
}
}
网站工厂类
//网站工厂类
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 (WebSite)pool.get(type);
}
public void getPoolCount(){
System.out.println(pool.size());
}
}
客户端
public class Client {
public static void main(String[] args) {
//创建一个工厂
WebSiteFactory factory=new WebSiteFactory();
WebSite webSite1=factory.getWebSiteCategory("新闻");
webSite1.user(new User("tom"));
WebSite webSite2=factory.getWebSiteCategory("博客");
webSite2.user(new User("king"));
WebSite webSite3=factory.getWebSiteCategory("微信");
webSite3.user(new User("lisi"));
WebSite webSite4=factory.getWebSiteCategory("掘金");
webSite4.user(new User("zhangsan"));
WebSite webSite5=factory.getWebSiteCategory("掘金");
webSite5.user(new User("wangwu"));
WebSite webSite6=factory.getWebSiteCategory("掘金");
webSite6.user(new User("wangwu"));
factory.getPoolCount();
}
}
运行结果
3.享元模式的优缺点和适用场景
主要优点
- 降低内存消耗:享元模式可以极大地减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而节约系统资源,提供系统性能
- 外部状态独立:享元模式外部状态相对独立,不会影响到内部状态,从而使得享元对象可以在不同环境中被共享
主要缺点
- 增加复杂度:享元模式使得系统变复杂,需要分离出内部状态以及外部状态,使得程序逻辑复杂化
- 运行时间变长:为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态使得运行时间变长
适用场景
- 一个系统有大量相似或相同对象,造成大量内存浪费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 由于需要维护享元池,造成一定的资源开销,因此在需要真正多次重复使用享元对象时才值得使用享元模式