天天看點

Java 版設計模式代碼案例 (二):結構型設計模式

作者:程式猿凱撒

1. 擴充卡模式(Adapter)

擴充卡模式将某個類的接口轉換成用戶端期望的另一個接口表示,目的是消除由于接口不比對所造成的類的相容性問題。

主要分為三類:類的擴充卡模式、對象的擴充卡模式、接口的擴充卡模式。

類的擴充卡模式

類擴充卡模式:當希望将一個類轉換成滿足另一個新接口的類時,可以使用類的擴充卡模式,建立一個新類,繼承原有的類,實作新的接口即可。

将 USB 接口轉為 VGA 接口:

java複制代碼public interface Usb {

    void playMKV();

}
           
java複制代碼public class UsbImpl implements Usb {

    @Override
    public void playMKV() {
        System.out.println("播放視訊内容");
    }

}
           

Usb2VgaAdapter 首先繼承 USBImpl 擷取 USB 的功能,其次,實作 VGA 接口,表示該類的類型為 VGA:

java複制代碼public interface Vga {

    void projection();

}
           
java複制代碼public class Usb2VgaAdapter extends UsbImpl implements Vga {

    @Override
    public void projection() {
        super.playMKV();
    }

}
           

Projector 将 USB 映射為 VGA,隻有 VGA 接口才可以連接配接上投影儀進行投影:

java複制代碼public class Projector {

    public <T> void projection(T t) {
        if (t instanceof Vga) {
            System.out.println("開始投影");
            Vga v = (Vga) t;
            v.projection();
        } else {
            System.out.println("接口不比對,無法投影");
        }
    }

}
           

測試,隻有 VGA 接口才能投影播放内容:

java複制代碼public class MainTest {

    public static void main(String[] args) {
        Projector projector = new Projector();
        Usb usb = new UsbImpl();
        projector.projection(usb);
        System.out.println("=================");
        Vga vga = new Usb2VgaAdapter();
        projector.projection(vga);
    }

}
           
markdown複制代碼接口不比對,無法投影
=================
開始投影
播放視訊内容
           

對象的擴充卡模式

對象擴充卡模式:當希望将一個對象轉換成滿足另一個新接口的對象時,可以建立一個 Wrapper 類,持有原類的一個執行個體,在 Wrapper 類的方法中,調用執行個體的方法就行。

對象擴充卡和類擴充卡使用了不同的方法實作适配,對象擴充卡使用組合,類擴充卡使用繼承。

scala複制代碼public class Usb2VgaAdapter extends UsbImpl implements Vga {} // 原先的代碼
           
java複制代碼public class Usb2VgaWrapper implements Vga {

    Usb usb = new UsbImpl();

    @Override
    public void projection() {
        usb.playMKV();
    }

}
           

能夠得到一樣的測試結果:

markdown複制代碼接口不比對,無法投影
=================
開始投影
播放視訊内容
           

接口的擴充卡模式

接口擴充卡模式:當不希望實作一個接口中所有的方法時,可以建立一個抽象類 Wrapper,實作所有方法,我們寫别的類的時候,繼承抽象類即可。

當不需要全部實作接口提供的方法時,可先設計一個抽象類實作接口,并為該接口中每個方法提供一個預設實作(空方法),那麼該抽象類的子類可有選擇地覆寫父類的某些方法來實作需求,它适用于一個接口不想使用其所有的方法的情況。

VGA 接口中增加一些别的方法:

java複制代碼public interface Vga {

    void projection();

    void m();

    void n();

}
           

所有繼承者都要實作 m() 和 n() 方法,我們可以引入中間層 Wrapper,讓真正的實作類選擇性實作 VGA 接口:

java複制代碼public abstract class Usb2VgaWrapper implements Vga {

    Usb usb = new UsbImpl();

    @Override
    public void projection() {
        usb.playMKV();
    }

    @Override
    public void m() {

    }

    @Override
    public void n() {

    }

}
           
java複制代碼public class Usb2VgaAdapter extends Usb2VgaWrapper {

    @Override
    public void projection() {
        super.projection();
    }

    @Override
    public void m() {
        super.m();
    }

}
           

能夠得到一樣的測試結果:

markdown複制代碼接口不比對,無法投影
=================
開始投影
播放視訊内容
           

根據合成複用原則,組合大于繼承。是以,類的擴充卡模式應該少用。

2. 裝飾者模式(Decorator)

裝飾者模式動态地将責任附加到對象上。若要擴充功能,裝置者提供了比繼承更有彈性的替代方案。

主要實作方式:

  • Decorator 繼承了 Component ,附加 Decorator 的 Component 本制還是一種 Component ,後續可進一步對其修飾;
  • Decorator 關聯了 Component ,便于對其附加行為。

咖啡店有多種口味的咖啡:

java複制代碼public abstract class Drink {

    private BigDecimal price;
    public String description = "";

    public abstract BigDecimal cost();

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

}
           
java複制代碼public class Coffee extends Drink {

    @Override
    public BigDecimal cost() {
        return super.getPrice();
    }

}
           
java複制代碼public class Espresso extends Coffee {

    public Espresso(BigDecimal price) {
        this.setPrice(price);
        this.setDescription(this.getClass().getSimpleName());
    }

}
           
java複制代碼public class LongBlack extends Coffee {

    public LongBlack(BigDecimal price) {
        this.setPrice(price);
        this.setDescription(this.getClass().getSimpleName());
    }

}
           

裝飾者和被裝飾者之間必須是一樣的類型,也就是要有共同的超類。

咖啡除了原味咖啡,還可以選擇加牛奶,加糖:

java複制代碼public class CoffeeDecorator extends Drink {

    private Drink drink;

    public CoffeeDecorator(Drink drink) {
        this.drink = drink;
    }

    @Override
    public BigDecimal cost() {
        return super.getPrice().add(drink.cost());
    }

    @Override
    public String getDescription() {
        return drink.getDescription() + " && " + super.getDescription();
    }

}
           
java複制代碼public class Milk extends CoffeeDecorator {

    public Milk(Drink drink) {
        super(drink);
        this.setPrice(new BigDecimal("0.20"));
        this.setDescription(this.getClass().getSimpleName());
    }

}
           
java複制代碼public class Sugar extends CoffeeDecorator {

    public Sugar(Drink drink) {
        super(drink);
        this.setPrice(new BigDecimal("0.10"));
        this.setDescription(this.getClass().getSimpleName());
    }

}
           

我們來實作一個測試類:

java複制代碼public class MainTest {

    public static void main(String[] args) {
        Drink order1 = new Espresso(new BigDecimal("10.00"));
        System.out.println("Order1 price: " + order1.cost());
        System.out.println("Order1 description: " + order1.getDescription());
        System.out.println("****************");
        Drink order2 = new LongBlack(new BigDecimal("20.00"));
        order2 = new Milk(order2);
        order2 = new Sugar(order2);
        System.out.println("Order2 price: " + order2.cost());
        System.out.println("Order2 description: " + order2.getDescription());
    }

}
           
markdown複制代碼Order1 price: 10.00
Order1 description: Espresso
****************
Order2 price: 20.30
Order2 description: LongBlack && Milk && Sugar
           

在這裡應用繼承并不是實作方法的複制,而是實作類型的比對。因為裝飾者和被裝飾者是同一個類型,是以裝飾者可以取代被裝飾者,這樣就使被裝飾者擁有了裝飾者獨有的行為。根據裝飾者模式的理念,我們可以在任何時候,實作新的裝飾者增加新的行為。

3. 代理模式(Proxy)

代理模式給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用。通俗的來講代理模式就是我們生活中常見的中介。

開閉原則:代理類除了是客戶類和委托類的中介之外,我們還可以通過給代理類增加額外的功能來擴充委托類的功能,這樣做我們隻需要修改代理類而不需要再修改委托類,符合代碼設計的開閉原則。

代理類主要負責為委托類預處理消息、過濾消息、把消息轉發給委托類,以及事後對傳回結果的處理等。代理類本身并不真正實作服務,而是通過調用委托類的相關方法,來提供特定的服務。真正的業務功能還是由委托類來實作,但是可以在業務功能執行的前後加入一些公共的服務。例如我們想給項目 加入緩存、日志這些功能 ,我們就可以使用代理類來完成,而沒必要打開已經封裝好的委托類。

靜态代理

靜态代理雖然可以做到在符合開閉原則的情況下對目标對象進行功能擴充,但是 代理對象與目标對象要實作相同的接口,我們得為每一個服務都得建立代理類,工作量太大不易管理,同時接口一旦發生改變,代理類也得相應修改。

java複制代碼public interface BuyHouse {

    void buyHouse();

}
           
java複制代碼public class BuyHouseImpl implements BuyHouse {

    @Override
    public void buyHouse() {
        System.out.println("買房的交易過程");
    }

}
           
java複制代碼public class BuyHouseProxy implements BuyHouse {

    private final BuyHouse buyHouse;

    public BuyHouseProxy(final BuyHouse buyHouse) {
        this.buyHouse = buyHouse;
    }

    @Override
    public void buyHouse() {
        System.out.println("買房前中介處理");
        buyHouse.buyHouse();
        System.out.println("買房後中介處理");
    }

}
           

動态代理(JDK)

JDK 動态代理的代理對象的生成,是利用 JDK 的 API,動态的在記憶體中建構代理對象(需要我們指定建立代理對象/目标對象實作的接口的類型)。代理類不用再實作接口了,但是要求被代理對象必須有接口。

java複制代碼public interface BuyHouseInterface {

    void buyHouse();

}
           
java複制代碼public class BuyHouseService implements BuyHouseInterface {

    @Override
    public void buyHouse() {
        System.out.println("買房的交易過程");
        this.doOther();
    }

    public void doOther() {
        System.out.println("做了點其他事情");
    }

}
           

java.lang.reflect.InvocationHandler 中的 invoke(Object proxy, Method method, Object[] args)方法:

  • 參數 Object proxy:代理對象(慎用)
  • 參數 Method method:目前執行的方法
  • 參數 Object[] args:目前執行的方法運作時傳遞過來的參數
java複制代碼public class BuyHouseJdkProxy implements InvocationHandler {

    private final Object target;

    public BuyHouseJdkProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("買房前中介處理");
        Object result = method.invoke(target, args);
        System.out.println("買房後中介處理");
        return result;
    }
           

java.lang.reflect.Proxy 中的 newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 生成一個代理對象:

  • 參數 ClassLoader loader:代理對象的類加載器 一般使用被代理對象的類加載器
  • 參數 Class<?>[] interfaces: 代理對象的要實作的接口 一般使用的被代理對象實作的接口
  • 參數 InvocationHandler h: (接口)執行處理類
java複制代碼public class BuyHouseJdkProxyTest {

    public static void main(String[] args) {
        BuyHouseService buyHouseService = new BuyHouseService();
        BuyHouseInterface buyHouseJdkProxy = (BuyHouseInterface)
                Proxy.newProxyInstance(
                        BuyHouseService.class.getClassLoader(),
                        new Class[]{BuyHouseInterface.class},
                        new BuyHouseJdkProxy(buyHouseService));
        buyHouseJdkProxy.buyHouse();
    }

}
           

雖然相對于靜态代理,動态代理大大減少了我們的開發任務,同時減少了對業務接口的依賴,降低了耦合度。但是還是有一點點小小的遺憾之處,那就是 它始終無法擺脫僅支援 interface 代理的桎梏(我們要使用被代理的對象的接口),因為它的設計注定了這個遺憾。

動态代理(CGLIB)

CGLIB 原理:動态生成一個要代理類的子類,子類重寫要代理的類的所有不是 final 的方法。在子類中采用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯。它比使用 JAVA 反射的 JDK 動态代理要快。

CGLIB 底層:使用位元組碼處理架構 ASM,來轉換位元組碼并生成新的類。不鼓勵直接使用 ASM,因為它要求你必須對 JVM 内部結構包括 class 檔案的格式和指令集都很熟悉。

java複制代碼public class BuyHouseService {

    public void buyHouse() {
        System.out.println("買房的交易過程");
        this.doOther();
    }

    public void doOther() {
        System.out.println("做了點其他事情");
    }

}
           

MethodInterceptor 中的 intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) 方法:

  • 參數 Object object:由 CGLIB 動态生成的代理類執行個體
  • 參數 Method method:為上文中實體類所調用的被代理的方法引用
  • 參數 Object[] args:為參數值清單
  • 參數 MethodProxy methodProxy:為生成的代理類對方法的代理引用
java複制代碼public class BuyHouseCglibProxy implements MethodInterceptor {

    private final Object target;

    public BuyHouseCglibProxy(Object target) {
        this.target = target;
    }

    public static <T> T getInstance(T target) {
        BuyHouseCglibProxy cglibProxy = new BuyHouseCglibProxy(target);
        Enhancer enhancer = new Enhancer();
        enhancer.setCallback(cglibProxy);
        enhancer.setSuperclass(target.getClass());
        return (T) enhancer.create();
    }

    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("買房前中介處理");
        Object result = methodProxy.invokeSuper(object, args);
        System.out.println("買房後中介處理");
        return result;
    }

}
           

Enhancer 類似 CGLib 中的一個位元組碼增強器,它可以友善的對你想要處理的類進行擴充。首先将被代理類 TargetObject 設定成父類,然後設定攔截器 TargetInterceptor,最後執行 enhancer.create() 動态生成一個代理類。

methodProxy.invokeSuper(object, args) 調用代理類執行個體上的 proxy 方法的父類方法(即實體類 TargetObject 中對應的方法)。

java複制代碼public class BuyHouseCglibProxyTest {

    public static void main(String[] args) {
        BuyHouseService buyHouseService = new BuyHouseService();
        BuyHouseService buyHouseCglibProxy = BuyHouseCglibProxy.getInstance(buyHouseService);
        buyHouseCglibProxy.buyHouse();
    }

}
           

CGLIB 建立的動态代理對象比 JDK 建立的動态代理對象的性能更高,但是 CGLIB 建立代理對象時所花費的時間卻比 JDK 多得多。是以對于單例的對象,因為無需頻繁建立對象,用 CGLIB 合适,反之使用 JDK 方式要更為合适一些。同時由于 CGLib 由于是采用動态建立子類的方法,對于 final 修飾的方法無法進行代理。

動态代理(JDK 和 CGLIB)的細節

細心的同學會發現,上述代碼中有同樣的如下片段:

java複制代碼public void buyHouse() {
    System.out.println("買房的交易過程");
    this.doOther();
}

public void doOther() {
    System.out.println("做了點其他事情");
}
           

JDK 動态代理 buyHouseJdkProxy.buyHouse() 的輸出結果:

複制代碼買房前中介處理
買房的交易過程
做了點其他事情
買房後中介處理
           

CGLIB 動态代理 buyHouseCglibProxy.buyHouse() 的輸出結果:

複制代碼買房前中介處理
買房的交易過程
買房前中介處理
做了點其他事情
買房後中介處理
買房後中介處理
           

内部調用的形式中,JDK 動态代理 不會去代理内部方法,而 CGLIB 動态代理 自動代理内部方法。

4. 外觀模式(Facade)

外觀模式隐藏了系統的複雜性,并向用戶端提供了一個可以通路系統的接口。簡單來說,該模式就是把一些複雜的流程封裝成一個接口供給外部使用者更簡單的使用。

外觀模式一般涉及到三個角色:

  • 門面角色:外觀模式的核心。它被客戶角色調用,它熟悉子系統的功能。内部根據客戶角色的需求預定了幾種功能的組合。(客戶調用,同時自身調用子系統功能)
  • 子系統角色:實作了子系統的功能。它對客戶角色和Facade時未知的。它内部可以有系統内的互相互動,也可以由供外界調用的接口。(實作具體功能)
  • 客戶角色:通過調用Facede來完成要實作的功能。(調用門面角色)
java複制代碼public class Computer {

    private CPU cpu;
    private Disk disk;
    private Memory memory;

    public Computer() {
        cpu = new CPU();
        memory = new Memory();
        disk = new Disk();
    }

    public void start() {
        System.out.println("Computer start begin");
        cpu.start();
        disk.start();
        memory.start();
        System.out.println("Computer start end");
    }

    public void shutDown() {
        System.out.println("Computer shutDown begin");
        cpu.shutDown();
        disk.shutDown();
        memory.shutDown();
        System.out.println("Computer shutDown end");
    }

    public static class CPU {

        public void start() {
            System.out.println("CPU is start...");
        }

        public void shutDown() {
            System.out.println("CPU is shutDown...");
        }

    }

    public static class Disk {

        public void start() {
            System.out.println("Disk is start...");
        }

        public void shutDown() {
            System.out.println("Disk is shutDown...");
        }

    }

    public static class Memory {

        public void start() {
            System.out.println("Memory is start...");
        }

        public void shutDown() {
            System.out.println("Memory is shutDown...");
        }

    }

}
           

每個 Computer 都有 CPU、Memory、Disk。在 Computer 開啟和關閉的時候,相應的部件也會開啟和關閉:

java複制代碼public class MainTest {

    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.start();
        System.out.println("=================");
        computer.shutDown();
    }

}
           
erlang複制代碼Computer start begin
CPU is start...
Disk is start...
Memory is start...
Computer start end
=================
Computer shutDown begin
CPU is shutDown...
Disk is shutDown...
Memory is shutDown...
Computer shutDown end
           

外觀模式主要的優點:

  • 松散耦合:使得用戶端和子系統之間解耦,讓子系統内部的子產品功能更容易擴充和維護;
  • 簡單易用:用戶端根本不需要知道子系統内部的實作,或者根本不需要知道子系統内部的構成,它隻需要跟 Facade 類互動即可;
  • 劃分通路層次:有些方法是對系統外的,有些方法是系統内部互相互動的使用的。子系統把那些暴露給外部的功能集中到門面中,這樣就可以實作用戶端的使用,很好的隐藏了子系統内部的細節。

5. 橋接模式(Bridge)

橋接模式将抽象與實作分離,使它們可以獨立變化。它是用組合關系代替繼承關系來實作,進而降低了抽象和實作這兩個可變次元的耦合度。

我們思考一個問題:我有 N 種不同品牌的手機,手機裡存在 M 個不同的軟體,我們要求每種不同品牌的手機中,不同的軟體都實作不一樣的功能,如果按照一般的繼承思路去擴充類,至少需要 N * M 個子類去實作不一樣的需求。

繼承可以很好的實作代碼複用(封裝)的功能,但這也是繼承的一大缺點。因為父類擁有的方法,子類也會繼承得到,無論子類需不需要,這說明繼承具備強侵入性(父類代碼侵入子類),同時會導緻子類臃腫。是以,在設計模式中,有一個原則為優先使用組合/聚合,而不是繼承。

我們換一種解決思路:品牌手機和手機的軟體本身就是兩種獨立不同的元素。但是他們之間又存在關聯關系,如果把這兩種不同的元素,放在兩個不同次元上去繼承擴充子類,再将兩個次元關聯起來,那麼我們的不同的子類數量可以減少到 N + M 個,通過次元組合類來達到 N * M 個需求的功能。

java複制代碼public interface Software {

    void run();

}
           
java複制代碼public class SoftwareA implements Software {

    @Override
    public void run() {
        System.out.println("Run software A");
    }

}
           
java複制代碼public class SoftwareB implements Software {

    @Override
    public void run() {
        System.out.println("Run software B");
    }

}
           

可以将抽象化部分與實作化部分分開,取消二者的繼承關系,改用組合關系。

java複制代碼public abstract class Phone {

    protected Software software;

    public void setSoftware(Software software) {
        this.software = software;
    }

    protected abstract void run();

    public void execute() {
        this.run();
        software.run();
    }

}
           
java複制代碼public class PhoneA extends Phone {

    @Override
    public void run() {
        System.out.println("Use phone A");
    }

}
           
java複制代碼public class PhoneB extends Phone {

    @Override
    public void run() {
        System.out.println("Use phone B");
    }

}
           
java複制代碼public class PhoneC extends Phone {

    @Override
    public void run() {
        System.out.println("Use phone C");
    }

}
           

橋接模式的一個常見使用場景就是替換繼承。我們知道,繼承擁有很多優點,比如,抽象、封裝、多态等,父類封裝共性,子類實作特性。

java複制代碼public class MainTest {

    public static void main(String[] args) {
        Phone phoneA = new PhoneA();
        phoneA.setSoftware(new SoftwareB());
        phoneA.execute();
    }

}
           
css複制代碼Use phone A
Run software B
           

當一個類内部具備兩種或多種變化次元時,使用橋接模式可以解耦這些變化的次元,使高層代碼架構穩定。 橋接模式通常适用于以下場景:

  1. 當一個類存在兩個獨立變化的次元,且這兩個次元都需要進行擴充時;
  2. 當一個系統不希望使用繼承或因為多層次繼承導緻系統類的個數急劇增加時;
  3. 當一個系統需要在構件的抽象化角色和具體化角色之間增加更多的靈活性時。

很多時候,我們分不清該使用繼承還是組合/聚合或其他方式等,其實可以從現實語義進行思考。因為軟體最終還是提供給現實生活中的人使用的,是服務于人類社會的,軟體是具備現實場景的。當我們從純代碼角度無法看清問題時,現實角度可能會提供更加開闊的思路。

6. 組合模式(Composite)

組合模式有時又叫作部分-整體模式,它是一種将對象組合成樹狀的層次結構的模式,用來表示“部分-整體”的關系,使使用者對單個對象群組合對象具有一緻的通路性。

組合模式的應用場景:

  • 在需要表示一個對象整體與部分的層次結構的場合。
  • 要求對使用者隐藏組合對象與單個對象的不同,使用者可以用統一的接口使用組合結構中的所有對象的場合。

組合模式一般用來描述整體與部分的關系,它将對象組織到樹形結構中,頂層的節點被稱為根節點,根節點下面可以包含樹枝節點和葉子節點,樹枝節點下面又可以包含樹枝節點和葉子節點。

java複制代碼public interface Component {

    public void operation();

}
           
java複制代碼public class Leaf implements Component {

    private String name;

    public Leaf(String name) {
        this.name = name;
    }

    @Override
    public void operation() {
        System.out.println("樹葉" + name + ":被通路!");
    }

}
           

其實根節點和樹枝節點本質上屬于同一種資料類型,可以作為容器使用;而葉子節點與樹枝節點在語義上不屬于用一種類型。但是在組合模式中,會把樹枝節點和葉子節點看作屬于同一種資料類型(用統一接口定義),讓它們具備一緻行為。

java複制代碼public class Composite implements Component {

    private ArrayList<Component> children = new ArrayList<>();

    public void add(Component c) {
        children.add(c);
    }

    public void remove(Component c) {
        children.remove(c);
    }

    public Component getChild(int i) {
        return children.get(i);
    }

    public void operation() {
        for (Component obj : children) {
            obj.operation();
        }
    }

}
           

這樣,在組合模式中,整個樹形結構中的對象都屬于同一種類型,帶來的好處就是使用者不需要辨識是樹枝節點還是葉子節點,可以直接進行操作,給使用者的使用帶來極大的便利。

java複制代碼public class MainTest {

    public static void main(String[] args) {
        Composite c0 = new Composite();
        Component leaf1 = new Leaf("1");
        c0.add(leaf1);

        Composite c1 = new Composite();
        c0.add(c1);
        Component leaf2 = new Leaf("2");
        Component leaf3 = new Leaf("3");
        c1.add(leaf2);
        c1.add(leaf3);

        c0.operation();
    }

}
           
複制代碼樹葉1:被通路!
樹葉2:被通路!
樹葉3:被通路!
           

組合模式的主要優點有:

  1. 組合模式使得用戶端代碼可以一緻地處理單個對象群組合對象,無須關心自己處理的是單個對象,還是組合對象,這簡化了用戶端代碼;
  2. 更容易在組合體内加入新的對象,用戶端不會因為加入了新的對象而更改源代碼,滿足“開閉原則”;

7. 享元模式(Flyweight)

享元模式通過共享的方式高效的支援大量細粒度的對象。在有大量對象時,有可能會造成記憶體溢出,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接傳回在記憶體中已有的對象,避免重新建立。

享元模式的應用場景:

  • 系統中有大量對象。
  • 這些對象消耗大量記憶體。
  • 這些對象的狀态大部分可以外部化。
  • 這些對象可以按照内蘊狀态分為很多組,當把外蘊對象從對象中剔除出來時,每一組對- 系統不依賴于這些對象身份,這些對象是不可分辨的。

簡單來說,我們抽取出一個對象的外部狀态(不能共享)和内部狀态(可以共享)。然後根據外部狀态的決定是否建立内部狀态對象。内部狀态對象是通過哈希表儲存的,當外部狀态相同的時候,不再重複的建立内部狀态對象,進而減少要建立對象的數量。

java複制代碼public interface IFlyweight {

    void print();

}
           
java複制代碼public class Flyweight implements IFlyweight {

    private String id;

    public Flyweight(String id) {
        this.id = id;
    }

    @Override
    public void print() {
        System.out.println("Flyweight.id = " + getId() + " ...");
    }

    public String getId() {
        return id;
    }

}
           

建立工廠,這裡要特别注意,為了避免享元對象被重複建立,我們使用 HashMap 中的 key 值保證其唯一。

java複制代碼public class FlyweightFactory {

    private Map<String, IFlyweight> flyweightMap = new HashMap();

    public IFlyweight getFlyweight(String str) {
        IFlyweight flyweight = flyweightMap.get(str);
        if (flyweight == null) {
            flyweight = new Flyweight(str);
            flyweightMap.put(str, flyweight);
        }
        return flyweight;
    }

    public int getFlyweightMapSize() {
        return flyweightMap.size();
    }

}
           

測試,我們建立三個字元串,但是隻會産生兩個享元對象。

java複制代碼public class MainTest {

    public static void main(String[] args) {
        FlyweightFactory flyweightFactory = new FlyweightFactory();
        IFlyweight flyweight1 = flyweightFactory.getFlyweight("A");
        IFlyweight flyweight2 = flyweightFactory.getFlyweight("B");
        IFlyweight flyweight3 = flyweightFactory.getFlyweight("A");
        flyweight1.print();
        flyweight2.print();
        flyweight3.print();
        System.out.println(flyweightFactory.getFlyweightMapSize());
    }

}
           
ini複制代碼Flyweight.id = A ...
Flyweight.id = B ...
Flyweight.id = A ...
2
           

享元模式 與 池化技術 的差別

對象池、連接配接池(比如資料庫連接配接池)、線程池等也是為了複用,但其與享元模式的複用有差別的。

享元模式中的“複用”可以了解為“共享使用”,在整個生命周期中,都是被所有使用者共享的,主要目的是節省空間。應用執行個體比如 Java 中的 String,如果有則傳回,如果沒有則建立一個字元串儲存在字元串緩存池裡面。

池化技術中的“複用”可以了解為“重複使用”,主要目的是節省時間(比如從對象池中取一個對象,不需要重新建立)。在任意時刻,每一個對象、連接配接、線程,并不會被多處使用,而是被一個使用者獨占,當使用完成之後,放回到池中,再由其他使用者重複利用。

作者:白菜說技術

連結:https://juejin.cn/post/7259385807550611514

繼續閱讀