淺談設計模式1——策略模式 | 適配器模式 | 工廠模式

前言

最近在看《Think In JAVA》,其中在講解繼承,組合,抽象類和接口的時候,提到了題中的幾個設計模式。這幾個設計模式也確實讓我更好的理解了JAVA中各個數據結構的含義。今天就結合書本還有本身的理解,稍微整理一下這幾個設計模式。html

Strategy Pattern | 策略模式

這裏就必需要提一下向上轉化這個概念。在繼承和接口中都有提到這個概念。
向上轉化在繼承中是指子類能夠向上轉化爲父類。好比,有一個Instrument類,以及它的一個子類Flute。子類重寫的play方法會覆蓋父類的方法。java

class Instrument{
        public void play(){
            ...
            //play instrument
        }
    }
    
    class Flute extends Instrument{
        public void play(){
            ...
            //play flute
        }
    }

若是這時有一個方法須要接收一個樂器參數並演奏,那麼無需寫多個重載方法接收各類不一樣的樂器,只須要一個接收Instrument類的方法。算法

public void play(Instrument instrument){
        instrument.play();
    }

在這個方法中傳入一個Flute對象,調用的將是flute中的play方法。這就是向上轉化,動態綁定的一個最簡單的例子。
其實接口也是同理,只是接口容許多種向上轉化。也就是說,JAVA中繼承是惟一的,而接口是能夠Implement多個的。所以JAVA中繼承向上轉化的路徑惟一,而接口向上轉化路徑不惟一。spring

接下來就要講到策略模式。sql

策略模式的概念以下:數據庫

Defines a set of encapsulated algorithms that can be swapped to carry out a specific behaviour
定義了一組封裝好的算法,這些算法分別執行不一樣的操做。在實際運行中,這些算法能夠動態切換來知足不一樣場景下的需求編程

策略模式的使用情景有:設計模式

  1. 將文件保存爲不一樣的格式安全

  2. 排序算法的多種實現數據結構

  3. 文件壓縮的多種實現

也就是說,策略模式將一組完成相同工做的不一樣方式的代碼分別放到不一樣的類中,並經過策略模式實如今運行中的相互切換。

clipboard.png

這是從網上找到的關於策略模式的UML圖。
策略模式是JAVA中繼承,抽象類以及接口的一種綜合應用。在策略模式中,咱們能夠根據一個「開放接口」設計出多種「具體策略」,而後在調用時只須要輸入「開放接口」,程序運行時會根據「開放接口」的具體實現來決定具體的運行結果。

上面設計模式的代碼以下:

//接口
public interface Strategy {
   public int doOperation(int num1, int num2);
}

//接口實現類
public class OperationAdd implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 + num2;
   }
}
public class OperationSubstract implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 - num2;
   }
}
public class OperationMultiply implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 * num2;
   }
}

//上下文
public class Context {
   private Strategy strategy;

   public Context(Strategy strategy){
      this.strategy = strategy;
   }

   public int executeStrategy(int num1, int num2){
      return strategy.doOperation(num1, num2);
   }
}

//具體調用
public class StrategyPatternDemo {
   public static void main(String[] args) {
      Context context = new Context(new OperationAdd());        
      System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationSubstract());        
      System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationMultiply());        
      System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
   }
}

下面我再講一個具體的例子來講明使用策略模式與不使用策略模式的差距。

假設咱們有一個壓縮文件的功能,壓縮文件有多種算法,如Zip,RAR等等。程序可以根據實際的操做系統以及性能等參數來選擇一個壓縮算法執行壓縮操做。咱們假設這個選擇具體算法的功能放置CompressionPreference類中。
其實這些對於客戶端來講都是透明的。也就是說,客戶端只知道會有一個壓縮功能,該功能須要客戶上傳要壓縮的文件。如此場景下,服務端只須要提供一個壓縮的接口,而無需暴露具體的實現。

代碼以下:

//選擇壓縮方法類,根據具體狀況返回壓縮的方法
public class CompressionPreference{
     public static CompressionStrategy getPreferedStrategy(){
         //根據系統的狀況或是用戶的選擇返回具體的壓縮算法
     }
}

//壓縮策略接口
public interface CompressionStrategy{
    void compress(List<File> files);
}

//壓縮策略的具體實現
public class ZipCompressionStrategy implements CompressionStrategy{
    @Override
    public void compress(List<File> files){
        //zip 壓縮
    }
}

public class RarCompressionStrategy implements CompressionStrategy{
    @Override
    public void compress(List<File> files){
        //RAR 壓縮
    }
}

public class CompressionContext{
    private CompressionStrategy strategy;
    
    //這裏根據CompressionPreference選擇壓縮策略
    public void setStrategy(CompressionStrategy strategy){this.strategy=strategy);}
    
    public void createArchieve(List<File> files){
        strategy.compress(files);
    }
}

//客戶端調用
public class Client{
    public static void main(String[] args) {
    CompressionContext ctx = new CompressionContext();
    //設置壓縮上下文
    ctx.setCompressionStrategy(CompressionPreference.getPreferedStrategy());
    ctx.createArchive(fileList);
  }
}

經過這樣的設計以後,若是須要添加新的算法,只須要增長一個CompressionStrategy的具體實現類,以及修改一下CompressionPreference中的方法便可。對於客戶端的調用不會產生任何影響。

若是不對算法進行封裝,直接容許客戶端調用的話。一方面,暴露了壓縮算法的種種實現,另外一方面,也增長了可能形成的錯誤調用。並且一旦增長新的壓縮算法,客戶端也須要知道這些本不須要知道的東西,調整本身的調用。這樣的代碼,可維護性實在是太差了。

適配器模式 | Adapter Design Pattern

定義:

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
將兩個不想兼容的接口經過適配器進行相互轉化。

適配器模式要比策略模式要好理解一些。在書中講解適配器模式時,其實是爲了補充說明如何面向接口編程。適配器模式,顧名思義,就是將原本並不繼承某個接口的類經過適配器轉化爲能夠經過該接口調用,它充當着連個不兼容的接口之間的橋樑。

從原含義上來說,適配器是指一個接口轉換器,在生活中最多見的接口轉換器就是你的手機充電線頭啦!充電頭將從插座中輸出的標準220V電壓(國內)轉化爲能夠安全充電的電壓。而且在另外一側提供了一個USB充電口,從而使手機能夠在一切含有USB端口的充電線下進行充電。再舉一個例子,也就是SD卡。使用相機的朋友知道,有些電腦是不提供SD卡接口的,那麼就須要將SD卡插入SD卡讀卡器,再將讀卡器經過USB接口插入電腦。這時電腦就能夠讀取SD卡中的內容了。

在書中的例子,適配器應用的場景是將不能夠修改的類改成繼承某個接口從而能夠做爲該接口的一個實現類被調用。書中的適配器模式有兩種實現方式,一種是經過代理,另外一種是經過繼承。兩種方式本質上是相同的,若是須要原類中的全部實現,則經過繼承方式實現適配器,若是隻是一部分實現,則經過代理的方式。具體狀況具體分析。


以上講的都太過抽象了,下面講一個具體的例子。

好比我有一個掃描類Scanner,他有一個方法,能夠接收全部繼承了Readable接口的類,並根據類中的狀況將其中的數據讀取出來。系統中已經有一些類,他們是能夠被Scanner讀取的,可是他們並無繼承Readable接口。本着開閉原則,我能夠給這些類添加一個適配器,使其能夠被Scanner讀取。經過這種模式,不管是出現新的Scanner讀取文件或是讀取系統已有的文件,都沒必要修改Scanner方法。只須要使其支持Readable接口就行。

public class Scanner{
        public void read(Readable material){
            material.read();
        }
    }
    
    public interface Readable{
        void read();
    }
    public class TXT implements Readable{
        ...
        public void read(){
            //讀取txt文件
        }
        ...
    }
    
    public class HTML{
        public void toReadableFormat(){
            //html文件也能夠被讀取,可是它並無繼承Readable接口,因此沒法被Scanner
            識別
        }
    }
    
    //這裏纔是適配器模式
    public class HTMLAdapter implements Readable{
        ...
        private HTML html;
        public HTMLAdapter(HTML html){this.html = html}
        public void read(){
            html.toReadableFormat();
        }
        ...
    }
    
    //這時候兩個文件均可以被讀取了
    public class Test{
        public static void main(String[] args){
            Scanner s = new Scanner();
            s.read(new TXT());
            s.read(new HTMLAdapter(new HTML()));
        }
    }

一個例子不夠,再來一個~

在自媒體的發展史中,媒體的格式愈來愈多樣化,從最初的文本,到MP3,再到視頻格式。若是如今有一個系統,它原本只支持MP3格式的文件的讀取,這時候要想該系統能夠支持新媒體類的文件的播放。新媒體類文件由另外一個團隊開發,擁有本身的開發接口和具體實現。如何才能將該模塊融入到現有系統中呢?

這時候就須要經過適配器模式來解決這個問題了。

clipboard.png

這是這個系統給的UML類圖。經過在原系統中新建一個MediaAdapter適配器繼承原媒體播放器的接口,從而使原系統能夠在不知下層變更的基礎上,繼續調用原來的play方法來實現播放功能。
具體代碼以下:

public interface MediaPlayer {
       public void play(String audioType, String fileName);
    }
    
    public interface AdvancedMediaPlayer {    
       public void playVlc(String fileName);
       public void playMp4(String fileName);
    }
    
    public class VlcPlayer implements AdvancedMediaPlayer{
       @Override
       public void playVlc(String fileName) {
          System.out.println("Playing vlc file. Name: "+ fileName);        
       }

       @Override
       public void playMp4(String fileName) {
          //do nothing
       }
    }
    
    public class Mp4Player implements AdvancedMediaPlayer{

       @Override
       public void playVlc(String fileName) {
          //do nothing
       }

       @Override
       public void playMp4(String fileName) {
          System.out.println("Playing mp4 file. Name: "+ fileName);        
       }
    }
    
    public class MediaAdapter implements MediaPlayer {

       AdvancedMediaPlayer advancedMusicPlayer;

       public MediaAdapter(String audioType){
   
          if(audioType.equalsIgnoreCase("vlc") ){
             advancedMusicPlayer = new VlcPlayer();            
         
          }else if (audioType.equalsIgnoreCase("mp4")){
             advancedMusicPlayer = new Mp4Player();
          }    
       }

       @Override
       public void play(String audioType, String fileName) {
   
          if(audioType.equalsIgnoreCase("vlc")){
             advancedMusicPlayer.playVlc(fileName);
          }
          else if(audioType.equalsIgnoreCase("mp4")){
             advancedMusicPlayer.playMp4(fileName);
          }
       }
    }
    
    public class AdapterPatternDemo {
       public static void main(String[] args) {
          MediaPlayer audioPlayer = new AudioPlayer();
          audioPlayer.play("mp3", "beyond the horizon.mp3");
          MediaPlayer videoPlayer = new MediaAdapter();
          videoPlayer.play("vlc", "far far away.vlc");
   }
}

工廠模式 | Factory Design Pattern

終於到工廠模式了~~~~寫了很久了呀QAQ

工廠模式是爲了管理一個接口之下衆多實現類。好比最多見的DAO接口。數據庫中的表少至10個多能夠至百個。在spring框架中,經過依賴倒置和自動注入實現了這麼多讀取數據庫的接口實現類的管理。那麼在沒有框架的場景下,如何纔可使上層代碼和下層具體的DAO接口解耦呢?這時就須要工廠模式。經過工廠模式得到具體DAO接口。

至於爲何要選擇這樣的一個工廠模式,而不是直接new一個具體的實現類呢?這裏舉個例子。比方說,有一個DAO接口,實現該接口的有UserDaoImpl, AccountDaoImpl等。假設有兩個類均用到UserDaoImpl。若是在這兩個類中均使用new來建立一個新的UserDaoImpl,那麼一旦有一天,由於需求變動,須要將UserDaoImpl換成AnotherUserDaoImpl,則須要在兩個類中分別修改。那麼若是有十個類,甚至一百個類都用到了這個Dao呢?這時候若是我是經過工廠來得到這個Dao,也就只須要在工廠中將返回值從原來的UserDaoImpl變成AnotherUserDaoImpl,並不會影響調用方。


簡單工廠模式 | Static Factory Method

下面給一個簡單的工廠模式的例子。

interface Dog
{
  public void speak ();
}

class Poodle implements Dog
{
  public void speak()
  {
    System.out.println("The poodle says \"arf\"");
  }
}

class Rottweiler implements Dog
{
  public void speak()
  {
    System.out.println("The Rottweiler says (in a very deep voice) \"WOOF!\"");
  }
}

class SiberianHusky implements Dog
{
  public void speak()
  {
    System.out.println("The husky says \"Dude, what's up?\"");
  }
}

class DogFactory
{
  public static Dog getDog(String criteria)
  {
    if ( criteria.equals("small") )
      return new Poodle();
    else if ( criteria.equals("big") )
      return new Rottweiler();
    else if ( criteria.equals("working") )
      return new SiberianHusky();

    return null;
  }
}

public class JavaFactoryPatternExample
{
  public static void main(String[] args)
  {
    // create a small dog
    Dog dog = DogFactory.getDog("small");
    dog.speak();

    // create a big dog
    dog = DogFactory.getDog("big");
    dog.speak();

    // create a working dog
    dog = DogFactory.getDog("working");
    dog.speak();
  }
}

在簡單的工廠模式中,工廠根據輸入的條件返回給一個接口的具體實現。
簡單工廠模式有一個問題,就是一旦工廠出現新的產品,就必須修改工廠中獲取產品的方法,這有違開閉原則。並且工廠模式承擔的壓力太重,可能會致使職責的混亂。最重要的是,簡單工廠模式中,獲取產品的方法是靜態方法,該方法沒法經過繼承等形式獲得擴展。


工廠方法模式 | Factory Method Pattern

這實際上是工廠模式的一個簡單的升級。考慮一個真實工廠的場景。它的產品Product之下每每還有許多分類,如軸承,輪胎。各個子分類每每也對應着不一樣的車間,如軸承車間,輪胎車間。若是還用簡單工廠模式返回一個Product,且不說向上轉型可能丟失的一些數據,並且工廠的壓力也太大了,由於可能要根據不一樣場景返回上百個不一樣類型但繼承了同一接口的類。這不符合設計原則。

這時候就出現了工廠方法模式。不只僅對產品抽象,還對工廠抽象。對不一樣的產品提供不一樣的工廠,將職責進一步細化,知足SRP(單一職責原則)。同時,由於不須要輸入無關的判斷數據,也解除了控制耦合。

具體例子有最多見的日誌系統。日誌系統之下每每針對各個不一樣的子系統,好比數據庫日誌子系統,好比文件日誌子系統。不一樣的日誌系統對應的日誌文件也不一樣。這時經過工廠方法模式就能夠很好的解決兩者之間的關係。

clipboard.png

在這張圖中還能夠繼續延伸,好比數據庫包括Mysql數據庫,Oracle數據庫等等。在UML圖中也能夠繼續根據MySqlLog建立MysqlLogFactory。

還有一個具體的例子就是JAVA中的數據庫鏈接。

Connection conn=DriverManager.getConnection("jdbc:microsoft:sqlserver://loc
alhost:1433; DatabaseName=DB;user=sa;password=");
Statement statement=conn.createStatement();
ResultSet rs=statement.executeQuery("select * from UserInfo");

這裏經過DriverManager工廠根據輸入的信息返回一個對應的鏈接。鏈接中再返回對應的抽象語句statement。根據工廠中的信息能夠知道,這個statement的底層實現一定是一個相似SqlServerStatement的實現。

抽象工廠模式 | Abstract Factory Method

產品等級結構:產品等級結構即產品的繼承結構,如一個抽象類是電視機,其子類有海爾電視機、海信電視機、TCL電視機,則抽象電視機與具體品牌的電視機之間構成了一個產品等級結構,抽象電視機是父類,而具體品牌的電視機是其子類。
產品族:在抽象工廠模式中,產品族是指由同一個工廠生產的,位於不一樣產品等級結構中的一組產品,如海爾電器工廠生產的海爾電視機、海爾電冰箱,海爾電視機位於電視機產品等級結構中,海爾電冰箱位於電冰箱產品等級結構中。

抽象工廠模式與工廠方法模式最大的區別在於,工廠方法模式針對的是一個產品等級結構,而抽象工廠模式則須要面對多個產品等級結構,一個工廠等級結構能夠負責多個不一樣產品等級結構中的產品對象的建立 。當一個工廠等級結構能夠建立出分屬於不一樣產品等級結構的一個產品族中的全部對象時,抽象工廠模式比工廠方法模式更爲簡單、有效率。

在這裏的上下文中,抽象工廠之下的子工廠被劃分爲海爾子工廠,海信子工廠。在海爾自工廠中能夠得到海爾冰箱(productA),海爾電視機(productB), 同理,在海信子工廠中,能夠得到海信冰箱(productA),海信電視機(productB)。

固然了 在大多數的應用場景下,工廠設計模式已經足夠了。

clipboard.png

References

Tutorialspoint Design Pattern
stackoverflow : how does the strategy design pattern work
dzon strategy design pattern
dzon adapter pattern
工廠模式的一個簡單例子
工廠模式的一篇極好博客
幾種工廠模式之間的對比

相關文章
相關標籤/搜索