一、享元模式的定義
享元(Flyweight)模式的定義:運用共享技術來有効地支持大量細粒度對象的復用。它通過共享已經存在的又橡來大幅度減少需要創建的對象數量,避免大量相似類的開銷,從而提高系統資源的利用率。
String常量池、數據庫連接池、緩沖池等等都是享元模式的應用,所以說享元模式是池技術的重要實現方式。
二、享元模式優缺點
享元模式的主要優點是:
- 相同對象只要保存一份,這降低了系統中對象的數量,從而降低了系統中細粒度對象給內存帶來的壓力。
其主要缺點是:
- 為了使對象可以共享,需要將一些不能共享的狀態外部化,這將增加程序的復雜性。
- 讀取享元模式的外部狀態會使得運行時間稍微變長。
三、享元模式的實現
享元模式中存在以下兩種狀態:
- 內部狀態,即不會隨着環境的改變而改變的可共享部分;
- 外部狀態,指隨環境改變而改變的不可以共享的部分。享元模式的實現要領就是區分應用中的這兩種狀態,並將外部狀態外部化。下面來分析其基本結構和實現方法。
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具體享元類的基類,為具體享元規范需要實現的公共接口,非享元的外部狀態以參數的形式通過方法傳入。
- 具體享元(Concrete Flyweight)角色:實現抽象享元角色中所規定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部狀態,它以參數的形式注入具體享元的相關方法中。
- 享元工廠(Flyweight Factory)角色:負責創建和管理享元角色。當客戶對象請求一個享元對象時,享元工廠檢査系統中是否存在符合要求的享元對象,如果存在則提供給客戶;如果不存在的話,則創建一個新的享元對象。
如圖所示是享元模式的結構圖。圖中的 UnsharedConcreteFlyweight 是非享元角色,里面包含了非共享的外部狀態信息 info;而 Flyweight 是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部狀態以參數的形式通過該方法傳入;ConcreteFlyweight 是具體享元角色,包含了關鍵字 key,它實現了抽象享元接口;FlyweightFactory 是享元工廠角色,它通過關鍵字 key 來管理具體享元;客戶角色通過享元工廠獲取具體享元,並訪問具體享元的相關方法。
代碼實現如下:
public class FlyweightPattern { public static void main(String[] args) { FlyweightFactory factory=new FlyweightFactory(); Flyweight f01=factory.getFlyweight("a"); Flyweight f02=factory.getFlyweight("a"); Flyweight f03=factory.getFlyweight("a"); Flyweight f11=factory.getFlyweight("b"); Flyweight f12=factory.getFlyweight("b"); f01.operation(new UnsharedConcreteFlyweight("第1次調用a。")); f02.operation(new UnsharedConcreteFlyweight("第2次調用a。")); f03.operation(new UnsharedConcreteFlyweight("第3次調用a。")); f11.operation(new UnsharedConcreteFlyweight("第1次調用b。")); f12.operation(new UnsharedConcreteFlyweight("第2次調用b。")); } } //非享元角色 class UnsharedConcreteFlyweight { private String info; UnsharedConcreteFlyweight(String info) { this.info=info; } public String getInfo() { return info; } public void setInfo(String info) { this.info=info; } } //抽象享元角色 interface Flyweight { public void operation(UnsharedConcreteFlyweight state); } //具體享元角色 class ConcreteFlyweight implements Flyweight { private String key; ConcreteFlyweight(String key) { this.key=key; System.out.println("具體享元"+key+"被創建!"); } public void operation(UnsharedConcreteFlyweight outState) { System.out.print("具體享元"+key+"被調用,"); System.out.println("非享元信息是:"+outState.getInfo()); } } //享元工廠角色 class FlyweightFactory { private HashMap<String, Flyweight> flyweights=new HashMap<String, Flyweight>(); public Flyweight getFlyweight(String key) { Flyweight flyweight=(Flyweight)flyweights.get(key); if(flyweight!=null) { System.out.println("具體享元"+key+"已經存在,被成功獲取!"); } else { flyweight=new ConcreteFlyweight(key); flyweights.put(key, flyweight); } return flyweight; } }
測試結果如下:
具體享元a被創建!
具體享元a已經存在,被成功獲取!
具體享元a已經存在,被成功獲取!
具體享元b被創建!
具體享元b已經存在,被成功獲取!
具體享元a被調用,非享元信息是:第1次調用a。
具體享元a被調用,非享元信息是:第2次調用a。
具體享元a被調用,非享元信息是:第3次調用a。
具體享元b被調用,非享元信息是:第1次調用b。
具體享元b被調用,非享元信息是:第2次調用b。
四、享元模式的應用實例
由於享元模式相對不好理解,此處就進行一個五子棋示例:五子棋同圍棋一樣,包含多個“黑”或“白”顏色的棋子,所以用享元模式比較好。
本實例中的棋子(ChessPieces)類是抽象享元角色,它包含了一個落子的 DownPieces(Graphics g,Point pt) 方法;白子(WhitePieces)和黑子(BlackPieces)類是具體享元角色,它實現了落子方法;Point 是非享元角色,它指定了落子的位置;WeiqiFactory 是享元工廠角色,它通過 ArrayList 來管理棋子,並且提供了獲取白子或者黑子的 getChessPieces(String type) 方法;客戶類(Chessboard)利用 Graphics 組件在框架窗體中繪制一個棋盤,並實現 mouseClicked(MouseEvent e) 事件處理方法,該方法根據用戶的選擇從享元工廠中獲取白子或者黑子並落在棋盤上。其結構圖如圖所示:
代碼如下:
public class WzqGame { public static void main(String[] args) { new Chessboard(); } } //棋盤 class Chessboard extends MouseAdapter { WeiqiFactory wf; JFrame f; Graphics g; JRadioButton wz; JRadioButton bz; private final int x=50; private final int y=50; private final int w=40; //小方格寬度和高度 private final int rw=400; //棋盤寬度和高度 Chessboard() { wf=new WeiqiFactory(); f=new JFrame("享元模式在五子棋游戲中的應用"); f.setBounds(100,100,500,550); f.setVisible(true); f.setResizable(false); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel SouthJP=new JPanel(); f.add("South",SouthJP); wz=new JRadioButton("白子"); bz=new JRadioButton("黑子",true); ButtonGroup group=new ButtonGroup(); group.add(wz); group.add(bz); SouthJP.add(wz); SouthJP.add(bz); JPanel CenterJP=new JPanel(); CenterJP.setLayout(null); CenterJP.setSize(500, 500); CenterJP.addMouseListener(this); f.add("Center",CenterJP); try { Thread.sleep(500); } catch(InterruptedException e) { e.printStackTrace(); } g=CenterJP.getGraphics(); g.setColor(Color.BLUE); g.drawRect(x, y, rw, rw); for(int i=1;i<10;i++) { //繪制第i條豎直線 g.drawLine(x+(i*w),y,x+(i*w),y+rw); //繪制第i條水平線 g.drawLine(x,y+(i*w),x+rw,y+(i*w)); } } public void mouseClicked(MouseEvent e) { Point pt=new Point(e.getX()-15,e.getY()-15); if(wz.isSelected()) { ChessPieces c1=wf.getChessPieces("w"); c1.DownPieces(g,pt); } else if(bz.isSelected()) { ChessPieces c2=wf.getChessPieces("b"); c2.DownPieces(g,pt); } } } //抽象享元角色:棋子 interface ChessPieces { public void DownPieces(Graphics g,Point pt); //下子 } //具體享元角色:白子 class WhitePieces implements ChessPieces { public void DownPieces(Graphics g,Point pt) { g.setColor(Color.WHITE); g.fillOval(pt.x,pt.y,30,30); } } //具體享元角色:黑子 class BlackPieces implements ChessPieces { public void DownPieces(Graphics g,Point pt) { g.setColor(Color.BLACK); g.fillOval(pt.x,pt.y,30,30); } } //享元工廠角色 class WeiqiFactory { private ArrayList<ChessPieces> qz; public WeiqiFactory() { qz=new ArrayList<ChessPieces>(); ChessPieces w=new WhitePieces(); qz.add(w); ChessPieces b=new BlackPieces(); qz.add(b); } public ChessPieces getChessPieces(String type) { if(type.equalsIgnoreCase("w")) { return (ChessPieces)qz.get(0); } else if(type.equalsIgnoreCase("b")) { return (ChessPieces)qz.get(1); } else { return null; } } }
另外再補充一個在坐標圖畫圓的例子,代碼如下:
interface Shape { void draw(); } class Circle implements Shape { private String color; private int x; private int y; private int radius; public Circle(String color){ this.color = color; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public void setRadius(int radius) { this.radius = radius; } @Override public void draw() { System.out.println("Circle: Draw() [Color : " + color +", x : " + x +", y :" + y +", radius :" + radius); } } class ShapeFactory { private static final HashMap<String, Shape> circleMap = new HashMap<String, Shape>(); public static Shape getCircle(String color) { Circle circle = (Circle)circleMap.get(color); if(circle == null) { circle = new Circle(color); circleMap.put(color, circle); System.out.println("Creating circle of color : " + color); } return circle; } } public class DrawShape { private static final String colors[] = { "Red", "Green", "Blue", "White", "Black" }; public static void main(String[] args) { for(int i=0; i < 7; ++i) { Circle circle = (Circle)ShapeFactory.getCircle(getRandomColor()); circle.setX(getRandomX()); circle.setY(getRandomY()); circle.setRadius(100); circle.draw(); } } private static String getRandomColor() { return colors[(int)(Math.random()*colors.length)]; } private static int getRandomX() { return (int)(Math.random()*100 ); } private static int getRandomY() { return (int)(Math.random()*100); } }
測試結果如下:
Creating circle of color : Green Circle: Draw() [Color : Green, x : 25, y :73, radius :100 Creating circle of color : Blue Circle: Draw() [Color : Blue, x : 13, y :37, radius :100 Creating circle of color : Red Circle: Draw() [Color : Red, x : 20, y :91, radius :100 Creating circle of color : White Circle: Draw() [Color : White, x : 29, y :57, radius :100 Circle: Draw() [Color : White, x : 66, y :36, radius :100 Circle: Draw() [Color : Blue, x : 48, y :4, radius :100 Creating circle of color : Black Circle: Draw() [Color : Black, x : 8, y :91, radius :100
五、享元模式的應用場景
前面分析了享元模式的結構與特點,下面分析它適用的應用場景。享元模式是通過減少內存中對象的數量來節省內存空間的,所以以下幾種情形適合采用享元模式。
- 系統中存在大量相同或相似的對象,這些對象耗費大量的內存資源。
- 大部分的對象可以按照內部狀態進行分組,且可將不同部分外部化,這樣每一個組只需保存一個內部狀態。
- 由於享元模式需要額外維護一個保存享元的數據結構,所以應當在有足夠多的享元實例時才值得使用享元模式。
六、享元模式的擴展
在前面介紹的享元模式中,其結構圖通常包含可以共享的部分和不可以共享的部分。在實際使用過程中,有時候會稍加改變,即存在兩種特殊的享元模式:單純享元模式和復合享元模式,下面分別對它們進行簡單介紹。
- 單純享元模式,這種享元模式中的所有的具體享元類都是可以共享的,不存在非共享的具體享元類,其結構圖如圖所示:
- 復合享元模式,這種享元模式中的有些享元對象是由一些單純享元對象組合而成的,它們就是復合享元對象。雖然復合享元對象本身不能共享,但它們可以分解成單純享元對象再被共享,其結構圖如圖所示: