【設計模式】常見設計模式

  最近依然在應對面試,有人問到設計模式的題目,之前自己看過,但是都忘記了,現在整理一下。

1.單例模式

  
  有時,允許自由創建某個類的實例沒有意義,反而可能會導致系統性能下降。例如:數據庫引擎訪問點、Hibernate的SessionFactory都只需要一個實例即可,此時可以使用單例模式。
  如果一個類始終只能創建一個實例,則稱這個類爲單例類,這種模式爲單例模式。
  Spring中框架中可以直接在配置時通過制定scope=”singleton”實現單例模式。
  Java代碼可以自己實現單例模式,如下:

class Singleton{
    //類變量緩存曾經創建的實例
    private static Singleton instance;
    //構造方法私有
    private Singleton(){}
    //靜態方法,返回類的實例
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

  思路:把構造方法設爲私有,這樣在類外就無法調用構造器生成對象的實例。然後在類中聲明一個類變量用來緩存生成的實例。這樣的話,還有個問題是無法生成這個對象的實例,因此需要寫一個方法生成那個單例的對象實例。做法:聲明一個靜態方法,判斷類變量對否爲null,如果是null說明沒有生成實例,就需要調用私有的構造方法生產一個實例;如果有了的話,就不需要如何操作。最後返回這個實例。
  

2.簡單工廠模式

  
  開發過程中經常遇到A實例需要調用B實例。這個時候大多數做法是直接new一個B出來。這樣的一個缺點就是代碼耦合了,因爲A中調用了B類的類名(硬編碼耦合)。後果就是如果我們後來重構了,需要用C來替換B,這樣的後果就是每個B都需要修改爲C。工作量很大。
  解決方法:因爲我們只是調用了B中的方法,不需要關心B的創建、實現過程,因此可以使用一個接口:IB,讓B實現接口IB。這樣A依賴的就不是具體的類了,而是具體的接口。然後創建一個工程類:IBFactory,該工廠負責產生IB的實例。A只需要調用IBFactory裏面的方法就可以拿到IB的實例了。
  將來如果出現重構:C代替B的情況,只需要讓C也實現IB接口,然後修改IBFactory裏面代碼,讓工廠產生C實例就可以了。
  代碼:
  IB接口。

public interface IB {
    public void show();
}

  B類

public class B implements IB {
    @Override
    public void show() {
        System.out.println("this is B!!");
    }
}

  C類

public class C implements IB {
    @Override
    public void show() {
        System.out.println("this is C!!");
    }
}

  IBFactory。工廠類,返回實例。

public class IBFactory {
    public IB getIB() {
        //如果是return new B();則返回B的實例
        //重構只需要修改這裏。
        return new C();
    }
}

  A類。

public class A {
    private IB ib;
    public A(IB ib){
        this.ib = ib;
    }
    public void show(){
        ib.show();
    }

    public static void main(String[] args) {
        IBFactory ibFactory = new IBFactory();
        A a = new A(ibFactory.getIB());
        a.show();
    }
}

3.工廠方法和抽象工廠

  
  (1)工廠方法:
  在上面2中,系統通過工廠方法生產了對象的實例,在工廠類中決定生產那個對象的實例。但是這樣也不太完美。修改生產的產品是,需要去工廠裏面直接修改代碼,這樣不太好。希望在調用工廠的時候(即在A中),直接可以判斷需要生產什麼產品。換言之,重構時,只需要修改A中代碼就可以了。
  解決辦法,可以設計一個工廠的接口,程序爲不同的產品設計不同的工廠。
  代碼:
  Factory類。生產不同工廠的工廠類。

public interface Factory {
    IB getIB();
}

  BFactory.java。生產B產品的工廠。

public class BFactory implements Factory {
    @Override
    public IB getIB() {
        return new B();
    }
}

  CFactory.java。生產C產品的工廠。

public class CFactory implements Factory {
    @Override
    public IB getIB() {
        return new C();
    }
}

  IB.java,B.java,C.java與前面簡單工廠類一樣,不在贅述。
  A.java代碼。

public class A {
    private IB ib;
    public A(IB ib){
        this.ib = ib;
    }
    public void show(){
        ib.show();
    }

    public static void main(String[] args) {
        //使用CFactory子類創建Factory
        Factory factory =  new CFactory();
        A a = new A(factory.getIB());
        a.show();
    }
}

  (2)抽象工廠
  對於上面的工廠方法,依然存在一種耦合:客戶端代碼與不同的工廠類耦合。解決這個辦法可以再增加一個工廠類,這個類來製造不同的工廠:
  代碼:
  添加類FactoryFactory.java

public class FactoryFactory {
    public static Factory getFactory(String type){
        if(type.equalsIgnoreCase("b"))
            return new BFactory();
        else
            return new CFactory();
    }
}

  A.java修改代碼:

public class A {
    private IB ib;
    public A(IB ib){
        this.ib = ib;
    }
    public void show(){
        ib.show();
    }

    public static void main(String[] args) {
        //通過傳參來確定生產的工廠
        Factory factory =  FactoryFactory.getFactory("B");
        A a = new A(factory.getIB());
        a.show();
    }
}

  
  “抽象工廠”與“簡單工廠”區別:簡單工廠直接生產對被調用對象;抽象工廠生產工廠對象。
  Spring框架的IoC容器可以認爲是抽象工廠。可以管理Bean實例,也可以管理工廠實例。
  

4.代理模式

  當客戶端代碼需要調用某個對象的時候,客戶端實際上不關心是否準確得到了這個對象,只要一個能提供該功能的對象即可,此時可以返回對象的代理。
  這種設計模式下,系統會爲某個對象生成一個代理,由這個代理控制源對象的引用。這種情況下,客戶端代碼僅僅持有一個被代理對象的接口,變爲面向接口編程。
  其實這裏可以看看動態代理、靜態代理的知識。
  代碼:
  BigImage.java。模擬大的圖片。

public class BigImage implements Image {
    public BigImage(){
        try {
            //暫停3秒,模擬系統開銷
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void show() {
        System.out.println("image show...");
    }
}

  ProxyImage.java,圖片的代理類。

public class ProxyImage implements Image {

    private Image image;

    public ProxyImage(Image image){
        this.image = image;
    }

    @Override
    public void show() {
        if(image == null){
            image = new BigImage();
        }
        image.show();
    }
}

  測試類。

public class ProxyTest {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        Image image = new ProxyImage(null);
        System.out.println("加載圖片耗時:"+(System.currentTimeMillis()-startTime));
        //調用show方法時,纔會真正執行加載
        long startTime2 = System.currentTimeMillis();
        image.show();
        System.out.println("加載圖片真正耗時:"+(System.currentTimeMillis()-startTime2));
    }
}

  結果:
  運行結果

  Hibernate框架使用了代理模式,比如它的延遲加載。加入A、B實體存在關聯關係,在加載A實體時,B應該被加載。採取延遲加載,系統會生產一個B的代理,只有A真正需要訪問B中變量的時候,纔會真正加載B實體。從而節約開銷。

5.命令模式

  很多時候,我們需要一個方法完成一個功能,而且,這個功能大多數步驟我們已知並確定,但是有少量具體的步驟,需要在執行這個方法的時候,才能確定。這個時候可以使用命令模式。比如:我們有一個方法遍歷數組的每一個元素,但是對元素的具體操作(迭代輸出、累加)需要在調用該方法的時候才能確定,則需要在調用該方法時指定具體的處理行爲。
  代碼:
  ProcessArray.java,裏面有一個each方法,用於處理數組。具體操作不知道,所以需要傳入一個Command參數。

public class ProcessArray {
    public void each(int[] target, Command cmd) {
        cmd.process(target);
    }
}

  Command.java,裏面的process方法定義了對於數組的處理行爲。這是一個接口

public interface Command {
    void process(int[] target);
}

  CommandTest.java。測試命令方法。

public class CommandTest {
    public static void main(String[] args) {
        ProcessArray processArray = new ProcessArray();
        int[] target = {5, 2, 8, 4};
        processArray.each(target, new Command() {
            @Override
            public void process(int[] target) {
                for (int temp : target) {
                    System.out.println("迭代輸出:" + temp);
                }
            }
        });
    }
}

  如果需要對數組進行其他的操作,只需要修改傳入的匿名類的實例。不同的Command實例封裝了不同的操作。
  

6.策略模式

  
  如果現在有這樣一個需求,有一件商品,不同的用戶(普通會員、VIP)所享受到的折扣不一樣。需要針對不同的打折需求計算不同的價格。以往的做法就是switch()語句解決。但是一個弊端就是,如果某天我們增加了一個新的打折情況,則需要在switch中添加新的case,並添加新的語句,然後針對新的打折。現在使用策略模式,我們只需要新建一個類,實現統一的接口,描述這種打折的具體操作就可以了,不需要修改之前的代碼。
  代碼:
  首先我們寫打折接口。DiscountStrategy.java。

public interface DiscountStrategy {
    double getDiscount(double originPrice);
}

  其次兩個打折具體類,實現這個接口。
  普通用戶打折。CommonDiscount .java。

public class CommonDiscount implements DiscountStrategy {
    @Override
    public double getDiscount(double originPrice) {
        System.out.println("普通用戶打折...");
        return originPrice * 0.7;
    }
}

  VIP用戶的打折。VIPDiscount .java。

public class VIPDiscount implements DiscountStrategy {
    @Override
    public double getDiscount(double originPrice) {
        System.out.println("VIP 打五折...");
        return originPrice * 0.5;
    }
}

  選擇策略算法、返回價格的類。DiscountContext.java。

public class DiscountContext {
    private DiscountStrategy strategy;
    public DiscountContext(DiscountStrategy strategy){
        this.strategy = strategy;
    }

    public double getDiscount(double price){
        return strategy.getDiscount(price);
    }

    //改變策略
    public void changeStrategy(DiscountStrategy strategy){
        this.strategy = strategy;
    }
}

  測試

public class Test {
    public static void main(String[] args) {
        //null代表普通用戶
        DiscountContext discountContext = new DiscountContext(new CommonDiscount());
        double price = 998.99;
        System.out.println("普通用戶價格:" + discountContext.getDiscount(price));
        discountContext.changeStrategy(new VIPDiscount());
        System.out.println("VIP用戶價格:" + discountContext.getDiscount(price));
    }
}

  之後的代碼修改中,如果新增加一種打折方式,只需要寫一個實現了打折接口的類,然後在測試類中添加

discountContext.changeStrategy(new /*打折類*/); 

  就可以了。
  
  Hibernate中數據庫方言Dialect就是使用的這種模式。

7.門面模式

  
  很多情況下,我們進行一個操作的步驟是固定的。比如餐館點餐吃飯,首先會有點餐,其次廚師做飯,最後服務員上菜…這些步驟都是一定的了。但是每次這樣一個過程都需要調用三個類(訂餐、做飯、上菜),比較麻煩,我們可以寫一個類(門面),類中一次調用這三個類。在以後顧客點餐的時候,直接調用這個門面。
 代碼:
 PayMent.java。訂餐。

public class Payment {
    public String pay(String food) {
        System.out.println("點餐:" + food + "一份!");
        return food;
    }
}

  Cook.java。廚師類。

public class Cook {
    public String cook(String food) {
        System.out.println("廚師正在烹調:" + food);
        return food;
    }
}

  Serve.java。服務類。

public class Serve {
    public void serve(String food) {
        System.out.println("服務員上菜:" + food);
    }
}

  Facade.java。門面類。

public class Facade {
    Payment payment;
    Cook cook;
    Serve serve;
    public Facade() {
        this.payment = new Payment();
        this.cook = new Cook();
        this.serve = new Serve();
    }
    public void serveFood(String food){
        payment.pay(food);
        cook.cook(food);
        serve.serve(food);
    }
}

  測試

public class Test {
    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.serveFood("牛肉麪");
    }
}

  我們在門面裏面封裝了要執行的步驟,在程序中可以直接調用門面完成一系列操作。
  Hibernate框架中,hibernateTemplate就是用了這種模式。例如save操作,封裝了SessionFactory、session等門面。
  

8.橋接模式

  
  開發中,遇到兩個維度的結構模式,僅僅使用繼承無法實現。比如麪條,可能有材料維度(牛肉、羊肉、豬肉),也有口味維度(清淡、微辣)等等。這個時候可以這樣解決。
  設置一個接口,專門記錄口味的。不同的口味,對應一個實現類。
  設置一個抽象類,該類有一個變量記錄麪條的口味。然後對於具體的材料(豬肉、牛肉等)繼承這個抽象類。
  這樣對於添加口味維度或者材料維度,都只需要添加一個類就可以。
  代碼:
  口味維度的兩個類。

public class PepperyStyle implements Peppery {
    @Override
    public String style() {
        return "辣口味";
    }
}
public class PlainStyle implements Peppery {
    @Override
    public String style() {
        return "清淡口味";
    }
}

  麪條抽象類。

public abstract class AbstractNoodle {
    Peppery style;
    public AbstractNoodle(Peppery style){
        this.style = style;
    }
    public abstract void eat();
}

  牛肉麪以及豬肉面類。

public class BeefNoodle extends AbstractNoodle {

    public BeefNoodle(Peppery style) {
        super(style);
    }

    @Override
    public void eat() {
        System.out.println("牛肉麪,口味:" + super.style.style());
    }
}
public class ProkyNoodle extends AbstractNoodle {

    public ProkyNoodle(Peppery style) {
        super(style);
    }

    @Override
    public void eat() {
        System.out.println("豬肉面,口味:" + super.style.style());
    }
}

  測試類。

public class Test {
    public static void main(String[] args) {
        AbstractNoodle noodle1 = new BeefNoodle(new PepperyStyle());
        noodle1.eat();
        AbstractNoodle noodle2 = new ProkyNoodle(new PlainStyle());
        noodle2.eat();
    }
}

9.觀察者模式

  比較常見的一種模式。
  定義了對象間一對多的依賴關係:一個或者多個觀察者對象觀察一個主題對象。當主題對象發生變化時,系統通知所有的觀察此對象的觀察者。
  一般包括四個角色:
  被觀察者的基類對象:持有多個觀察者的引用。
  觀察者接口:所有觀察者必須實現的一個接口。
  被觀察者實現類:繼承被觀察者的基類對象。
  觀察者實現類:實現觀察者接口。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章