2、19享元/蝇绳模式
定义
1)享元模式是池技术的重要实现方式。运用共享对象技术可有效地支持大量的细粒度的对象。享元模式的定义提出了两个要求:细粒度对象与共享对象。
2)要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那么我们就将这些对象的信息分为两个部分:内部状态与外部状态。
2.1)内部状态
内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变,如例子中的id、postAddress等,它们可以作为一个对象的动态附加信息,不必直接储存在具体某个对象中,属于可以共享的部分。
2.2)外部状态
外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,如例子中的”考试科目+考试地点”复合字符串,它是一批对象的统一标识,是唯一的一个索引值。
3)在享元对象内部并且不会随环境改变而改变的共享部分,可以称为是享元对象的内部状态,而随环境改变而改变的、不可以共享的状态就是外部状态了。事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。
通用类图
1)Flyweight抽象享元角色
产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。
2)ConcreteFlyweight具体享元角色
具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。
3)unsharedConcreteFlyweight不可共享的享元角色
不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。
4)FlyweightFactory享元工厂
职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。
通用案例
import java.util.HashMap;
/**
* 场景类
*/
public class Client1 {
public static void main(String[] args) {
System.out.println(FlyweightFactory.getFlyweight("test1"));
}
}
/**
* 抽象享元角色
* TODO 抽象享元角色一般为抽象类,在实际项目中,一般是一个实现类,它是描述一类事物的方法.
* TODO 在抽象角色中,一般需要把外部状态和内部状态(当然了,也可以没有内部状态,只有行为也是可以的.)定义出来,避免子类的随意扩展.
*/
abstract class Flyweight{
//内部状态
private String intrinsic;
//外部状态
//注意这里加上了final关键字,如果无意修改,那么池就混乱了.
//在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是session级的常量或变量.
protected final String extrinsic;
//要求享元角色必须接受外部状态
public Flyweight(String extrinsic) {
this.extrinsic = extrinsic;
}
//定义业务操作
abstract void operate();
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}
/**
* 具体享元角色
*/
class ConcreteFlyweight1 extends Flyweight{
//接受外部状态
public ConcreteFlyweight1(String extrinsic) {
super(extrinsic);
}
//根据外部状态进行逻辑处理
@Override
void operate() {
//业务逻辑
}
}
/**
* 具体享元角色
*/
class ConcreteFlyweight2 extends Flyweight{
//接受外部状态
public ConcreteFlyweight2(String extrinsic) {
super(extrinsic);
}
//根据外部状态进行逻辑处理
@Override
void operate() {
//业务逻辑
}
}
/**
* 享元工厂
* TODO 设计一个共享对象池的功能:
* TODO (把对象的相同属性提取出来,不同的属性在系统内进行赋值处理,那么就可以建立一个对象池了.)
* TODO 1)容器定义(定义一个池容器,在这个容器中容纳哪些对象)
* TODO 2)提供客户端访问的接口(提供一个接口供客户端访问,池中有可用对象时,可以直接从池中获得,否则建立一个新的对象,并放置到池中.)
*/
class FlyweightFactory{
//定义一个池容器
private static HashMap<String,Flyweight> pool = new HashMap<>();
//享元工厂
public static Flyweight getFlyweight(String extrinsic){
//需要返回的对象
Flyweight flyweight;
//在池中没有该对象
if(pool.containsKey(extrinsic)){
flyweight = pool.get(extrinsic);
}else{
//根据外部状态创建享元对象
flyweight = new ConcreteFlyweight1(extrinsic);
//放置到池中
pool.put(extrinsic,flyweight);
}
return flyweight;
}
}
线程安全问题
类图
案例
import java.util.HashMap;
/**
* 线程安全问题场景类
* TODO 在需要的地方考虑一下线程安全。对象池中的对象尽量多,多到足够满足业务为止
*/
public class Client3 {
public static void main(String[] args) {
//在对象池中初始化四个对象
SignInfoFactory.getSignInfo("科目1");
SignInfoFactory.getSignInfo("科目2");
SignInfoFactory.getSignInfo("科目3");
SignInfoFactory.getSignInfo("科目4");
//取得对象
SignInfo signInfo = SignInfoFactory.getSignInfo("科目4");
while(true){
signInfo.setId("zhangsan");
signInfo.setLocation("zhangsan");
new MultiThread(signInfo).start();
signInfo.setId("lisi");
signInfo.setLocation("lisi");
new MultiThread(signInfo).start();
}
}
}
class SignInfoFactory {
//池容器
private static HashMap<String,SignInfo> pool = new HashMap<>();
//从池中获取对象
public static SignInfo getSignInfo(String key){
//设置返回对象
SignInfo result;
//池中若没有该对象,则建立,并放入池中
if(!pool.containsKey(key)){
result = new SignInfo();
pool.put(key,result);
}else{
result = pool.get(key);
}
return result;
}
}
class MultiThread extends Thread{
private SignInfo signInfo;
public MultiThread(SignInfo signInfo){
this.signInfo = signInfo;
}
@Override
public void run() {
if(!signInfo.getId().equals(signInfo.getLocation())){
System.out.println("编号:" + signInfo.getId());
System.out.println("考试地点:" + signInfo.getLocation());
System.out.println("线程不安全了!");
}
}
}
class SignInfo{
//报名人员的ID
private String id;
//考试地点
private String location;
//考试科目
private String subject;
//邮寄地址
private String postAddress;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getPostAddress() {
return postAddress;
}
public void setPostAddress(String postAddress) {
this.postAddress = postAddress;
}
}
编号:lisi
考试地点:zhangsan
线程不安全了!
编号:zhangsan
考试地点:lisi
线程不安全了!
性能平衡
类图1
案例1(以ExtrinsicState类作为外部状态)
import java.util.HashMap;
/**
* 场景类
* TODO 以ExtrinsicState类作为外部状态
*/
public class Client4 {
public static void main(String[] args) {
//初始化对象池
ExtrinsicState state1 = new ExtrinsicState();
state1.setSubject("科目1");
state1.setLocation("上海1");
SignInfoFactory.getSignInfo(state1);
ExtrinsicState state2 = new ExtrinsicState();
state2.setSubject("科目2");
state2.setLocation("北京2");
SignInfoFactory.getSignInfo(state2);
//计算执行100万次需要的时间
long currentTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
SignInfoFactory.getSignInfo(state2);
}
long tailTime = System.currentTimeMillis();
System.out.println("执行时间:" + (tailTime - currentTime) + "ms");
}
}
/**
* 享元工厂
*/
class SignInfoFactory{
//池容器
private static HashMap<ExtrinsicState,SignInfo> pool = new HashMap<>();
//从池中获取对象
public static SignInfo getSignInfo(ExtrinsicState key){
//设置返回对象
SignInfo result;
//池中没有该对象,则建立,并放入池中。
if(!pool.containsKey(key)){
result = new SignInfo();
result.setState(key);
pool.put(key,result);
}else{
result = pool.get(key);
}
return result;
}
}
/**
* 外部状态类
* TODO 如果把一个对象作为MAP类的键值,一定要确保重写了equals和hashCode方法,只有hashCode值相等,并且equals返回结果为true,两个对象才相等。
* TODO 否则会出现通过键值搜索失败的情况,例如map.get(object)、map.contains(object)等会返回失败的结果。
*/
class ExtrinsicState{
//考试科目
private String subject;
//考试地点
private String location;
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof ExtrinsicState){
ExtrinsicState state = (ExtrinsicState)obj;
return state.getLocation().equals(this.location) && state.getSubject().equals(this.subject);
}
return false;
}
@Override
public int hashCode() {
return this.subject.hashCode() + this.location.hashCode();
}
}
/**
* 报考信息
*/
class SignInfo{
//报名人员的ID
private String id;
//外部状态对象
private ExtrinsicState state;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public ExtrinsicState getState() {
return state;
}
public void setState(ExtrinsicState state) {
this.state = state;
}
}
输出结果:
执行时间:236ms
类图2
案例2-推荐性能高(以java基本类型等(这里String)作为外部状态)
import java.util.HashMap;
/**
* 场景类
* TODO 以java基本类型等(这里String)作为外部状态。
* TODO 外部状态最好以java的基本类型作为标识,可以大幅地提升效率。
*/
public class Client5 {
public static void main(String[] args) {
String key1 = "科目1";
String key2 = "上海2";
//初始化对象池
SignInfoFactory.getSignInfo(key1);
//计算执行100万次需要的时间
long currentTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
SignInfoFactory.getSignInfo(key2);
}
long tailTime = System.currentTimeMillis();
System.out.println("执行时间:" + (tailTime - currentTime) + "ms");
}
}
/**
* 享元工厂
*/
class SignInfoFactory{
//池容器
private static HashMap<String,SignInfo> pool = new HashMap<>();
//从池中获取对象
public static SignInfo getSignInfo(String key){
//设置返回对象
SignInfo result;
//池中没有该对象,则建立,并放入池中。
if(!pool.containsKey(key)){
result = new SignInfo();
result.setState(key);
pool.put(key,result);
}else{
result = pool.get(key);
}
return result;
}
}
/**
* 报考信息
*/
class SignInfo{
//报名人员的ID
private String id;
//考试地点
private String location;
//考试科目
private String subject;
//邮寄地址
private String postAddress;
//外部状态对象
private String state;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getPostAddress() {
return postAddress;
}
public void setPostAddress(String postAddress) {
this.postAddress = postAddress;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
输出结果:
执行时间:65ms
API中的享元/蝇绳模式
/**
* 场景类
* TODO 虽然可以使用享元模式实现对象池,但是享元模式和对象池还有是比较大的区别的.
* TODO 对象池着重在对象的复用上,池中的每个对象都是可替换的,从同一个池中获得A对象和B对象对客户端来说是完全相同的,它主要解决复用。
* TODO 享元模式主要解决的是对象的共享问题,如果建立多个可共享的细粒度对象则是其关注的重点.
*/
public class Client6 {
public static void main(String[] args) {
String str1 = "和谐";
String str2 = "社会";
String str3 = "和谐社会";
String str4;
str4 = str1 + str2;
System.out.println(str3 == str4);
//String类中的intern()方法,如果是String对象池中有该类型的值,则直接返回对象池中的对象.
//比如字符串把它的引用指向XX在内存中的共享。
str4 = (str1 + str2).intern();
System.out.println(str3 == str4);
}
}
优点和缺点
1)享元模式是一个非常简单的模式,它可以大大减少应用程序创建的对象,降低程序内存的占用,增加程序的性能,但它同时也提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。
2)减少运行时对象实例的个数,节省内存。
3)将许多”虚拟”对象的状态集中管理。
4)单个的逻辑实例将无法拥有独立而不同的行为。(个人理解:多个的逻辑实例就可以。足够多的实例在内存中,参考线程安全问题实例)
使用场景
1)系统中存在大量的相似对象。
2)细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
3)需要缓冲池的场景。
4)类要轻量级,粒度要小。粒度小了,带来的问题就是对象太多,那就用共享技术来解决。
5)如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。
6)效果:因为使用了共享对象,实例总数就大大减少了,如果共享的对象越多,存储节约也就越多,节约量随着共享状态的增多而增大。
7)使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。因此,应当在有足够多的对象实例可供共享时才值得使用享元模式。
8)当一个类有许多的实例,而这些实例能被同一个方法(比如工厂类等)控制的时候,就可以使用享元模式。
标题:设计模式(十九)结构型模式(对象结构型模式)享元/蝇绳模式-FLYWEIGHT
作者:yazong
地址:https://blog.llyweb.com/articles/2020/08/28/1598547445111.html