【白話設計模式二十一】享元模式(Flyweight)

#0 系列目錄#java

#1 場景問題# ##1.1 加入權限控制## 考慮這樣一個問題,給系統加入權限控制,這基本上是全部的應用系統都有的功能了。算法

對於應用系統而言,通常先要登陸系統,纔可使用系統的功能,登陸事後,用戶的每次操做都須要通過權限系統的控制,確保該用戶有操做該功能的權限,同時還要控制該用戶對數據的訪問權限、修改權限等等。總之一句話,一個安全的系統,須要對用戶的每一次操做都要作權限檢測,包括功能和數據,以確保只有得到相應受權的人,才能執行相應的功能,操做相應的數據數據庫

舉個例子來講吧:普通人員都有能查看到本部門人員列表的權限,可是在人員列表中每一個人員的薪資數據,普通人員是不能夠看到的;而部門經理在查看本部門人員列表的時候,就能夠看到每一個人員相應的薪資數據。編程

如今就要來實現爲系統加入權限控制的功能,該怎麼實現呢?設計模式

爲了讓你們更好的理解後面講述的知識,先介紹一點權限系統的基礎知識。幾乎全部的權限系統都分紅兩個部分,一個是受權部分,一個是驗證部分,爲了理解它們,首先解釋兩個基本的名詞:安全實體和權限緩存

安全實體:就是被權限系統檢測的對象,好比工資數據。安全

權限:就是須要被校驗的權限對象,好比查看、修改等。服務器

安全實體和權限一般要一塊兒描述纔有意義,好比有這麼個描述:「如今要檢測登陸人員對工資數據是否有查看的權限」, 「工資數據」這個安全實體和「查看」這個權限必定要一塊兒描述。若是隻出現安全實體描述,那就變成這樣:「如今要檢測登陸人員對工資數據」,對工資數據幹什麼呀,沒有後半截,一看就知道不完整;固然只有權限描述也不行,那就變成:「如今要檢測登陸人員是否有查看的權限」,對誰的查看權限啊,也不完整。因此安全實體和權限一般要一塊兒描述。session

瞭解了上面兩個名詞,來看看什麼是受權和驗證:數據結構

所謂受權是指:把對某些安全實體的某些權限分配給某些人員的過程。

所謂驗證是指:判斷某我的員對某個安全實體是否擁有某個或某些權限的過程。

也就是說,受權過程便是權限的分配過程,而驗證過程則是權限的匹配過程。在目前應用系統的開發中,多數是利用數據庫來存放受權過程產生的數據,也就是說:受權是向數據庫裏面添加數據、或是維護數據的過程,而匹配過程就變成了從數據庫中獲取相應數據進行匹配的過程了。

爲了讓問題相對簡化一點,就不去考慮權限的另外兩個特徵,一個是繼承性,一個是最近匹配原則,都什麼意思呢,仍是解釋一下:

  1. 權限的繼承性指的是:若是多個安全實體存在包含關係,而某個安全實體沒有相應的權限限制,那麼它會繼承包含它的安全實體的相應權限。

好比:某個大樓和樓內的房間都是安全實體,很明顯大樓這個安全實體會包含樓內的房間這些安全實體,能夠認爲大樓是樓內房間的父級實體。如今來考慮一個具體的權限——進入某個房間的權限。若是這個房間沒有門,也就是誰均可以進入,至關於這個房間對應的安全實體,沒有進入房間的權限限制,那麼是否是說全部的人均可以進入這個房間呢?固然不是,某人能進入這個房間的前提是:這我的要有權限進入這個大樓,也就是說,這個時候房間這個安全實體,它自己沒有進入權限的限制,可是它會繼承父級安全實體的進入權限。

  1. 權限的最近匹配原則指的是:若是多個安全實體存在包含關係,而某個安全實體沒有相應的權限限制,那麼它會向上尋找並匹配相應權限限制,直到找到一個離這個安全實體最近的擁有相應權限限制的安全實體爲止。若是把整個層次結構都尋找完了都沒有匹配到相應權限限制的話,那就說明全部人對這個安全實體都擁有這個相應的權限限制。

繼續上面權限繼承性的例子,若是如今這個大樓是坐落在某個機關大院內,這就演變成了,要進入某個房間,首先要有進入大樓的權限,要進入大樓又須要有能進入機關大院的權限。

所謂最近匹配原則就是,若是某個房間沒有門,也就意味着這個房間沒有進入的權限限制,那麼它就會向上繼續尋找並匹配,看看大樓有沒有進入的權限限制,若是有就使用這個權限限制,終止尋找;若是沒有,繼續向上尋找,直到找到一個匹配的爲止。若是最後大院也沒有進入的權限限制,那就變成全部人均可以進入到這個房間裏面來了。

##1.2 不使用模式的解決方案##

  1. 看看如今都已經有什麼了

系統的受權工做已經完成,受權數據記錄在數據庫裏面,具體的數據結構就不去展開了,反正裏面記錄了人員對安全實體所擁有的權限。假如如今系統中已有以下的受權數據:

張三  對  人員列表   擁有    查看的權限
李四  對  人員列表   擁有    查看的權限
李四  對  薪資數據   擁有    查看的權限
李四  對  薪資數據   擁有    修改的權限
  1. 思路選擇

因爲操做人員進行受權操做事後,各人員被授予的權限是記錄在數據庫中的,剛開始有開發人員提出,每次用戶操做系統的時候,都直接到數據庫裏面去動態查詢,以判斷該人員是否擁有相應的權限,但很快就被否決掉了,試想一下,用戶操做那麼頻繁,每次都到數據庫裏面動態查詢,這會嚴重加重數據庫服務器的負擔,使系統變慢。

爲了加快系統運行的速度,開發小組決定採用必定的緩存,當每一個人員登陸的時候,就把該人員能操做的權限獲取到,存儲在內存中,這樣每次操做的時候,就直接在內存裏面進行權限的校驗,速度會大大加快,這是典型的以空間換時間的作法。

  1. 實現示例

(1)首先定義描述受權數據的數據對象,示例代碼以下:

/**
 * 描述受權數據的數據model
 */
public class AuthorizationModel {
    /**
     * 人員
     */
    private String user;
    /**
     * 安全實體
     */
    private String securityEntity;
    /**
     * 權限
     */
    private String permit;
    public String getUser() {
       return user;
    }
    public void setUser(String user) {
       this.user = user;
    }
    public String getSecurityEntity() {
       return securityEntity;
    }
    public void setSecurityEntity(String securityEntity) {
       this.securityEntity = securityEntity;
    }
    public String getPermit() {
       return permit;
    }
    public void setPermit(String permit) {
       this.permit = permit;
    }
}

(2)爲了測試方便,作一個模擬的內存數據庫,把受權數據存儲在裏面,用最簡單的字符串存儲的方式。示例代碼以下:

/**
 * 供測試用,在內存中模擬數據庫中的值
 */
public class TestDB {
    /**
     * 用來存放受權數據的值
     */
    public static Collection<String> colDB = new ArrayList<String>();
    static{
       //經過靜態塊來填充模擬的數據    
       colDB.add("張三,人員列表,查看");
       colDB.add("李四,人員列表,查看");
       colDB.add("李四,薪資數據,查看");
       colDB.add("李四,薪資數據,修改");
       //增長更多的受權數據
       for(int i=0;i<3;i++){
           colDB.add("張三"+i+",人員列表,查看");
       }
    }  
}

(3)接下來實現登陸和權限控制的業務,示例代碼以下:

/**
 * 安全管理,實現成單例
 */
public class SecurityMgr {
    private static SecurityMgr securityMgr = new SecurityMgr();
    private SecurityMgr(){     
    }
    public static SecurityMgr getInstance(){
       return securityMgr;
    }
    /**
     * 在運行期間,用來存放登陸人員對應的權限,
     * 在Web應用中,這些數據一般會存放到session中
     */
    private Map<String,Collection<AuthorizationModel>> map = new HashMap<String,Collection<AuthorizationModel>>();
  
    /**
     * 模擬登陸的功能
     * @param user 登陸的用戶
     */
    public void login(String user){
       //登陸時就須要把該用戶所擁有的權限,從數據庫中取出來,放到緩存中去
       Collection<AuthorizationModel> col = queryByUser(user);
       map.put(user, col);
    }
    /**
     * 判斷某用戶對某個安全實體是否擁有某權限
     * @param user 被檢測權限的用戶
     * @param securityEntity 安全實體
     * @param permit 權限
     * @return true表示擁有相應權限,false表示沒有相應權限
     */
    public boolean hasPermit(String user,String securityEntity,String permit){
       Collection<AuthorizationModel> col = map.get(user);
       if(col==null || col.size()==0){
           System.out.println(user+"沒有登陸或是沒有被分配任何權限");
           return false;
       }
       for(AuthorizationModel am : col){
           //輸出當前實例,看看是否同一個實例對象
           System.out.println("am=="+am);
           if(am.getSecurityEntity().equals(securityEntity) && am.getPermit().equals(permit)){
              return true;
           }
       }
       return false;
    }
    /**
     * 從數據庫中獲取某人所擁有的權限
     * @param user 須要獲取所擁有的權限的人員
     * @return 某人所擁有的權限
     */
    private Collection<AuthorizationModel> queryByUser(String user){
       Collection<AuthorizationModel> col = new ArrayList<AuthorizationModel>();
       for(String s : TestDB.colDB){
           String ss[] = s.split(",");
           if(ss[0].equals(user)){
              AuthorizationModel am = new AuthorizationModel();
              am.setUser(ss[0]);
              am.setSecurityEntity(ss[1]);
              am.setPermit(ss[2]);
            
              col.add(am);
           }
       }
       return col;
    }
}

(4)好很差用呢,寫個客戶端來測試一下,示例代碼以下:

public class Client {
    public static void main(String[] args) {
       //須要先登陸,而後再判斷是否有權限
       SecurityMgr mgr = SecurityMgr.getInstance();
       mgr.login("張三");
       mgr.login("李四");   
       boolean f1 = mgr.hasPermit("張三","薪資數據","查看");
       boolean f2 = mgr.hasPermit("李四","薪資數據","查看");     
     
       System.out.println("f1=="+f1);
       System.out.println("f2=="+f2);
       for(int i=0;i<3;i++){
           mgr.login("張三"+i);
           mgr.hasPermit("張三"+i,"薪資數據","查看");
       }
    }
}

運行結果以下:

am==cn.javass.dp.flyweight.example1.AuthorizationModel@1eed786
am==cn.javass.dp.flyweight.example1.AuthorizationModel@187aeca
am==cn.javass.dp.flyweight.example1.AuthorizationModel@e48e1b
f1==false
f2==true
am==cn.javass.dp.flyweight.example1.AuthorizationModel@12dacd1
am==cn.javass.dp.flyweight.example1.AuthorizationModel@119298d
am==cn.javass.dp.flyweight.example1.AuthorizationModel@f72617

輸出結果中的f1爲false,表示張三對薪資數據沒有查看的權限;而f2爲true,表示李四對對薪資數據有查看的權限,是正確的,基本完成了功能。

##1.3 有何問題## 看了上面的實現,很簡單,並且還考慮了性能的問題,在內存中緩存了每一個人相應的權限數據,使得每次判斷權限的時候,速度大大加快,實現得挺不錯,難道有什麼問題嗎?

仔細想一想,問題就來了,既有緩存這種方式固有的問題,也有咱們本身實現上的問題。先說說緩存固有的問題吧,這個不在本次討論之列,你們瞭解一下。

  1. 緩存時間長度的問題

這些數據應該被緩存多久,若是是Web應用,這種跟登陸人員相關的權限數據,可能是放在session中進行緩存,這樣session超時的時候,就會被清除掉。若是不是Web應用呢?就得本身來控制了,另外就算是在Web應用中,也不必定非要緩存到session超時才清除。總之,控制緩存數據應該被緩存多長時間,是實現高效緩存的一個問題點。

  1. 緩存數據和真實數據的同步問題

這裏的同步是指的數據同步,不是多線程的同步。好比:上面的受權數據是存放在數據庫裏的,運行的時候緩存到內存裏面,若是真實的受權數據在運行期間發生了改變,那麼緩存裏的數據就應該和數據庫的數據同步,以保持一致,不然數據就錯了。如何合理的同步數據,也是實現高效緩存的一個問題點。

  1. 緩存的多線程併發控制

對於緩存的數據,有些操做從裏面取值,有些操做向緩存裏面添加值,有些操做在清除過時的緩存數據,有些操做在進行緩存和真實數據的同步,在一個多線程的環境下,如何合理的對緩存進行併發控制,也是實現高效緩存的一個問題點。

先簡單提這麼幾個,事實上,實現合理、高效的緩存也不是一件很輕鬆的事情,好在這些問題,都不在咱們此次的討論之列,這裏的重心仍是來說述模式,而不是緩存實現。

再來看看前面實現上的問題,仔細觀察在上面輸出結果中框住的部分,這些值是輸出對象實例獲得的,默認輸出的是對象的hashCode值,而默認的hashCode值能夠用來判斷是否是同一對象實例。在Java中,默認的equals方法比較的是內存地址,而equals方法和hashCode方法的關係是:equals方法返回true的話,那麼這兩個對象實例的hashCode必須相同;而hashCode相同,equals方法並不必定返回true,也就是說兩個對象實例不必定是同一對象實例。換句話說,若是hashCode不一樣的話,鐵定不是同一個對象實例

仔細看看上面輸出結果,框住部分的值是不一樣的,代表這些對象實例確定不是同一個對象實例,而是多個對象實例。這就引出一個問題了,就是對象實例數目太多,爲何這麼說呢?看看就描述這麼幾條數據,數數看有多少個對象實例呢?目前是一條數據就有一個對象實例,這很恐怖,數據庫的數據量是很大的,若是有幾萬條,幾十萬條,豈不是須要幾萬個,甚至幾十萬個對象實例,這會耗費掉大量的內存。

另外,這些對象的粒度都很小,都是簡單的描述某一個方面的對象,並且不少數據是重複的,在這些大量重複的數據上耗費掉了不少的內存。好比在前面示例的數據中就會發現有重複的部分,見下面框住的部分:

張三  對  人員列表   擁有    查看的權限
李四  對  人員列表   擁有    查看的權限
李四  對  薪資數據   擁有    查看的權限
李四  對  薪資數據   擁有    修改的權限

前面講過,對於安全實體和權限通常要聯合描述,所以對於「人員列表 這個安全實體 的 查看權限 限制」,就算是受權給不一樣的人員,這個描述是同樣的。假設在某極端狀況下,要把「人員列表 這個安全實體 的 查看權限 限制」受權給一萬我的,那麼數據庫裏面會有一萬條記錄,按照前面的實現方式,會有一萬個對象實例,而這些實例裏面,有大部分的數據是重複的,並且會重複一萬次,你以爲這是否是個很大的問題呢?

把上面的問題描述出來就是:在系統當中,存在大量的細粒度對象,並且存在大量的重複數據,嚴重耗費內存,如何解決?

#2 解決方案# ##2.1 享元模式來解決## 用來解決上述問題的一個合理的解決方案就是享元模式。那麼什麼是享元模式呢?

  1. 享元模式定義

輸入圖片說明

  1. 應用享元模式來解決的思路

仔細觀察和分析上面的受權信息,會發現有一些數據是重複出現的,好比:人員列表、薪資數據、查看、修改等等。至於人員相關的數據,考慮到每一個描述受權的對象都是和某我的員相關的,因此存放的時候,會把相同人員的受權信息組織在一塊兒,就不去考慮人員數據的重複性了。

如今形成內存浪費的主要緣由:就是細粒度對象太多,並且有大量重複的數據。若是可以有效的減小對象的數量,減小重複的數據,那麼就可以節省很多內存。一個基本的思路就是緩存這些包含着重複數據的對象,讓這些對象只出現一次,也就只耗費一分內存了

可是請注意,並非全部的對象都適合緩存,由於緩存的是對象的實例,實例裏面存放的主要是對象屬性的值。所以,若是被緩存的對象的屬性值常常變更,那就不適合緩存了,由於真實對象的屬性值變化了,那麼緩存裏面的對象也必需要跟着變化,不然緩存中的數據就跟真實對象的數據不一樣步,能夠說是錯誤的數據了。

所以,須要分離出被緩存對象實例中,哪些數據是不變且重複出現的,哪些數據是常常變化的,真正應該被緩存的數據是那些不變且重複出現的數據,把它們稱爲對象的內部狀態,而那些變化的數據就不緩存了,把它們稱爲對象的外部狀態

這樣在實現的時候,把內部狀態分離出來共享,稱之爲享元,經過共享享元對象來減小對內存的佔用把外部狀態分離出來,放到外部,讓應用在使用的時候進行維護,並在須要的時候傳遞給享元對象使用爲了控制對內部狀態的共享,而且讓外部能簡單的使用共享數據,提供一個工廠來管理享元,把它稱爲享元工廠

##2.2 模式結構和說明## 享元模式的結構如圖20.1所示:

輸入圖片說明

Flyweight:享元接口,經過這個接口flyweight能夠接受並做用於外部狀態。經過這個接口傳入外部的狀態,在享元對象的方法處理中可能會使用這些外部的數據。

ConcreteFlyweight:具體的享元實現對象,必須是可共享的,須要封裝flyweight的內部狀態。

UnsharedConcreteFlyweight:非共享的享元實現對象,並非全部的Flyweight實現對象都須要共享。非共享的享元實現對象一般是對共享享元對象的組合對象。

FlyweightFactory:享元工廠,主要用來建立並管理共享的享元對象,並對外提供訪問共享享元的接口。

Client:享元客戶端,主要的工做是維持一個對flyweight的引用,計算或存儲享元對象的外部狀態,固然這裏能夠訪問共享和不共享的flyweight對象。

##2.3 享元模式示例代碼##

  1. 先看享元的接口定義,經過這個接口flyweight能夠接受並做用於外部狀態,示例代碼以下:
/***
 * 享元接口,經過這個接口享元能夠接受並做用於外部狀態
 */
public interface Flyweight {
    /**
     * 示例操做,傳入外部狀態
     * @param extrinsicState 示例參數,外部狀態
     */
    public void operation(String extrinsicState);
}
  1. 接下來看看具體的享元接口的實現,先看共享享元的實現,封裝flyweight的內部狀態,固然也能夠提供功能方法,示例代碼以下:
/**
 * 享元對象
 */
public class ConcreteFlyweight implements Flyweight{
    /**
     * 示例,描述內部狀態
     */
    private String intrinsicState;
    /**
     * 構造方法,傳入享元對象的內部狀態的數據
     * @param state 享元對象的內部狀態的數據
     */
    public ConcreteFlyweight(String state){
       this.intrinsicState = state;
    }

    public void operation(String extrinsicState) {
       //具體的功能處理,可能會用到享元內部、外部的狀態
    }  
}

再看看不須要共享的享元對象的實現,並非全部的Flyweight對象都須要共享,Flyweight接口使共享成爲可能,但並不強制共享。示例代碼以下:

/**
 * 不須要共享的flyweight對象,
 * 一般是將被共享的享元對象做爲子節點,組合出來的對象
 */
public class UnsharedConcreteFlyweight implements Flyweight{
    /**
     * 示例,描述對象的狀態
     */
    private String allState;
  
    public void operation(String extrinsicState) {
       // 具體的功能處理
    }
}
  1. 在享元模式中,客戶端不能直接建立共享的享元對象實例,必須經過享元工廠來建立。接下來看看享元工廠的實現,示例代碼以下:
/**
 * 享元工廠
 */
public class FlyweightFactory {
    /**
     * 緩存多個flyweight對象,這裏只是示意一下
     */
    private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>();
    /**
     * 獲取key對應的享元對象
     * @param key 獲取享元對象的key,只是示意
     * @return key 對應的享元對象
     */
    public Flyweight getFlyweight(String key) {
       //這個方法裏面基本的實現步驟以下:      
       //1:先從緩存裏面查找,是否存在key對應的Flyweight對象
       Flyweight f = fsMap.get(key);

       //2:若是存在,就返回相對應的Flyweight對象
       if(f==null){
           //3:若是不存在
           //3.1:建立一個新的Flyweight對象
           f = new ConcreteFlyweight(key);
           //3.2:把這個新的Flyweight對象添加到緩存裏面
           fsMap.put(key,f);
           //3.3:而後返回這個新的Flyweight對象
       }

       return f;
    }
}
  1. 最後來看看客戶端的實現,客戶端一般會維持一個對flyweight的引用,計算或存儲一個或多個flyweight的外部狀態。示例代碼以下:
/**
 * Client對象,一般會維持一個對flyweight的引用,
 * 計算或存儲一個或多個flyweight的外部狀態
 */
public class Client {
    //具體的功能處理
}

##2.4 使用享元模式重寫示例## 再次分析上面的受權信息,實際上重複出現的數據主要是對安全實體和權限的描述,又考慮到安全實體和權限的描述通常是不分開的,那麼找出這些重複的描述,好比:人員列表的查看權限。並且這些重複的數據是能夠重用的,好比給它們配上不一樣的人員,就能夠組合成爲不一樣的受權描述,如圖20.2所示:

輸入圖片說明

圖20.2就能夠描述以下的信息:

張三  對  人員列表   擁有    查看的權限
李四  對  人員列表   擁有    查看的權限
王五  對  人員列表   擁有    查看的權限

很明顯,能夠把安全實體和權限的描述定義成爲享元,而和它們結合的人員數據,就能夠作爲享元的外部數據。爲了演示簡單,就把安全實體對象和權限對象簡化成了字符串,描述一下它們的名字。

  1. 按照享元模式,也爲了系統的擴展性和靈活性,給享元定義一個接口,外部使用享元仍是面向接口來編程,示例代碼以下:
/***
 * 描述受權數據的享元接口
 */
public interface Flyweight {
    /**
     * 判斷傳入的安全實體和權限,是否和享元對象內部狀態匹配
     * @param securityEntity 安全實體
     * @param permit 權限
     * @return true表示匹配,false表示不匹配
     */
    public boolean match(String securityEntity,String permit);
}
  1. 定義了享元接口,該來實現享元對象了,這個對象須要封裝受權數據中重複出現部分的數據,示例代碼以下:
/**
 * 封裝受權數據中重複出現部分的享元對象
 */
public class AuthorizationFlyweight implements Flyweight{
    /**
     * 內部狀態,安全實體
     */
    private String securityEntity;
    /**
     * 內部狀態,權限
     */
    private String permit;
    /**
     * 構造方法,傳入狀態數據
     * @param state 狀態數據,包含安全實體和權限的數據,用","分隔
     */
    public AuthorizationFlyweight(String state){
       String ss[] = state.split(",");
       securityEntity = ss[0];
       permit = ss[1];
    }
  
    public String getSecurityEntity() {
       return securityEntity;
    }
    public String getPermit() {
       return permit;
    }

    public boolean match(String securityEntity, String permit) {
       if(this.securityEntity.equals(securityEntity) && this.permit.equals(permit)){
           return true;
       }
       return false;
    }  
}
  1. 定義好了享元,來看看如何管理這些享元,提供享元工廠來負責享元對象的共享管理和對外提供訪問享元的接口

享元工廠通常不須要不少個,實現成爲單例便可享元工廠負責享元對象的建立和管理,基本的思路就是在享元工廠裏面緩存享元對象。在Java中最經常使用的緩存實現方式,就是定義一個Map來存放緩存的數據,而享元工廠對外提供的訪問享元的接口,基本上就是根據key值到緩存的Map中獲取相應的數據,這樣只要有了共享,同一份數據就能夠重複使用了,示例代碼以下:

/**
 * 享元工廠,一般實現成爲單例
 */
public class FlyweightFactory {
    private static FlyweightFactory factory = new FlyweightFactory();
    private FlyweightFactory(){    
    }
    public static FlyweightFactory getInstance(){
       return factory;
    }
    /**
     * 緩存多個flyweight對象
     */
    private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>();
    /**
     * 獲取key對應的享元對象
     * @param key 獲取享元對象的key
     * @return key對應的享元對象
     */
    public Flyweight getFlyweight(String key) {
       Flyweight f = fsMap.get(key);
       if(f==null){
           f = new AuthorizationFlyweight(key);
           fsMap.put(key,f);
       }
       return f;
    }
}
  1. 使用享元對象

實現完享元工廠,該來看看如何使用享元對象了。按照前面的實現,須要一個對象來提供安全管理的業務功能,就是前面的那個SecurityMgr類,這個類如今在享元模式中,就充當了Client的角色,注意這個Client角色和咱們平時說的測試客戶端是兩個概念,這個Client角色是使用享元的對象

SecurityMgr的實現方式基本上模仿前面的實現,也會有相應的改變,變化大體以下:

緩存的每一個人員的權限數據,類型變成了Flyweight的了;

在原來queryByUser方法裏面,經過new來建立受權對象的地方,修改爲了經過享元工廠來獲取享元對象,這是使用享元模式最重要的一點改變,也就是否是直接去建立對象實例,而是經過享元工廠來獲取享元對象實例;

示例代碼以下:

/**
 * 安全管理,實現成單例
 */
public class SecurityMgr {
    private static SecurityMgr securityMgr = new SecurityMgr();
    private SecurityMgr(){     
    }
    public static SecurityMgr getInstance(){
       return securityMgr;
    }  
    /**
     * 在運行期間,用來存放登陸人員對應的權限,
     * 在Web應用中,這些數據一般會存放到session中
     */
    private Map<String,Collection<Flyweight>> map = new HashMap<String,Collection<Flyweight>>();
    /**
     * 模擬登陸的功能
     * @param user 登陸的用戶
     */
    public void login(String user){
       //登陸時就須要把該用戶所擁有的權限,從數據庫中取出來,放到緩存中去
       Collection<Flyweight> col = queryByUser(user);
       map.put(user, col);
    }
    /**
     * 判斷某用戶對某個安全實體是否擁有某權限
     * @param user 被檢測權限的用戶
     * @param securityEntity 安全實體
     * @param permit 權限
     * @return true表示擁有相應權限,false表示沒有相應權限
     */
    public boolean hasPermit(String user,String securityEntity,String permit){
       Collection<Flyweight> col = map.get(user);
       if(col==null || col.size()==0){
           System.out.println(user+"沒有登陸或是沒有被分配任何權限");
           return false;
       }
       for(Flyweight fm : col){
           //輸出當前實例,看看是否同一個實例對象
           System.out.println("fm=="+fm);
           if(fm.match(securityEntity, permit)){
              return true;
           }
       }
       return false;
    }
    /**
     * 從數據庫中獲取某人所擁有的權限
     * @param user 須要獲取所擁有的權限的人員
     * @return 某人所擁有的權限
     */
    private Collection<Flyweight> queryByUser(String user){
       Collection<Flyweight> col = new ArrayList<Flyweight>();
       for(String s : TestDB.colDB){
           String ss[] = s.split(",");
           if(ss[0].equals(user)){
              Flyweight fm = FlyweightFactory.getInstance().getFlyweight(ss[1]+","+ss[2]);
              col.add(fm);
           }
       }
       return col;
    }
}
  1. 所用到的TestDB沒有任何變化,這裏就不去贅述了。

  2. 客戶端測試代碼也沒有任何變化,也不去贅述了。

運行測試一下,看看效果,主要是看看是否是能有效地減小那些重複數據對象的數量。運行結果以下:

fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@12dacd1
f1==false
f2==true
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b

仔細觀察結果中框住的部分,會發現六條數據中,有五條的hashCode是同一個值,根據咱們的實現,能夠判定這是同一個對象。也就是說,如今只有兩個對象實例,而前面的實現中有六個對象實例。

如同示例的那樣,對於封裝安全實體和權限的這些細粒度對象,既是受權分配的單元對象,也是權限檢測的單元對象。可能有不少人對某個安全實體擁有某個權限,若是爲每一個人都從新建立一個對象來描述對應的安全實體和權限,那樣就太浪費內存空間了。

經過共享封裝了安全實體和權限的對象,不管多少人擁有這個權限,實際的對象實例都是隻有一個,這樣既減小了對象的數目,又節省了寶貴的內存空間,從而解決了前面提出的問題。

#3 模式講解# ##3.1 認識享元模式##

  1. 變與不變

享元模式設計的重點就在於分離變與不變,把一個對象的狀態分紅內部狀態和外部狀態,內部狀態是不變的,外部狀態是可變的。而後經過共享不變的部分,達到減小對象數量、並節約內存的目的。在享元對象須要的時候,能夠從外部傳入外部狀態給共享的對象,共享對象會在功能處理的時候,使用本身內部的狀態和這些外部的狀態。

事實上,分離變與不變是軟件設計上最基本的方式之一,好比預留接口,爲何在這個地方要預留接口,一個常見的緣由就是這裏存在變化,可能在從此須要擴展、或者是改變已有的實現,所以預留接口作爲「可插入性的保證」。

  1. 共享與不共享

在享元模式中,享元對象又有共享與不共享之分,這種狀況一般出如今跟組合模式合用的狀況,一般共享的是葉子對象,通常不共享的部分是由共享部分組合而成的,因爲全部細粒度的葉子對象都已經緩存了,那麼緩存組合對象就沒有什麼意義了。這個在後面給你們一個示例。

  1. 內部狀態和外部狀態

享元模式的內部狀態,一般指的是包含在享元對象內部的、對象自己的狀態,一般是獨立於使用享元的場景的信息,通常建立事後就再也不變化的狀態,所以能夠共享。

外部狀態指的是享元對象以外的狀態,取決於使用享元的場景,會根據使用場景而變化,所以不可共享。若是享元對象須要這些外部狀態的話,能夠從外部傳遞到享元對象裏面,好比經過方法的參數來傳遞。

也就是說享元模式真正緩存和共享的數據是享元的內部狀態,而外部狀態是不該該被緩存共享的

另一點,內部狀態和外部狀態是獨立的,外部狀態的變化不該該影響到內部狀態。

  1. 實例池

在享元模式中,爲了建立和管理共享的享元部分,引入了享元工廠,享元工廠中通常都包含有享元對象的實例池,享元對象就是緩存在這個實例池中的

簡單介紹一點實例池的知識,所謂實例池,指的是緩存和管理對象實例的程序,一般實例池會提供對象實例的運行環境,並控制對象實例的生命週期

工業級的實例池實現上有兩個最基本的難點,一個就是動態控制實例數量一個就是動態分配實例來提供給外部使用。這些都是須要算法來作保證的。

假如實例池裏面已有了3個實例,可是客戶端請求很是多,有些忙不過來,那麼實例池的管理程序就應該判斷出來,到底幾個實例才能知足如今的客戶需求,理想情況是剛恰好,就是既可以知足應用的須要,又不會形成對象實例的浪費,假如通過判斷5個實例正好,那麼實例池的管理程序就應該能動態的建立2個新的實例。

這樣運行了一段時間,客戶端的請求減小了,這個時候實例池的管理程序又應該動態的判斷,究竟幾個實例是最好的,多了明顯浪費資源,假如通過判斷只須要1個實例就能夠了,那麼實例池的管理程序應該銷燬掉多餘的4個實例,以釋放資源。這就是動態控制實例數量。

對於動態分配實例,也說明一下吧,假如實例池裏面有3個實例,這個時候來了一個新的請求,到底調度哪個實例去執行客戶的請求呢,若是有空閒實例,那就是它了,要是沒有空閒實例呢,是新建一個實例,仍是等待運行中的實例,等它運行完了就來處理這個請求呢?具體如何調度,也是須要算法來保障的。

回到享元模式中來,享元工廠中的實例池可沒有這麼複雜,由於共享的享元對象基本上都是一個實例,通常不會出現同一個享元對象有多個實例的狀況,這樣就不用去考慮動態建立和銷燬享元對象實例的功能;另外只有一個實例,也就不存在動態調度的麻煩,反正就是它了

這也主要是由於享元對象封裝的多半是對象的內部狀態,這些狀態一般是不變的,有一個實例就夠了,不須要動態控制生命週期,也不須要動態調度,它只須要作一個緩存而已,沒有上升到真正的實例池那麼個高度。

  1. 享元模式的調用順序示意圖

享元模式的使用上,有兩種狀況,一種是沒有「不須要共享」的享元對象,就如同前面的示例那樣,只有共享享元對象的狀況;還有一種是既有共享享元對象,又有不須要共享的享元對象的狀況,這種狀況後面再示例。

這裏看看只有共享享元對象的狀況下,享元模式的調用順序,如圖20.3所示:

輸入圖片說明

  1. 誰來初始化共享對象

在享元模式中,一般是在第一次向享元工廠請求獲取共享對象的時候,進行共享對象的初始化,並且多半都是在享元工廠內部實現,不會從外部傳入共享對象。固然能夠從外部傳入一些建立共享對象須要的值,享元工廠能夠按照這些值去初始化須要共享的對象,而後就把建立好的共享對象的實例放入享元工廠內部的緩存中,之後再請求這個共享對象的時候就不用再建立了。

##3.2 不須要共享的享元實現## 可能有些朋友看到這個標題會很疑惑,享元不就是要共享的對象嗎?不共享,叫什麼享元啊?

確實有不須要共享的享元實現,這種狀況多出如今組合結構中,對於使用已經緩存的享元組合出來的對象,就沒有必要再緩存了,也就是把已經緩存的享元當作葉子結點,組合出來的組合對象就不須要再被緩存了。也把這種享元稱爲複合享元

好比上面的權限描述,若是出現組合權限描述,在這個組合對象裏面包含不少個共享的權限描述,那麼這個組合對象就不用緩存了,這個組合對象的存在只是爲了在受權的時候更加方便

具體點說吧,好比要給某人分配「薪資數據」這個安全實體的「修改」權限,那麼必定會把「薪資數據」的「查看權限」也分配給這我的,若是按照前面的作法,這就須要分配兩個對象,爲了方便,乾脆把這兩個描述組合起來,打包成一個對象,命名成爲「操做薪資數據」,那麼分配權限的時候,能夠這麼描述:

把  「操做薪資數據」  分配給   張三

這句話的意思就至關於

把  「薪資數據」 的  「查看」權限   分配給   張三
把  「薪資數據」 的  「修改」權限   分配給   張三

這樣一來,「操做薪資數據」就至關因而一個不須要共享的享元,它實際由享元「薪資數據 的 查看 權限」,和享元「薪資數據 的 修改 權限」這兩個享元組合而成,所以「操做薪資數據」自己也就不須要再共享了。

這樣分配權限的時候就會簡單一點。

可是這種組合對象,在權限系統中通常不用於驗證,也就是說驗證的時候仍是一個一個進行判斷,由於在存儲受權信息的時候是一條一條存儲的。但也不排除有些時候始終要檢查多個權限,乾脆把這些權限打包,而後直接驗證是否有這個組合權限,只是這種狀況應用得比較少而已。

仍是用示例來講明吧,在上面已經實現的系統裏面添加不須要共享的享元實現。此時系統結構如圖20.4所示:

輸入圖片說明

  1. 首先要在享元接口上添加上對組合對象的操做,主要是要添加向組合對象中加入子對象的方法,示例代碼以下:
/***
 * 描述受權數據的享元接口
 */
public interface Flyweight {
    /**
     * 判斷傳入的安全實體和權限,是否和享元對象內部狀態匹配
     * @param securityEntity 安全實體
     * @param permit 權限
     * @return true表示匹配,false表示不匹配
     */
    public boolean match(String securityEntity,String permit);
    /**
     * 爲flyweight添加子flyweight對象
     * @param f 被添加的子flyweight對象
     */
    public void add(Flyweight f);  
}
  1. 享元接口改變了,那麼原來共享的享元對象也須要實現這個方法,這個方法主要是針對組合對象的,所以在葉子對象裏面拋出不支持的例外就行了,示例代碼以下:
/**
 * 封裝受權數據中重複出現部分的享元對象
 */
public class AuthorizationFlyweight implements Flyweight{
  
    public void add(Flyweight f) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
}
  1. 接下來實現新的不須要共享的享元對象,其實就是組合共享享元對象的對象,這個組合對象中,須要保存全部的子對象,另外它在實現match方法的時候,是經過遞歸的方式,在整個組合結構中進行匹配。示例代碼以下:
/**
 * 不須要共享的享元對象的實現,也是組合模式中的組合對象
 */
public class UnsharedConcreteFlyweight implements Flyweight{
    /**
     * 記錄每一個組合對象所包含的子組件
     */
    private List<Flyweight> list = new ArrayList<Flyweight>();
  
    public void add(Flyweight f) {
       list.add(f);
    }
  
    public boolean match(String securityEntity, String permit) {
       for(Flyweight f : list){
           //遞歸調用
           if(f.match(securityEntity, permit)){
              return true;
           }
       }
       return false;
    }
}
  1. 在繼續實現以前,先來準備測試數據,也就是TestDB,須要有一些改變

首先是受權數據要區分是單條的受權,仍是組合的受權,這個在每條受權數據後面添加一個標識來描述

而後增長一個描述組合數據的記錄,使用一個Map來存放

具體的示例代碼以下:

/**
 * 供測試用,在內存中模擬數據庫中的值
 */
public class TestDB {
    /**
     * 用來存放單獨受權數據的值
     */
    public static Collection<String> colDB = new ArrayList<String>();
    /**
     * 用來存放組合受權數據的值,
     * key爲組合數據的id,value爲該組合包含的多條受權數據的值
     */
    public static Map<String,String[]> mapDB = new HashMap<String,String[]>();
   
    static{
       //經過靜態塊來填充模擬的數據,增長一個標識來代表是否組合受權數據
       colDB.add("張三,人員列表,查看,1");
       colDB.add("李四,人員列表,查看,1");
       colDB.add("李四,操做薪資數據,,2");
     
       mapDB.put("操做薪資數據",new String[]{"薪資數據,查看","薪資數據,修改"});
     
       //增長更多的受權數據
       for(int i=0;i<3;i++){
           colDB.add("張三"+i+",人員列表,查看,1");
       }
    }
}
  1. 享元工廠不須要變化,這裏就不去贅述了

  2. 接下來該實現安全管理的類了,這個類至關於享元模式的Client角色,此次在這個類裏面,不單純使用共享的享元對象,它還會使用不須要共享的享元對象

主要的變化集中在queryByUser方法裏面,本來只是經過享元工廠來獲取共享的享元對象便可,可是此次還須要在這裏建立不須要共享的享元對象。示例代碼以下:

public class SecurityMgr {
    private static SecurityMgr securityMgr = new SecurityMgr();
    private SecurityMgr(){     
    }
    public static SecurityMgr getInstance(){
       return securityMgr;
    }
    /**
     * 在運行期間,用來存放登陸人員對應的權限,
     * 在Web應用中,這些數據一般會存放到session中
     */
    private Map<String,Collection<Flyweight>> map = new HashMap<String,Collection<Flyweight>>();
    /**
     * 模擬登陸的功能
     * @param user 登陸的用戶
     */
    public void login(String user){
       //登陸時就須要把該用戶所擁有的權限,從數據庫中取出來,放到緩存中去
       Collection<Flyweight> col = queryByUser(user);
       map.put(user, col);
    }
    /**
     * 判斷某用戶對某個安全實體是否擁有某權限
     * @param user 被檢測權限的用戶
     * @param securityEntity 安全實體
     * @param permit 權限
     * @return true表示擁有相應權限,false表示沒有相應權限
     */
    public boolean hasPermit(String user,String securityEntity,String permit){
       Collection<Flyweight> col = map.get(user);
       System.out.println("如今測試"+securityEntity+"的"+permit+"權限,map.size="+map.size());
       if(col==null || col.size()==0){
           System.out.println(user+"沒有登陸或是沒有被分配任何權限");
           return false;
       }
       for(Flyweight fm : col){
           //輸出當前實例,看看是否同一個實例對象
           System.out.println("fm=="+fm);
           if(fm.match(securityEntity, permit)){
              return true;
           }
       }
       return false;
    }  
    /**
     * 從數據庫中獲取某人所擁有的權限
     * @param user 須要獲取所擁有的權限的人員
     * @return 某人所擁有的權限
     */
    private Collection<Flyweight> queryByUser(String user){
       Collection<Flyweight> col = new ArrayList<Flyweight>();
       for(String s : TestDB.colDB){
           String ss[] = s.split(",");
           if(ss[0].equals(user)){
              Flyweight fm = null;
              if(ss[3].equals("2")){
                  //表示是組合
                  fm = new UnsharedConcreteFlyweight();
                  //獲取須要組合的數據
                  String tempSs[] = TestDB.mapDB.get(ss[1]);
                  for(String tempS : tempSs){
                     Flyweight tempFm = FlyweightFactory.getInstance().getFlyweight(tempS);
                     //把這個對象加入到組合對象中
                     fm.add(tempFm);
                  }
              }else{
                  fm = FlyweightFactory.getInstance().getFlyweight(ss[1]+","+ss[2]);
              }
            
              col.add(fm);
           }
       }
       return col;
    }  
}
  1. 客戶端測試沒有太大的變化,增長一條測試「李四對薪資數據的修改權限」,示例代碼以下:
public class Client {
    public static void main(String[] args) throws Exception{
       //須要先登陸,而後再判斷是否有權限
       SecurityMgr mgr = SecurityMgr.getInstance();
       mgr.login("張三");
       mgr.login("李四");   
       boolean f1 = mgr.hasPermit("張三","薪資數據","查看");
       boolean f2 = mgr.hasPermit("李四","薪資數據","查看");
       boolean f3 = mgr.hasPermit("李四","薪資數據","修改");
     
       System.out.println("f1=="+f1);
       System.out.println("f2=="+f2);
       System.out.println("f3=="+f3);
     
       for(int i=0;i<3;i++){
           mgr.login("張三"+i);
           mgr.hasPermit("張三"+i,"薪資數據","查看");
       }
    }
}

能夠運行測試一下,看看效果,結果示例以下:

如今測試薪資數據的查看權限,map.size=2
fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1
如今測試薪資數據的查看權限,map.size=2
fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1
fm==cn.javass.dp.flyweight.example4.UnsharedConcreteFlyweight@1ad086a
如今測試薪資數據的修改權限,map.size=2
fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1
fm==cn.javass.dp.flyweight.example4.UnsharedConcreteFlyweight@1ad086a
f1==false
f2==true
f3==true
如今測試薪資數據的查看權限,map.size=3
fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1
如今測試薪資數據的查看權限,map.size=4
fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1
如今測試薪資數據的查看權限,map.size=5
fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1

##3.3 對享元對象的管理## 雖然享元模式對於共享的享元對象實例的管理要求,沒有實例池對實例管理的要求那麼高,可是也仍是有不少自身的特色功能,好比:引用計數、垃圾清除等。所謂垃圾,就是在緩存中存在,可是再也不須要被使用的緩存中的對象

所謂引用計數,就是享元工廠可以記錄每一個享元被使用的次數;而垃圾清除,則是大多數緩存管理都有的功能,緩存不能只往裏面放數據,在不須要這些數據的時候,應該把這些數據從緩存中清除,釋放相應的內存空間,以節省資源

在前面的示例中,共享的享元對象是不少人共享的,基本上能夠一直存在於系統中,不用清除。可是垃圾清除是享元對象管理的一個很常見功能,仍是經過示例給你們講一下,看看如何實現這些常見的功能。

  1. 實現引用計數的基本思路

要實現引用計數,就在享元工廠裏面定義一個Map,它的key值跟緩存享元對象的key是同樣的,而value就是被引用的次數,這樣當外部每次獲取該享元的時候,就把對應的引用計數取出來加上1,而後再記錄回去。

  1. 實現垃圾回收的基本思路

要實現垃圾回收就比較麻煩點,首先要能肯定哪些是垃圾?其次是什麼時候回收?還有由誰來回收?如何回收?解決了這些問題,也就能實現垃圾回收了。

爲了肯定哪些是垃圾,一個簡單的方案是這樣的,定義一個緩存對象的配置對象,在這個對象中描述了緩存的開始時間和最長不被使用的時間,這個時候判斷是垃圾的計算公式以下:當前的時間 - 緩存的開始時間 >= 最長不被使用的時間。固然,每次這個對象被使用的時候,就把那個緩存開始的時間更新爲使用時的當前時間,也就是說若是一直有人用的話,這個對象是不會被判斷爲垃圾的。

什麼時候回收的問題,固然是判斷出來是垃圾了就能夠回收了。

關鍵是誰來判斷垃圾,還有誰來回收垃圾的問題。一個簡單的方案是定義一個內部的線程,這個線程在享元工廠被建立的時候就啓動運行。由這個線程每隔必定的時間來循環緩存中全部對象的緩存配置,看看是不是垃圾,若是是垃圾,那就能夠啓動回收了。

怎麼回收呢?這個比較簡單,就是直接從緩存的map對象中刪除掉相應的對象,讓這些對象沒有引用的地方,那麼這些對象就能夠等着被虛擬機的垃圾回收來回收掉了

  1. 代碼示例

(1)分析了這麼多,仍是看代碼示例會比較清楚,先看緩存配置對象,示例代碼以下:

/**
 * 描述享元對象緩存的配置對象
 */
public class CacheConfModel{
    /**
     * 緩存開始計時的開始時間
     */
    private long beginTime;
    /**
     * 緩存對象存放的持續時間,實際上是最長不被使用的時間
     */
    private double durableTime;
    /**
     * 緩存對象須要被永久存儲,也就是不須要從緩存中刪除
     */
    private boolean forever;
    public boolean isForever() {
       return forever;
    }
    public void setForever(boolean forever) {
       this.forever = forever;
    }
    public long getBeginTime() {
       return beginTime;
    }
    public void setBeginTime(long beginTime) {
       this.beginTime = beginTime;
    }
    public double getDurableTime() {
       return durableTime;
    }
    public void setDurableTime(double durableTime) {
       this.durableTime = durableTime;
    }
}

(2)對享元對象的管理的工做,是由享元工廠來完成的,所以上面的功能,也集中在享元工廠裏面來實現,在上一個例子的基礎之上,來實現這些功能,改進後的享元工廠相對而言稍複雜一點,大體有以下改變:

添加一個Map,來緩存被共享對象的緩存配置的數據;

添加一個Map,來記錄緩存對象被引用的次數;

爲了測試方便,定義了一個常量來描述緩存的持續時間;

提供獲取某個享元被使用的次數的方法;

在獲取享元的對象裏面,就要設置相應的引用計數和緩存設置了,示例採用的是內部默認設置一個緩存設置,其實也能夠改造一下獲取享元的方法,從外部傳入緩存設置的數據;

提供一個清除緩存的線程,實現判斷緩存數據是否已是垃圾了,若是是,那就把它從緩存中清除掉;

基本上從新實現了享元工廠,示例代碼以下:

/**
 * 享元工廠,一般實現成爲單例
 * 加入實現垃圾回收和引用計數的功能
 */
public class FlyweightFactory {
    private static FlyweightFactory factory = new FlyweightFactory();
    private FlyweightFactory(){
       //啓動清除緩存值的線程
       Thread t = new ClearCache();
       t.start();
    }
    public static FlyweightFactory getInstance(){
       return factory;
    }

    /**
     * 緩存多個flyweight對象
     */
    private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>();
    /**
     * 用來緩存被共享對象的緩存配置,key值和上面map的同樣
     */
    private Map<String,CacheConfModel> cacheConfMap = new HashMap<String,CacheConfModel>();
    /**
     * 用來記錄緩存對象被引用的次數,key值和上面map的同樣
     */
    private Map<String,Integer> countMap = new HashMap<String,Integer>();
    /**
     * 默認保存6秒鐘,主要爲了測試方便,這個時間能夠根據應用的要求設置
     */
    private final long DURABLE_TIME = 6*1000L;
  
    /**
     * 獲取某個享元被使用的次數
     * @param key 享元的key
     * @return 被使用的次數
     */
    public synchronized int getUseTimes(String key){
       Integer count = countMap.get(key);
       if(count==null){
           count = 0;
       }
       return count;
    }
    /**
     * 獲取key對應的享元對象
     * @param key 獲取享元對象的key
     * @return key對應的享元對象
     */
    public synchronized Flyweight getFlyweight(String key) {
       Flyweight f = fsMap.get(key);
       if(f==null){
           f = new AuthorizationFlyweight(key);
           fsMap.put(key,f);
           //同時設置引用計數
           countMap.put(key, 1);

           //同時設置緩存配置數據
           CacheConfModel cm = new CacheConfModel();
           cm.setBeginTime(System.currentTimeMillis());
           cm.setForever(false);
           cm.setDurableTime(DURABLE_TIME);
         
           cacheConfMap.put(key, cm);
       }else{
           //表示還在使用,那麼應該從新設置緩存配置
           CacheConfModel cm = cacheConfMap.get(key);
           cm.setBeginTime(System.currentTimeMillis());
           //設置回去
           this.cacheConfMap.put(key, cm);
           //同時計數加1
           Integer count = countMap.get(key);
           count++;
           countMap.put(key, count);
       }
       return f;
    }
    /**
     * 刪除key對應的享元對象,連帶清除對應的緩存配置和引用次數的記錄,不對外
     * @param key 要刪除的享元對象的key
     */
    private synchronized void removeFlyweight(String key){
       this.fsMap.remove(key);
       this.cacheConfMap.remove(key);
       this.countMap.remove(key);
    }
    /**
     * 維護清除緩存的線程,內部使用
     */
    private  class ClearCache extends Thread{
       public void run(){
           while(true){
              Set<String> tempSet = new HashSet<String>();
              Set<String> set = cacheConfMap.keySet();
              for(String key : set){
                  CacheConfModel ccm = cacheConfMap.get(key);
                  //比較是否須要清除
                  if((System.currentTimeMillis() - ccm.getBeginTime()) >= ccm.getDurableTime()){
                     //能夠清除,先記錄下來
                     tempSet.add(key);
                  }
              }
              //真正清除
              for(String key : tempSet){
                  FlyweightFactory.getInstance().removeFlyweight(key);
              }
              System.out.println("now thread="+fsMap.size() +",fsMap=="+fsMap.keySet());
              //休息1秒再從新判斷
              try {
                  Thread.sleep(1000L);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
           }
       }
    }
}

注意:getUseTimes、removeFlyweight和getFlyweight這幾個方法是加了同步的,緣由是在多線程環境下使用它們,容易出現併發錯誤,好比一個線程在獲取享元對象,而另外一個線程在刪除這個緩存對象

(3)要想看出引用計數的效果來,SecurityMgr須要進行一點修改,至少不要再緩存數據了,要直接從享元工廠中獲取數據,不然就沒有辦法準確引用計數了,大體改變以下:

去掉了放置登陸人員對應權限數據的緩存;

不須要實現登陸功能,在這個示意程序裏面,登陸方法已經不用實現任何功能,所以直接去掉;

原來經過map獲取值的地方,直接經過queryByUser獲取就行了;

示例代碼以下:

public class SecurityMgr {
    private static SecurityMgr securityMgr = new SecurityMgr();
    private SecurityMgr(){     
    }
    public static SecurityMgr getInstance(){
       return securityMgr;
    }
    /**
     * 判斷某用戶對某個安全實體是否擁有某權限
     * @param user 被檢測權限的用戶
     * @param securityEntity 安全實體
     * @param permit 權限
     * @return true表示擁有相應權限,false表示沒有相應權限
     */
    public boolean hasPermit(String user,String securityEntity,String permit){
       Collection<Flyweight> col = this.queryByUser(user);
       if(col==null || col.size()==0){
           System.out.println(user+"沒有登陸或是沒有被分配任何權限");
           return false;
       }
       for(Flyweight fm : col){
           if(fm.match(securityEntity, permit)){
              return true;
           }
       }
       return false;
    }
    /**
     * 從數據庫中獲取某人所擁有的權限
     * @param user 須要獲取所擁有的權限的人員
     * @return 某人所擁有的權限
     */
    private Collection<Flyweight> queryByUser(String user){
       Collection<Flyweight> col = new ArrayList<Flyweight>();
     
       for(String s : TestDB.colDB){
           String ss[] = s.split(",");
           if(ss[0].equals(user)){
              Flyweight fm = null;
              if(ss[3].equals("2")){
                  //表示是組合
                  fm = new UnsharedConcreteFlyweight();
                  //獲取須要組合的數據
                  String tempSs[] = TestDB.mapDB.get(ss[1]);
                  for(String tempS : tempSs){
                     Flyweight tempFm = FlyweightFactory.getInstance().getFlyweight(tempS);
                     //把這個對象加入到組合對象中
                     fm.add(tempFm);
                  }
              }else{
                  fm = FlyweightFactory.getInstance().getFlyweight(ss[1]+","+ss[2]);
              }            
              col.add(fm);
           }
       }
       return col;
    }  
}

(4)仍是寫個客戶端來試試看,上面的享元工廠可否實現對享元對象的管理,尤爲是對於垃圾回收和計數方面的功能,對於垃圾回收的功能不須要新加任何的測試代碼,而對於引用計數的功能,須要寫代碼來調用才能看到效果,示例代碼以下:

public class Client {
    public static void main(String[] args) throws Exception{
       SecurityMgr mgr = SecurityMgr.getInstance();
       boolean f1 = mgr.hasPermit("張三","薪資數據","查看");
       boolean f2 = mgr.hasPermit("李四","薪資數據","查看");
       boolean f3 = mgr.hasPermit("李四","薪資數據","修改");

       for(int i=0;i<3;i++){
           mgr.hasPermit("張三"+i,"薪資數據","查看");
       }  
     
       //特別提醒:這裏查看的引用次數,不是指測試使用的次數,指的是
       //SecurityMgr的queryByUser方法經過享元工廠去獲取享元對象的次數
       System.out.println("薪資數據,查看 被引用了"+FlyweightFactory.getInstance().getUseTimes("薪資數據,查看")+"次");
       System.out.println("薪資數據,修改 被引用了"+FlyweightFactory.getInstance().getUseTimes("薪資數據,修改")+"次");
       System.out.println("人員列表,查看 被引用了"+FlyweightFactory.getInstance().getUseTimes("人員列表,查看")+"次");
    }
}

進行緩存的垃圾回收功能的是個線程在運行,因此你不終止該線程運行,程序會一直運行下去,運行部分結果以下:

薪資數據,查看 被引用了2次
薪資數據,修改 被引用了2次
人員列表,查看 被引用了6次
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=0,fsMap==[]
now thread=0,fsMap==[]

##3.4 享元模式的優缺點##

  1. 減小對象數量,節省內存空間

可能有的朋友認爲共享對象會浪費空間,可是若是這些對象頻繁使用,那麼實際上是節省空間的。由於佔用空間的大小等於每一個對象實例佔用的大小再乘以數量,對於享元對象來說,基本上就只有一個實例,大大減小了享元對象的數量,並節省很多的內存空間

節省的空間取決於如下幾個因素:由於共享而減小的實例數目、每一個實例自己所佔用的空間。假如每一個對象實例佔用2個字節,若是不共享數量是100個,而共享事後就只有一個了,那麼節省的空間約等於:(100-1) X 2 字節。

  1. 維護共享對象,須要額外開銷

如同前面演示的享元工廠,在維護共享對象的時候,若是功能複雜,會有不少額外的開銷,好比有一個線程來維護垃圾回收。

##3.5 思考享元模式##

  1. 享元模式的本質

享元模式的本質:分離與共享

分離的是對象狀態中變與不變的部分,共享的是對象中不變的部分。享元模式的關鍵之處就在於分離變與不變,把不變的部分做爲享元對象的內部狀態,而變化部分就做爲外部狀態,由外部來維護,這樣享元對象就可以被共享,從而減小對象數量,並節省大量的內存空間。

理解了這個本質後,在使用享元模式的時候,就會去考慮,哪些狀態須要分離?如何分離?分離後如何處理?哪些須要共享?如何管理共享的對象?外部如何使用共享的享元對象?是否須要不共享的對象?等等問題。

把這些問題都思考清楚,找到相應的解決方法,那麼享元模式也就應用起來了,多是標準的應用,也多是變形的應用,但萬變不離其宗。

  1. 什麼時候選用享元模式

建議在以下狀況中,選用享元模式:

若是一個應用程序使用了大量的細粒度對象,可使用享元模式來減小對象數量;

若是因爲使用大量的對象,形成很大的存儲開銷,可使用享元模式來減小對象數量,並節約內存;

若是對象的大多數狀態均可以轉變爲外部狀態,好比經過計算獲得,或是從外部傳入等,可使用享元模式來實現內部狀態和外部狀態的分離;

若是不考慮對象的外部狀態,能夠用相對較少的共享對象取代不少組合對象,可使用享元模式來共享對象,而後組合對象來使用這些共享對象;

##3.6 相關模式##

  1. 享元模式與單例模式

這兩個模式能夠組合使用。

一般狀況下,享元模式中的享元工廠能夠實現成爲單例。另外,享元工廠裏面緩存的享元對象,都是單實例的,能夠當作是單例模式的一種變形控制,在享元工廠裏面來單例享元對象。

  1. 享元模式與組合模式

這兩個模式能夠組合使用。

在享元模式裏面,存在不須要共享的享元實現,這些不須要共享的享元一般是對共享的享元對象的組合對象,也就是說,享元模式一般會和組合模式組合使用,來實現更復雜的對象層次結構

  1. 享元模式與狀態模式

這兩個模式能夠組合使用。

可使用享元模式來共享狀態模式中的狀態對象,一般在狀態模式中,會存在數量很大的、細粒度的狀態對象,並且它們基本上都是能夠重複使用的,都是用來處理某一個固定的狀態的,它們須要的數據一般都是由上下文傳入,也就是變化部分都分離出去了,因此能夠用享元模式來實現這些狀態對象。

  1. 享元模式與策略模式

這兩個模式能夠組合使用。

可使用享元模式來實現策略模式中的策略對象,跟狀態模式同樣,在策略模式中也存在大量細粒度的策略對象,它們須要的數據一樣是從上下文傳入的,因此可使用享元模式來實現這些策略對象。

相關文章
相關標籤/搜索