在面向對象程式設計過程中,有時會面臨要建立大量相同或相似對象執行個體的問題。建立那麼多的對象将會耗費很多的系統資源,它是系統性能提高的一個瓶頸。
例如,圍棋和五子棋中的黑白棋子,圖像中的坐标點或顔色,區域網路中的路由器、交換機和集線器,教室裡的桌子和凳子等。這些對象有很多相似的地方,如果能把它們相同的部分提取出來共享,則能節省大量的系統資源,這就是享元模式的産生背景。
享元模式的定義與特點
享元(Flyweight)模式的定義:運用共享技術來有效地支援大量細粒度對象的複用。它通過共享已經存在的對象來大幅度減少需要建立的對象數量、避免大量相似類的開銷,進而提高系統資源的使用率。
享元模式的主要優點是:相同對象隻要儲存一份,這降低了系統中對象的數量,進而降低了系統中細粒度對象給記憶體帶來的壓力。
其主要缺點是:
- 為了使對象可以共享,需要将一些不能共享的狀态外部化,這将增加程式的複雜性。
- 讀取享元模式的外部狀态會使得運作時間稍微變長。
享元模式的結構與實作
享元模式的定義提出了兩個要求,細粒度和共享對象。因為要求細粒度,是以不可避免地會使對象數量多且性質相近,此時我們就将這些對象的資訊分為兩個部分:内部狀态和外部狀态。
- 内部狀态指對象共享出來的資訊,存儲在享元資訊内部,并且不回随環境的改變而改變;
- 外部狀态指對象得以依賴的一個标記,随環境的改變而改變,不可共享。
比如,連接配接池中的連接配接對象,儲存在連接配接對象中的使用者名、密碼、連接配接URL等資訊,在建立對象的時候就設定好了,不會随環境的改變而改變,這些為内部狀态。而當每個連接配接要被回收利用時,我們需要将它标記為可用狀态,這些為外部狀态。
享元模式的本質是緩存共享對象,降低記憶體消耗。
1. 模式的結構
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具體享元類的基類,為具體享元規範需要實作的公共接口,非享元的外部狀态以參數的形式通過方法傳入。
- 具體享元(Concrete Flyweight)角色:實作抽象享元角色中所規定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部狀态,它以參數的形式注入具體享元的相關方法中。
- 享元工廠(Flyweight Factory)角色:負責建立和管理享元角色。當客戶對象請求一個享元對象時,享元工廠檢査系統中是否存在符合要求的享元對象,如果存在則提供給客戶;如果不存在的話,則建立一個新的享元對象。
圖 1 是享元模式的結構圖,其中:
- UnsharedConcreteFlyweight 是非享元角色,裡面包含了非共享的外部狀态資訊 info;
- Flyweight 是抽象享元角色,裡面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部狀态以參數的形式通過該方法傳入;
- ConcreteFlyweight 是具體享元角色,包含了關鍵字 key,它實作了抽象享元接口;
- FlyweightFactory 是享元工廠角色,它是關鍵字 key 來管理具體享元;
- 客戶角色通過享元工廠擷取具體享元,并通路具體享元的相關方法。
2. 模式的實作
享元模式的實作代碼如下:
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。
享元模式的應用執行個體
【例1】享元模式在五子棋遊戲中的應用。
分析:五子棋同圍棋一樣,包含多個“黑”或“白”顔色的棋子,是以用享元模式比較好。
本執行個體中:
- 棋子(ChessPieces)類是抽象享元角色,它包含了一個落子的 DownPieces(Graphics g,Point pt) 方法;
- 白子(WhitePieces)和黑子(BlackPieces)類是具體享元角色,它實作了落子方法;
- Point 是非享元角色,它指定了落子的位置;
- WeiqiFactory 是享元工廠角色,它通過 ArrayList 來管理棋子,并且提供了擷取白子或者黑子的 getChessPieces(String type) 方法;
- 客戶類(Chessboard)利用 Graphics 元件在架構窗體中繪制一個棋盤,并實作 mouseClicked(MouseEvent e) 事件處理方法,該方法根據使用者的選擇從享元工廠中擷取白子或者黑子并落在棋盤上。
圖 2 所示是其結構圖。
程式代碼如下:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
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;
}
}
}
程式運作結果如圖 3 所示。
享元模式的應用場景
當系統中多處需要同一組資訊時,可以把這些資訊封裝到一個對象中,然後對該對象進行緩存,這樣,一個對象就可以提供給多出需要使用的地方,避免大量同一對象的多次建立,降低大量記憶體空間的消耗。
享元模式其實是工廠方法模式的一個改進機制,享元模式同樣要求建立一個或一組對象,并且就是通過工廠方法模式生成對象的,隻不過享元模式為工廠方法模式增加了緩存這一功能。
前面分析了享元模式的結構與特點,下面分析它适用的應用場景。享元模式是通過減少記憶體中對象的數量來節省記憶體空間的,是以以下幾種情形适合采用享元模式。
- 系統中存在大量相同或相似的對象,這些對象耗費大量的記憶體資源。
- 大部分的對象可以按照内部狀态進行分組,且可将不同部分外部化,這樣每一個組隻需儲存一個内部狀态。
- 由于享元模式需要額外維護一個儲存享元的資料結構,是以應當在有足夠多的享元執行個體時才值得使用享元模式。
享元模式的擴充
在前面介紹的享元模式中,其結構圖通常包含可以共享的部分和不可以共享的部分。在實際使用過程中,有時候會稍加改變,即存在兩種特殊的享元模式:單純享元模式和複合享元模式,下面分别對它們進行簡單介紹。
(1) 單純享元模式,這種享元模式中的所有的具體享元類都是可以共享的,不存在非共享的具體享元類,其結構圖如圖 4 所示。
(2) 複合享元模式,這種享元模式中的有些享元對象是由一些單純享元對象組合而成的,它們就是複合享元對象。雖然複合享元對象本身不能共享,但它們可以分解成單純享元對象再被共享,其結構圖如圖 5 所示。