設計模式之享元模式
享元模式定義
享元模式(Flyweight Pattern)是池技術的重要實現方式,使用共享物件可有效地支援大量的細粒度的物件。
通用類圖
具體實現
當應用程式中存在大量的物件時,程式的效能會有所下降,而且可能會造成記憶體溢位。這時除了增加硬體資源或者借用NoSQL資料庫儲存等方式外,我們還可以使用享元模式來解決問題。
同一型別的物件往往屬性值各部相同,但某些屬性的取值範圍是確定的。比如說student類中屬性grade(年級)和class(班級)它們的取值範圍在一個特定的環境中是確定的,對於某個小學來說grade的取值是 1~ 5,class的取值是1~7 。我們可以把grade+class作為物件的一個標記,是一批物件的標識,比如一年級一班。對於像name、age和address等資訊作為物件共享的部分,它可以在程式中動態新增和改變。這樣的話就可以把程式中幾千個物件縮減為5x7=35個物件(這裡只是舉個例子,實際程式中當然不會只有這麼少的物件)。我們把動態改變的屬性稱為內部狀態(name,age,address),不能共享的、用於作為唯一索引的屬性稱為外部狀態(grade+class)。
Flyweight類:抽象享元角色
package com.yrs.flyweight; /** * @Author: yangrusheng * @Description: 抽象享元角色 * @Date: Created in 16:37 2018/10/20 * @Modified By: */ public abstract class Flyweight { //內部狀態 private String intrinsic; //外部狀態 private final String extrinsic; //要求享元角色必須接受外部狀態 public Flyweight(String extrinsic) { this.extrinsic = extrinsic; } //定義業務操作 public abstract void operate(); public String getIntrinsic() { return intrinsic; } public void setIntrinsic(String intrinsic) { this.intrinsic = intrinsic; } }
ConcreteFlyweight類:具體享元角色
package com.yrs.flyweight; /** * @Author: yangrusheng * @Description: 具體享元角色 * @Date: Created in 16:42 2018/10/20 * @Modified By: */ public class ConcreteFlyweitht1 extends Flyweight { public ConcreteFlyweitht1(String extrinsic) { super(extrinsic); } @Override public void operate() { System.out.println(this.getClass().getName() + " " + this.getIntrinsic()); } }
package com.yrs.flyweight;
/**
* @Author: yangrusheng
* @Description: 具體享元角色
* @Date: Created in 16:42 2018/10/20
* @Modified By:
*/
public class ConcreteFlyweitht2 extends Flyweight {
public ConcreteFlyweitht2(String extrinsic) {
super(extrinsic);
}
@Override
public void operate() {
System.out.println(this.getClass().getName() + " " + this.getIntrinsic());
}
}
FlyweightFactory類:享元工廠,主要是構造一個池容器,同時提供從池中獲取物件的方法。
package com.yrs.flyweight;
import java.util.HashMap;
/**
* @Author: yangrusheng
* @Description: 享元工廠
* @Date: Created in 16:45 2018/10/20
* @Modified By:
*/
public class FlyweightFactory {
//定義一個池容器
private static HashMap<String, Flyweight> pool = new HashMap<String, Flyweight>();
//享元工廠
public static Flyweight getFlyweight(String extrinsic) {
//需要返回的物件
Flyweight flyweight = null;
//判斷池中是否有該物件
if (pool.containsKey(extrinsic)) {
System.out.println("從池中獲取");
flyweight = pool.get(extrinsic);
} else {
System.out.println("新建物件");
//根據外部狀態建立享元物件
flyweight = new ConcreteFlyweitht1(extrinsic);
//放入池中
pool.put(extrinsic, flyweight);
}
return flyweight;
}
}
Client類:客戶類
package com.yrs.flyweight;
/**
* @Author: yangrusheng
* @Description:
* @Date: Created in 16:59 2018/10/20
* @Modified By:
*/
public class Client {
public static void main(String[] args) {
Flyweight flyweight1 = FlyweightFactory.getFlyweight("yrs");
flyweight1.setIntrinsic("25");
flyweight1.operate();
Flyweight flyweight2 = FlyweightFactory.getFlyweight("yrs");
flyweight2.operate();
flyweight2.setIntrinsic("26");
flyweight2.operate();
}
}
優缺點
大大減少應用程式建立的物件,降低程式記憶體的佔用,增強程式的效能,但它同時也提高了系統複雜性,需要分離出外部狀態和內部狀態,而且外部狀態具有固化特性,不應該隨內部狀態改變而改變,否則導致系統的邏輯混亂。
使用場景:
- 系統中存在大量的相似物件。
- 細粒度的物件都具備較接近的外部狀態,而且內部狀態與環境無關,也就是說物件沒有特定身份。
- 需要緩衝池的場景。
享元模式延伸
執行緒安全問題
當我們把大量的物件縮減為較少的物件時,就會面臨執行緒安全問題。因為可能多個執行緒會同時操作外部索引為 “一年級一班” 的物件,執行緒A把物件的age值改為7後要進行後續的操作時,執行緒B把該物件的age的值改為了8。這時就會造成系統資料錯誤等問題。如何避免呢?可能需要通過加鎖或者使物件池中的物件儘可能多等機制來保證執行緒安全。
效能平衡
可能會有人問可不可以把外部索引作為一個單獨的類,然後在共享屬性類裡作為成員變數來作為索引標識。從技術實現上看當然可以,但是從程式效能上看會降低效能。這從何說起呢?在享元工廠類中儲存物件池,客戶類從物件池中依據外部索引獲取物件,這時需要從HashMap中找出等於外部索引的物件。如果外部索引是一個物件,比較時就要用到equals和hashCode方法,執行效率就比較低。如果外部索引是Java基本型別的話,就可以很快的比較值是否相等。
參考
- 《設計模式之禪-第2版》