研磨設計模式 之 享元模式(Flyweight)1——跟著cc學設計系列

xinqing010發表於2014-01-15

20.1  場景問題

20.1.1  加入許可權控制

       考慮這樣一個問題,給系統加入許可權控制,這基本上是所有的應用系統都有的功能了。

       對於應用系統而言,一般先要登入系統,才可以使用系統的功能,登入過後,使用者的每次操作都需要經過許可權系統的控制,確保該使用者有操作該功能的許可權,同時還要控制該使用者對資料的訪問許可權、修改許可權等等。總之一句話,一個安全的系統,需要對使用者的每一次操作都要做許可權檢測,包括功能和資料,以確保只有獲得相應授權的人,才能執行相應的功能,操作相應的資料。

       舉個例子來說吧:普通人員都有能檢視到本部門人員列表的許可權,但是在人員列表中每個人員的薪資資料,普通人員是不可以看到的;而部門經理在檢視本部門人員列表的時候,就可以看到每個人員相應的薪資資料。

       現在就要來實現為系統加入許可權控制的功能,該怎麼實現呢?

       為了讓大家更好的理解後面講述的知識,先介紹一點許可權系統的基礎知識。幾乎所有的許可權系統都分成兩個部分,一個是授權部分,一個是驗證部分,為了理解它們,首先解釋兩個基本的名詞:安全實體和許可權。

  • 安全實體:就是被許可權系統檢測的物件,比如工資資料。
  • 許可權:就是需要被校驗的許可權物件,比如檢視、修改等。

安全實體和許可權通常要一起描述才有意義,比如有這麼個描述:“現在要檢測登入人員對工資資料是否有檢視的許可權”, “工資資料”這個安全實體和“檢視”這個許可權一定要一起描述。如果只出現安全實體描述,那就變成這樣:“現在要檢測登入人員對工資資料”,對工資資料幹什麼呀,沒有後半截,一看就知道不完整;當然只有許可權描述也不行,那就變成:“現在要檢測登入人員是否有檢視的許可權”,對誰的檢視許可權啊,也不完整。所以安全實體和許可權通常要一起描述。

瞭解了上面兩個名詞,來看看什麼是授權和驗證:

  • 所謂授權是指:把對某些安全實體的某些許可權分配給某些人員的過程。
  • 所謂驗證是指:判斷某個人員對某個安全實體是否擁有某個或某些許可權的過程。

       也就是說,授權過程即是許可權的分配過程,而驗證過程則是許可權的匹配過程。在目前應用系統的開發中,多數是利用資料庫來存放授權過程產生的資料,也就是說:授權是向資料庫裡面新增資料、或是維護資料的過程,而匹配過程就變成了從資料庫中獲取相應資料進行匹配的過程了。

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

  • 許可權的繼承性指的是:如果多個安全實體存在包含關係,而某個安全實體沒有相應的許可權限制,那麼它會繼承包含它的安全實體的相應許可權。
        比如:某個大樓和樓內的房間都是安全實體,很明顯大樓這個安全實體會包含樓內的房間這些安全實體,可以認為大樓是樓內房間的父級實體。現在來考慮一個具體的許可權——進入某個房間的許可權。如果這個房間沒有門,也就是誰都可以進入,相當於這個房間對應的安全實體,沒有進入房間的許可權限制,那麼是不是說所有的人都可以進入這個房間呢?當然不是,某人能進入這個房間的前提是:這個人要有許可權進入這個大樓,也就是說,這個時候房間這個安全實體,它本身沒有進入許可權的限制,但是它會繼承父級安全實體的進入許可權。
  • 許可權的最近匹配原則指的是:如果多個安全實體存在包含關係,而某個安全實體沒有相應的許可權限制,那麼它會向上尋找並匹配相應許可權限制,直到找到一個離這個安全實體最近的擁有相應許可權限制的安全實體為止。如果把整個層次結構都尋找完了都沒有匹配到相應許可權限制的話,那就說明所有人對這個安全實體都擁有這個相應的許可權限制。
        繼續上面許可權繼承性的例子,如果現在這個大樓是坐落在某個機關大院內,這就演變成了,要進入某個房間,首先要有進入大樓的許可權,要進入大樓又需要有能進入機關大院的許可權。
        所謂最近匹配原則就是,如果某個房間沒有門,也就意味著這個房間沒有進入的許可權限制,那麼它就會向上繼續尋找並匹配,看看大樓有沒有進入的許可權限制,如果有就使用這個許可權限制,終止尋找;如果沒有,繼續向上尋找,直到找到一個匹配的為止。如果最後大院也沒有進入的許可權限制,那就變成所有人都可以進入到這個房間裡面來了。

20.1.2  不使用模式的解決方案

1:看看現在都已經有什麼了

系統的授權工作已經完成,授權資料記錄在資料庫裡面,具體的資料結構就不去展開了,反正裡面記錄了人員對安全實體所擁有的許可權。假如現在系統中已有如下的授權資料:

張三  對  人員列表   擁有    檢視的許可權

李四  對  人員列表   擁有    檢視的許可權

李四  對  薪資資料   擁有    檢視的許可權

李四  對  薪資資料   擁有    修改的許可權

2:思路選擇

       由於操作人員進行授權操作過後,各人員被授予的許可權是記錄在資料庫中的,剛開始有開發人員提出,每次使用者作業系統的時候,都直接到資料庫裡面去動態查詢,以判斷該人員是否擁有相應的許可權,但很快就被否決掉了,試想一下,使用者操作那麼頻繁,每次都到資料庫裡面動態查詢,這會嚴重加劇資料庫伺服器的負擔,使系統變慢。

為了加快系統執行的速度,開發小組決定採用一定的快取,當每個人員登入的時候,就把該人員能操作的許可權獲取到,儲存在記憶體中,這樣每次操作的時候,就直接在記憶體裡面進行許可權的校驗,速度會大大加快,這是典型的以空間換時間的做法。

3:實現示例

(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 colDB =

new ArrayList();

    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> map =

       new HashMap>();

   

    /**

     * 模擬登入的功能

     * @param user 登入的使用者

     */

    public void login(String user){

       //登入時就需要把該使用者所擁有的許可權,從資料庫中取出來,放到快取中去

        Collection 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 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 queryByUser(

String user){

       Collection col =

new ArrayList();

       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,表示李四對對薪資資料有檢視的許可權,是正確的,基本完成了功能。

20.1.3  有何問題

看了上面的實現,很簡單,而且還考慮了效能的問題,在記憶體中快取了每個人相應的許可權資料,使得每次判斷許可權的時候,速度大大加快,實現得挺不錯,難道有什麼問題嗎?

仔細想想,問題就來了,既有快取這種方式固有的問題,也有我們自己實現上的問題。先說說快取固有的問題吧,這個不在本次討論之列,大家瞭解一下。

  • 快取時間長度的問題,就是這些資料應該被快取多久,如果是Web應用,這種跟登入人員相關的許可權資料,多是放在session中進行快取,這樣session超時的時候,就會被清除掉。如果不是Web應用呢?就得自己來控制了,另外就算是在Web應用中,也不一定非要快取到session超時才清除。總之,控制快取資料應該被快取多長時間,是實現高效快取的一個問題點。
  • 快取資料和真實資料的同步問題,這裡的同步是指的資料同步,不是多執行緒的同步。比如:上面的授權資料是存放在資料庫裡的,執行的時候快取到記憶體裡面,如果真實的授權資料在執行期間發生了改變,那麼快取裡的資料就應該和資料庫的資料同步,以保持一致,否則資料就錯了。如何合理的同步資料,也是實現高效快取的一個問題點。
  • 快取的多執行緒併發控制,對於快取的資料,有些操作從裡面取值,有些操作向快取裡面新增值,有些操作在清除過期的快取資料,有些操作在進行快取和真實資料的同步,在一個多執行緒的環境下,如何合理的對快取進行併發控制,也是實現高效快取的一個問題點。

先簡單提這麼幾個,事實上,實現合理、高效的快取也不是一件很輕鬆的事情,好在這些問題,都不在我們這次的討論之列,這裡的重心還是來講述模式,而不是快取實現。

再來看看前面實現上的問題,仔細觀察在上面輸出結果中框住的部分,這些值是輸出物件例項得到的,預設輸出的是物件的hashCode值,而預設的hashCode值可以用來判斷是不是同一物件例項。在Java中,預設的equals方法比較的是記憶體地址,而equals方法和hashCode方法的關係是:equals方法返回true的話,那麼這兩個物件例項的hashCode必須相同;而hashCode相同,equals方法並不一定返回true,也就是說兩個物件例項不一定是同一物件例項。換句話說,如果hashCode不同的話,鐵定不是同一個物件例項。

仔細看看上面輸出結果,框住部分的值是不同的,表明這些物件例項肯定不是同一個物件例項,而是多個物件例項。這就引出一個問題了,就是物件例項數目太多,為什麼這麼說呢?看看就描述這麼幾條資料,數數看有多少個物件例項呢?目前是一條資料就有一個物件例項,這很恐怖,資料庫的資料量是很大的,如果有幾萬條,幾十萬條,豈不是需要幾萬個,甚至幾十萬個物件例項,這會耗費掉大量的記憶體。

另外,這些物件的粒度都很小,都是簡單的描述某一個方面的物件,而且很多資料是重複的,在這些大量重複的資料上耗費掉了很多的記憶體。比如在前面示例的資料中就會發現有重複的部分,見下面框住的部分:

張三  對  人員列表   擁有    檢視的許可權

李四  對  人員列表   擁有    檢視的許可權

李四  對  薪資資料   擁有    檢視的許可權

李四  對  薪資資料   擁有    修改的許可權

前面講過,對於安全實體和許可權一般要聯合描述,因此對於“人員列表 這個安全實體 的 檢視許可權 限制”,就算是授權給不同的人員,這個描述是一樣的。假設在某極端情況下,要把“人員列表 這個安全實體 的 檢視許可權 限制”授權給一萬個人,那麼資料庫裡面會有一萬條記錄,按照前面的實現方式,會有一萬個物件例項,而這些例項裡面,有大部分的資料是重複的,而且會重複一萬次,你覺得這是不是個很大的問題呢?

       把上面的問題描述出來就是:在系統當中,存在大量的細粒度物件,而且存在大量的重複資料,嚴重耗費記憶體,如何解決?

 

---------------------------------------------------------------------------

私塾線上學習網原創內容  跟著cc學設計系列 之 研磨設計模式

研磨設計討論群【252780326】

原創內容,轉載請註明出處【http://sishuok.com/forum/blogPost/list/0/5637.html

---------------------------------------------------------------------------

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26715458/viewspace-1070999/,如需轉載,請註明出處,否則將追究法律責任。

相關文章