11.java設計模式之享元模式

xiaokantianse發表於2020-11-19

基本需求:

  • 小型的外包專案,給客戶A做一個產品展示網站,客戶A的朋友感覺效果不錯,也希望做這樣的產品展示網站,但是要求都有些不同
  • 每個客戶要求釋出的方式不一樣,A要求以新聞的方式釋出,B要求以部落格的方式釋出,C要求以公眾號的方式釋出,並且多個釋出人可能選擇用同一種方式釋出

傳統方案:

  • 直接複製A客戶的網站,在其上按照B的要求修改,定製化,給每個網站都租用一個空間
  • 需要的網站結構相似度很高,而且都不是高訪問量網站,如果分成多個虛擬空間來處理,相當於一個相同網站的例項物件很多,造成伺服器的資源浪費
  • 解決思路:整合到一個網站中,共享其相關的程式碼和資料,對於硬碟、記憶體、CPU、資料庫空間等伺服器資源都可以達成共享,減少伺服器資源

基本介紹:

  • 享元模式(Flyweight)也叫'蠅量模式',主要用於減少建立物件的數量,以減少記憶體佔用和提高效能。這種型別的設計模式屬於結構型模式,它提供了減少物件數量從而改善應用所需的物件結構的方式。享元模式嘗試重用現有的同類物件,如果未找到匹配的物件,則建立新物件,有匹配的物件,則直接使用

  • 運用共享技術有效地支援大量細粒度的物件,主要解決:在有大量物件時,有可能會造成記憶體溢位,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接返回在記憶體中已有的物件,避免重新建立

  • 常用於系統底層開發,解決系統的效能問題。像資料庫連線池,裡面都是建立好的連線物件,在這些連線物件中有我們需要的則直接拿來用,避免重新建立,如果沒有我們需要的,則建立一個

  • 享元模式能夠解決重複物件的記憶體浪費的問題,當系統中有大量相似物件,需要緩衝池時。不需總是建立新物件,可以從緩衝池裡拿。這樣可以降低系統記憶體,同時提高效率

  • 享元模式經典的應用場景就是池技術了,String 常量池、資料庫連線池、緩衝池等等都是享元模式的應用,享元模式是池技術的重要實現方式

  • UML類圖(原理)

    • 說明
      • FlyWeight 是抽象的享元角色, 他是產品的抽象類, 同時定義出物件的外部狀態和內部狀態的介面或實現
        • 享元模式提出了兩個要求:細粒度和共享物件。這裡就涉及到內部狀態和外部狀態了,即將物件的資訊分為兩個部分:內部狀態和外部狀態
        • 內部狀態指物件共享出來的資訊, 儲存在享元物件內部且不會隨環境的改變而改變
        • 外部狀態指物件得以依賴的一個標記,是 隨環境改變而改變的、不可共享的狀態
        • 例如:下五子棋,圍棋等,使用的棋子就可以用享元模式定義,棋子的顏色就是內部狀態(兩種顏色不會改變),棋子在棋盤的位置隨選手的落子而決定就是外部狀態;
          • 可在棋子類 建立兩個屬性 一個顏色,一個位置,落子時每次在工廠獲取的都是同一個物件,只不過位置屬性的值不同而已,達到了這兩個棋子物件的複用
      • ConcreteFlyWeight 是具體的享元角色,是具體的產品類,實現抽象角色定義相關業務
      • UnSharedConcreteFlyWeight 是不可共享的角色,一般不會出現在享元工廠(一般也不會出現)
      • FlyWeightFactory 享元工廠類,內部構建一個池容器(集合),同時提供從池中獲取物件方法
  • UML類圖(案例)

    • 說明

      • WebSite有不同的釋出狀態,用type表示,也就是其內部狀態,ConcreteWebSite為其實現類,具體的享元角色
      • User為外部狀態,不同的User可以用相同的方式去釋出WebSite
      • WebSiteFactory為工廠類,用於維護WebSite池
    • 程式碼實現

      • public abstract class WebSite {
        
           // 享元抽象類
        
           // 此屬性為內部狀態,釋出的形式只有幾種,新聞,部落格,公眾號
           protected String type;
        
           public WebSite(String type) {
               this.type = type;
           }
        
           // 使用User類作為外部狀態,不同的User可以釋出同一形式的網站(實際使用的是同一WebSite物件)達到了物件的複用
           public abstract void use(User user);
        
        }
        
        // 具體的享元類
        class ConcreteWebSite extends WebSite{
        
           // 具體的享元產品
        
           public ConcreteWebSite(String type) {
               super(type);
           }
        
           @Override
           public void use(User user) {
               System.out.println(user.getName() + "用" + this.type + "形式釋出了網站");
           }
        
        }
        
      • @Data
        @AllArgsConstructor
        public class User {
        
           // WebSite的外部狀態
           private String name;
        
        }
        
      • public class WebSiteFactory {
        
           // 享元工廠類,內部維護享元產品池
        
           private Map<String, WebSite> webSiteMap = new HashMap<>();
        
           public WebSite getWebSiteCategory(String type) {
               if (!webSiteMap.containsKey(type)) {
                   // 池中沒有,建立新的享元產品維護到池中,返回新建立的享元產品
                   webSiteMap.put(type, new ConcreteWebSite(type));
               }
               return webSiteMap.get(type);
           }
        
           // 獲取池中享元產品的數量
           public int getWebSiteCount() {
               return webSiteMap.size();
           }
        
        }
        
      • public class Client {
           public static void main(String[] args) {
               WebSiteFactory webSiteFactory = new WebSiteFactory();
               WebSite webSite1 = webSiteFactory.getWebSiteCategory("新聞");
               webSite1.use(new User("張三"));
               WebSite webSite2 = webSiteFactory.getWebSiteCategory("部落格");
               webSite2.use(new User("李四"));
               WebSite webSite3 = webSiteFactory.getWebSiteCategory("公眾號");
               webSite3.use(new User("王五"));
               WebSite webSite4 = webSiteFactory.getWebSiteCategory("新聞");
               webSite4.use(new User("王五"));
               System.out.println("池中享元物件的數量為:" + webSiteFactory.getWebSiteCount()); // 3 個
               System.out.println(webSite1 == webSite4); // true 其實通過享元工廠獲取的是同一個物件
           }
        }
        
        

jdk原始碼:

  • 在jdk原始碼的Integer類中就使用到了享元模式,Integer.valueOf()方法,在-128-127之間返回的都是同一個Integer的快取物件

  • public class Client {
       public static void main(String[] args) {
           // Integer類中就使用到了享元模式,Integer.valueOf()方法,在-128-127之間返回的都是同一個Integer的快取物件
           // 如果使用 valueOf 方法得到一個 Integer 例項,範圍在 -128 - 127 ,執行速度比 new 快
           Integer w = Integer.valueOf(127);
           Integer x = new Integer(127);
           Integer y = Integer.valueOf(127);
           Integer z = new Integer(127);
           System.out.println(w == x); // false
           System.out.println(w == y); // true
           System.out.println(x == z); // false new出來肯定不一樣 在堆記憶體進行分配
       }
    }
    
  • // 原始碼部分
    public class Integer {
       
       ......
    public static Integer valueOf(int i) {
           // 呼叫該方法在-128-127內就返回快取物件,使用的享元模式
           if (i >= IntegerCache.low && i <= IntegerCache.high)
               return IntegerCache.cache[i + (-IntegerCache.low)];
           return new Integer(i);
       }
       ......
           
       // 靜態內部類 IntegerCache 
       private static class IntegerCache {
           static final int low = -128;
           static final int high;
           static final Integer cache[];
    
           static {
               // high value may be configured by property
               int h = 127;
               String integerCacheHighPropValue =
                   sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
               if (integerCacheHighPropValue != null) {
                   try {
                       int i = parseInt(integerCacheHighPropValue);
                       i = Math.max(i, 127);
                       // Maximum array size is Integer.MAX_VALUE
                       h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                   } catch( NumberFormatException nfe) {
                       // If the property cannot be parsed into an int, ignore it.
                   }
               }
               // 為Integer.high賦值
               high = h;
    
               cache = new Integer[(high - low) + 1];
               int j = low;
               // 迴圈建立Integer物件,並放入緩衝池,此處緩衝池使用的是陣列
               for(int k = 0; k < cache.length; k++)
                   cache[k] = new Integer(j++);
    
               // range [-128, 127] must be interned (JLS7 5.1.7)
               assert IntegerCache.high >= 127;
           }
    
           private IntegerCache() {}
       }
       
    }
    

注意事項:

  • 在享元模式這樣理解,“享”就表示共享,“元”表示物件系統中有大量物件,這些物件消耗大量記憶體,並且物件的狀態大部分可以外部化時,我們就可以考慮選用享元模式
  • 用唯一標識碼判斷,如果在記憶體中有,則返回這個唯一標識碼所標識的物件,用 HashMap/HashTable 儲存,大大減少了物件的建立,降低了程式記憶體的佔用,提高效率
  • 享元模式提高了 系統的複雜度。需要分離出 內部狀態和外部狀態,而外部狀態具有固化特性,不應該隨著內部狀態的改變而改變,這是我們使用享元模式需要注意的地方
  • 使用享元模式時,注意劃分內部狀態和外部狀態,並且需要有一個工廠類加以控制
  • 享元模式經典的應用場景是需要緩衝池的場景,比如 String 常量池、資料庫連線池

相關文章