一、概述
举两个例子。
例子1, A找牛二做了一个轻量小型门户网站,牛二花了几天做好后,租了服务器a,完工。后来,B看到了,觉得不错,也找牛二做了一个这样的网站,牛二就复制了之前的那份代码,租了服务器b,完工。后来C、D......
例子2, 做一个围棋游戏,围棋由许许多多的黑子和白子组成,面向对象的思想,每个棋子都是一个对象,这样就需要生成许许多多的对象。A玩这个游戏,生成了n个棋子,如果这时B也玩这个游戏,又要生成n个棋子......
从上面两个例子可以看出什么问题呢?
1. 资源过度浪费。
2. 重复代码过多,冗余严重。
3. 维护困难。
这个时候不禁想,如果资源可以共享就好了,反正都是一样的“东西”,就像网盘上的共享文件一样,需要的时候去下载一下就好,每个人都可以去下载,但共享文件只有一份,这样不是更好。
那么如何做到共享呢?
二、享元模式
1. 定义
享元模式,运用共享技术有效地支持大量细粒度的对象。(引自《大话设计模式》)
2. 说明
其实享元模式就是运用共享技术有效地支持大量细粒度对象的复用。对于相似或相同的享元对象我们可以重复使用,我们将这些对象存储的地方称之为享元池。这样,相同的对象在共享池中只存在一个,用的时候从共享池中取即可。避免资源的浪费。
这里面涉及到两个概念,内部状态和外部状态。
内部状态(Intrinsic State):在享元对象内部且不会随着环境的改变而改变的状态,是可以共享的部分。
外部状态(Extrinsic State):会随着环境改变而改变的状态,且不可共享的部分。
共享模式就是为了解决程序中大量生成细粒度的类实例来表示数据,当这些对象除了少数几个参数外基本上都是相同的时候,我们就可以设计成共享模式,将相同的参数设为类对象的内部状态,个别不同的参数设为类对象的外部状态,在方法调用时再传进来,这样就达到了共享的目的,可以减少生成大量的实例。
3. 基本结构图
Flyweight:享元类。可以为抽象类或接口,声明了具体享元类需要实现的方法,通过这个类可以向外界提供享元对象的内部数据(内部状态) ,也可以接受并作用外部数据(外部状态)。在享元类中,通常将内部状态(intrinsicState)和外部状态(extrinsicState)分开处理,内部状态作为享元类的内部成员,外部状态通过注入的方式添加到享元类中。
ConcreteFlyweight:具体享元类,继承Flyweight抽象类或实现Flyweight接口,为内部状态增加存储空间。
UnsharedConcreteFlyweight:非共享具体享元类,指那些不需要共享的Flyweight子类。这里并不强制共享,如果不需要共享时可通过非共享具体享元类创建对象。
FlyweightFactory:享元工厂类,就是一个工厂类,用来创建并管理Flyweight对象。结合了工厂模式的设计,主要是用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。
4. 基本代码
Flyweight,共享抽象类,这里包含了内部状态和外部状态,当有多个内部状态时,可以通过实体类的方式引入。
public abstract class Flyweight {
//内部状态
protected String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
//外部状态通过注入的方式传入共享类中
public abstract void operation(String extrinsicState);
}
ConcreteFlyweight
public class ConcreteFlyweight extends Flyweight {
public ConcreteFlyweight(String intrinsicState) {
super(intrinsicState);
}
@Override
public void operation(String extrinsicState) {
System.out.println("具体享元类,内部状态:"+intrinsicState+",外部状态:"+extrinsicState);
}
}
UnsharedConcreteFlyweight
public class UnsharedConcreteFlyweight extends Flyweight {
public UnsharedConcreteFlyweight(String intrinsicState) {
super(intrinsicState);
}
@Override
public void operation(String extrinsicState) {
System.out.println("不共享的具体享元类,内部状态:"+intrinsicState+",外部状态:"+extrinsicState);
}
}
FlyweightFactory
public class FlyweightFactory {
//通过map存储共享对象,实现享元池
private Map flyweights = new HashMap();
public Flyweight getFlyweight(String key) {
//如果对象存在,则直接从享元池中取出返回
if(flyweights.containsKey(key)) {
return (Flyweight) flyweights.get(key);
} else {//如果对象不存在,则创建新对象返回,并将新对象存储到享元池中
Flyweight f = new ConcreteFlyweight(key);
flyweights.put(key, f);
return f;
}
}
public void count() {
System.out.println("享元池的大小:"+flyweights.size());
}
}
测试类
public class FlyweightTest {
public static void main(String[] args) {
//创建工厂
FlyweightFactory factory = new FlyweightFactory();
//生成一个内部状态为inside、外部状态为outside的共享对象
Flyweight fw = factory.getFlyweight("inside");
fw.operation("outside");
//生成一个内部状态为inside、外部状态为outside2的共享对象
Flyweight fw2 = factory.getFlyweight("inside");
fw2.operation("outside2");
//生成一个内部状态为inside2、外部状态为outside的共享对象
Flyweight fw3 = factory.getFlyweight("inside2");
fw3.operation("outside2");
//共享池中共享对象的个数
factory.count();
}
}
结果
通过这个结果可以很清晰地看出,享元对象的个数与内部状态有关,而与外部状态无关。也就是说,当内部状态相同时,我们不会重复实例化对象,直接从共享池中取出。
三、实战应用
就用上面的网站的例子吧。
先分析一下内部状态和外部状态,网站的类型是内部状态,如oa系统、博客等,因为所有的oa系统可以用同一个模板,所有的博客网站可以用同一个模板。网站的账户是外部状态,这个很好理解,满足不同的用户嘛。
外部状态类User,这里将外部状态独立出来,方便设置多个外部状态属性,这里只包含“网站的账户名”这一个属性。
/**
* 外部状态类
*/
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
网站抽象类Website,内部状态为抽象类的属性,外部状态从外部注入。
public abstract class Website {
//内部状态
protected String type;
public Website(String type) {
this.type = type;
}
//外部状态通过注入的方式传入共享类中
public abstract void operation(User user);
}
具体网站实现类
public class ConcreteWebsite extends Website {
public ConcreteWebsite(String type) {
super(type);
}
@Override
public void operation(User user) {
System.out.println("具体享元类,内部状态:"+type+",外部状态:"+user.getName());
}
}
网站工厂类
public class WebsiteFactory {
//通过map存储共享对象,实现享元池
private Map flyweights = new HashMap();
public Website getWebsite(String key) {
//如果对象存在,则直接从享元池中取出返回
if(flyweights.containsKey(key)) {
return (Website) flyweights.get(key);
} else {//如果对象不存在,则创建新对象返回,并将新对象存储到享元池中
Website website = new ConcreteWebsite(key);
flyweights.put(key, website);
return website;
}
}
public void count() {
System.out.println("享元池的大小:"+flyweights.size());
}
}
测试类
public class FlyweightTest {
public static void main(String[] args) {
//创建工厂
WebsiteFactory factory = new WebsiteFactory();
User user = new User();
//获取类型(内部状态)为"oa系统"的共享对象
Website oa1 = factory.getWebsite("oa系统");
user.setName("小A");//设置用户名(外部状态)为小A
oa1.operation(user);
//获取类型(内部状态)为"oa系统"的共享对象
Website oa2 = factory.getWebsite("oa系统");
user.setName("小B");//设置用户名(外部状态)为小B
oa2.operation(user);
//获取类型(内部状态)为"oa系统"的共享对象
Website oa3 = factory.getWebsite("oa系统");
user.setName("小C");//设置用户名(外部状态)为小C
oa3.operation(user);
//同理,获取类型为"博客"的共享对象
Website c1 = factory.getWebsite("博客");
user.setName("大a");//设置用户名(外部状态)为小a
c1.operation(user);
Website c2 = factory.getWebsite("博客");
user.setName("大b");
c2.operation(user);
Website c3 = factory.getWebsite("博客");
user.setName("大c");
c3.operation(user);
//共享池中共享对象的个数
factory.count();
}
}
结果
从结果可以看出,相同内部状态的享元对象只有一个,即使外部状态不同也不会多次创建。
四、总结
1. 使用场景
- 当一个系统中存在大量相似或相同的对象,并且这些大量的对象可以对系统造成很大的开销。
- 当对象的大多数状态为外部状态,且如果删除这些外部状态后,大多数对象变得相同,可以用一个共享对象取代时,可以考虑享元模式。
2. 总结
- 享元模式可以减少系统中对象的数量,节省资源和空间。
- 享元模式区分内部状态和外部状态,需要仔细区分哪些是外部状态,哪些是内部状态,具有一定难度和复杂度。
- 外部状态相对独立,即使更改了也不会影响到内部状态。
- 内部状态相对稳定,属于共享部分,存储于享元对象内部。外部状态属于变化部分,由外部注入。
- 享元模式的核心在于共享工厂,由工厂来进行合理地创建共享对象。
写在最后,
本文主要是小猫看了《大话设计模式》的一些记录笔记,再加之自己的一些理解整理出此文,方便以后查阅,仅供参考。