設計模式——從工廠方法模式到 IOC/DI思想

糖拌蕃茄發表於2021-03-30

回顧簡單工廠

回顧:從介面的角度去理解簡單工廠模式

前面說到了簡單工廠的本質是選擇實現,說白了是由一個專門的類去負責生產我們所需要的物件,從而將物件的建立從程式碼中剝離出來,實現鬆耦合。我們來看一個例子:

我們要建立一個檔案匯出工具

public interface FileOper{

  public Boolean exceptFile(String data);
}
public class XMLFileOp implment FileOper{

   public Boolean exceptFile(String data){
      System.out.println("匯出一個xml檔案");
      return true;
   } 
}
public class Factory{

    public static FileOper createFileOp(){

        return new XMLFileOp();
    } 

}
public Class Test{

 public static void main(String args[]){

    FileOper op = Factory.createFileOp();
    op.exceptFile("測試");

}


}

這樣看起來沒什麼問題,那麼我們既然做出來了這個結構,就是為了後續的擴充套件它,例子中只是為了實現XML檔案的匯出,後續,我們可以自己實現一個txt檔案的匯出類,只需要實現FileOper介面就好:

public Class TxtFileOp implment FileOper{ 

public Boolean ExceptFile(String data){
System.out.println(
"匯出txt檔案");
return true
}
}

這時候我們還是通過工廠來獲取這個物件,只需將Factory中追加一個else if 即可通過傳參來獲取想要的物件了。

工廠方法模式

仔細分析上面的場景,事實上在實現匯出檔案的業務邏輯中,它根本不知道要使用哪一種匯出檔案的格式,因此這個物件根本就不應該和具體匯出檔案的物件耦合在一起,它只需要面向匯出檔案介面(FileOper)就好,這是工廠的思想,我們上面用簡單工廠沒錯啊,但是後面又加入了新的擴充套件

這樣一來,又有新的問題,面對新的類,簡單工廠便不能提供動態的擴充套件,必須要去修改內部的程式碼,破壞了開閉原則。我們上一篇也提到了,簡單工廠也有它自身的缺陷,其中最嚴重的就是,它雖然對依賴物件的主體實現瞭解耦,可是它本身內部卻耦合較為嚴重。這時候我們可以看看工廠方法模式了,工廠方法模式的思路很有意思:老子不管了!採取無為而治的方式。不是需要介面物件麼,那就定義一個方法來建立,可是事實上它自己是不知道如何建立這個介面物件的,不過這不重要,定義成抽象方法就行了,交給子類去實現,老子欠債,兒子你來還。

工廠方法的結構

Product:工廠方法所建立的具體物件的統一介面。

Factory:為該類產品的抽象工廠,其內部有宣告的工廠方法,工廠方法多為抽象方法,且返回一個Product物件

ProductOne:為具體的產品,也就是Product的具體實現類,真正的工廠產物。

SpecificFactory:具體的工廠,用於生產指定型別的Product,例如圖中它只負責生產 ProductOne這個物件。

工廠方法模式的樣例程式碼

public class AbstractFactoryTest {
    public static void main(String[] args) {
        try {
            Product a;
            AbstractFactory af;
            af = (AbstractFactory) ReadXML1.getObject();
            a = af.newProduct();
            a.show();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}
//抽象產品:提供了產品的介面
interface Product {
    public void show();
}
//具體產品1:實現抽象產品中的抽象方法
class ConcreteProduct1 implements Product {
    public void show() {
        System.out.println("具體產品1顯示...");
    }
}
//具體產品2:實現抽象產品中的抽象方法
class ConcreteProduct2 implements Product {
    public void show() {
        System.out.println("具體產品2顯示...");
    }
}
//抽象工廠:提供了廠品的生成方法
interface AbstractFactory {
    public Product newProduct();
}
//具體工廠1:實現了廠品的生成方法
class ConcreteFactory1 implements AbstractFactory {
    public Product newProduct() {
        System.out.println("具體工廠1生成-->具體產品1...");
        return new ConcreteProduct1();
    }
}
//具體工廠2:實現了廠品的生成方法
class ConcreteFactory2 implements AbstractFactory {
    public Product newProduct() {
        System.out.println("具體工廠2生成-->具體產品2...");
        return new ConcreteProduct2();
    }
}

  基於XML解析的外部配置檔案

class ReadXML1 {
    //該方法用於從XML配置檔案中提取具體類類名,並返回一個例項物件
    public static Object getObject() {
        try {
            //建立文件物件
            DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = dFactory.newDocumentBuilder();
            Document doc;
            doc = builder.parse(new File("src/FactoryMethod/config1.xml"));
            //獲取包含類名的文字節點
            NodeList nl = doc.getElementsByTagName("className");
            Node classNode = nl.item(0).getFirstChild();
            String cName = "FactoryMethod." + classNode.getNodeValue();
            //System.out.println("新類名:"+cName);
            //通過類名生成例項物件並將其返回
            Class<?> c = Class.forName(cName);
            Object obj = c.newInstance();
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

 

上面是一個較為初級也較為經典的工廠方法模板,工廠方法還有另一種用法,即抽象的工廠父類除了建立物件的方法之外,還包含其他的一些方法,這一種的實現方式是 :父類會是一個抽象類,裡面包含建立所需物件的抽象方法,這些抽象方法就是工廠方法。父類裡面,通常會有使用這些產品物件來實現一定的功能的方法。而這些方法所實現的功能通常都是公共功能,不管子類選擇了何種具體的產品實現,這些方法總能正常執行。

之所以會有上面兩種方式,主要原因在於工廠方法對於客戶端的支援,這裡需要弄清楚一個問題,誰在使用工廠方法所建立的物件?

事實上,在工廠方法模式裡,應該是工廠中的其他方法來使用工廠所建立的物件,為了方便,工廠方法建立的物件也可直接提供給外部的客戶端來呼叫,但工廠方法的本意是由Factory抽象父類內部的方法來使用工廠方法建立的物件。

以下這幅時序圖,即說明了客戶端呼叫factory的兩種方式。

其實客戶端應該使用Factory物件,或者是由Factory所建立出來的產品物件,對於客戶端使用Factory物件,這個時候工廠方法建立的物件,是Factory中的某些方法在用,對於使用那些由Factory建立出來的物件,這個時候工廠方法建立的物件,是構成客戶端所需物件的一部分。

我們基於上面的認識,重新來完成一開始那個檔案匯出樣例:

 

/**
 * 檔案匯出介面
 * 實現將指定資料的匯出
 * 擴充套件:實現該介面,可指定生產具體檔案型別
 * @author GCC
 */
public interface ExportFileApi {

    /**
     * 匯出檔案
     * @param data 待匯出資料
     * @return boolean
     */
    boolean exportFile(String data);

}
/**
 * 生產Excel檔案匯出器
 * @author GCC
 */
public class ExportExcelFile implements ExportFileApi {
    @Override
    public boolean exportFile(String data) {
        //todo 處理資料
        return false;
    }
}
/**
 * 匯出功能口,工廠
 * @author GCC
 */
public abstract class ExportFileOperate {

    public Logger logger = Logger.getLogger(ExportFileOperate.class);

    public void export(String data){
        ExportFileApi exportoper = methodFactory();
        if(exportoper.exportFile(data)){
            logger.info("檔案匯出成功");
            return;
        }
        logger.error("we檔案匯出失敗");

    }

    //工廠方法
    protected abstract ExportFileApi methodFactory();

}
/**
 * 將指定資料匯出為Excel檔案
 * @author GCC
 */
public class ExportExcelFileFactory extends ExportFileOperate {

    @Override
    protected ExportFileApi methodFactory() {
        return new ExportExcelFile();
    }
}
/**
 * 客戶用例
 */
public class App 
{
    static Logger logger = Logger.getLogger(App.class);

    public static void main( String[] args )
    {
        ExportFileOperate ex = new ExportExcelFileFactory();
        ex.export("測試資料");
    }
}

工廠方法與簡單工廠的區別

下面我們看一個例子,這裡我打算做個計算器,如果用簡單工廠模式來做,它的結構是這樣的:

為了工廠更完整,採用傳參的靜態工廠方式來實現,這樣我簡單工廠裡將通過Switch語句來管控生產哪一種計算類,這時候,突然來了新的需求,我需要一個乘法的功能,這時候我就得實現計算器介面,完成一個乘法類,同時去簡單工廠的程式碼裡,追加一個case。

同理,我使用工廠方法的模式來做這個功能,這塊的類圖則如上圖一樣,只不過我的工廠程式碼裡不需要Switch了,同樣的,我需要一個乘法的計算能力,實現計算器介面,完成乘法類,然後實現工廠的父類,實現一個乘法的工廠子類,然後再用乘法的工廠子類來建立乘法類。然後再去修改客戶端。

上面一對比,嘿,這升級版的工廠方法怎麼比簡單工廠還複雜了!?肯定很多同學在看工廠設計模式的時候很困惑,簡單工廠和工廠方法的區別在哪,明明感覺用簡單工廠更方便呢?

其實回頭好好看看設計原則,就會發現,這是一個解耦的過程,簡單工廠模式最大的優點在於工廠類中包含了必要的邏輯判斷,根據客戶的選擇動態的例項化相關的產品類,對於客戶端來說,除去了與具體產品物件的依賴。但問題就是隨著你的新需求,如果使用簡單工廠,那麼你就不得不去破壞開閉原則,而看起來改動更為複雜的工廠方法模式,你並不需要對以前的程式碼進行改動,只需要繼承,擴充套件即可。仔細觀察一下,簡單工廠是讓客戶端與依賴物件進行解耦,而工廠方式模式又是對工廠的一層解耦,原本內部耦合性較強的if else,變成了由客戶端或者配置檔案來控制,工廠方法將簡單工廠內部的邏輯判斷移到了使用它的外部(客戶端或者配置檔案)來控制。本來擴充套件是需要修改工廠類原始碼的,現在變成了客戶端修改呼叫或者配置檔案中的一個引數。

工廠方法模式的意義

工廠方法模式的主要思想是讓父類在不知情的情況下,完成自身功能的呼叫,而具體的實現則延遲到子類來做;或者說是在靜態工廠中,將其原本耦合的if else抽離出來,配合配置文件使用,將寫死的if else靈活實現(配置檔案並不是預設必須要有的)。這樣在設計的時候,不用去考慮具體的實現,需要某個物件,把它通過工廠方法返回就好,在使用這些物件實現功能的時候還是通過介面來操作,這裡就有一點IOC的韻味了。

工廠方法模式與IOC、DI

什麼是IOC/DI?

想想之前沒有學習設計模式,剛學會使用Java就被Spring的bean配置檔案支配的恐懼~

那時候你說自己學Java,對方一定會問你Spring,說到Spring,肯定避不開“什麼是IOC,什麼是DI?”這個讓人頭痛的問題,那麼,到底什麼是IOC,DI?

看完工廠的設計思想,對這個問題才開始了真正的思考。

IOC——控制反轉

DI——依賴注入

除了上面脫口而出的回答,我想我們這些物件導向的程式猿們,應該有個更深入的理解,到底什麼是控制反轉,什麼是依賴注入。要想理解上面兩個概念,必須把問題拆開來看,先搞清楚基本的問題幾個問題:

主客體是誰,或者說參與這個概念的都有誰?

什麼叫依賴?為什麼會有依賴?

什麼叫注入?注入的是什麼?誰注入誰?

控制反轉,誰控制誰,控制的是什麼,既然叫反轉,正轉是啥?

下面我們一個個來解決問題:

1、參與者,說起參與者,一般我們在這個概念中是有三個參與者,具體某個類,容器,某個物件所依賴的外部資源(另一個物件),就好比我有三個類,A,B,C,A物件我們把它想象成一個客戶端,B是它需要的一個外部的資源,C是一個叫容器的第三方。

2、什麼叫依賴,這個就比較好說了,你有一個A類,但是你A類的成員變數有一個是B類的例項宣告,那麼A就依賴於B,也就是說,A如果想正常運轉(或者說功能正常),必須得依賴於它的成員變數B,至於為什麼會有依賴,那也好理解了,物件導向就是將功能封裝,每個物件都功能單一,這樣有些複雜的物件需要實現複雜的功能,就必須需要其他類的協同。

3、注入,就是說,A你的成員變數B只是宣告瞭一個變數,它對於物件A來說,只是一個引用,一個字元,本身並沒有實體,你可以new 一下這個變數的建構函式,才能使這個變數真正有靈魂,又或者用它來承接外部傳進來的同類實體,這裡外部傳進來B的方式就叫注入,注入的是這個變數型別 具體的例項化物件。誰來注入,當然是容器C來注入給A,將B注入給A

4、簡單來說就是容器來控制A,控制的就是A所依賴的物件B例項的建立,反轉是與正轉對應來說的,什麼是正轉呢,A類中有個物件B的成員變數,正常情況下,A類中功能用到B物件的時候,A要主動去獲取一個B物件,例如new一下,這種情況被稱為正向的。這樣就比較好理解反轉了,A不再去主動獲取B物件了,而是被動的等待B的到來(注入),等待容器C獲取一個B的例項,然後反向的注入進A。

 

 

所以,綜上來看,控制反轉和依賴注入其實說的是同一件事,說白了就是物件建立這個責任歸誰的問題,依賴注入是從應用程式的角度去描述,應用程式依賴外部容器去建立並注入它所需要的外部資源物件。控制反轉是從容器的角度出發,容器控制應用程式,由容器反向地嚮應用程式注入其所需要的外部物件

 

其實IOC/DI並不是一種程式碼實現,更多的它是一種思想,它從思想上完成了 “主從換位” 的變化,應用程式本來是主體,佔絕對地位,它需要什麼都會主動出擊去獲取,過強的控制慾導致了它耦合過重,而在IOC/DI思想中,應用程式變成被動的等待容器的注入,需要啥只能提出來,什麼時候給,給什麼樣子的,主動權完全交給了容器,較強的實現瞭解耦,程式的靈活性也就高了

 

 工廠方法與IOC/DI思想

從某個角度來看,工廠方法模式跟IOC/DI的思想很貼近。

上面也說過了,IOC/DI就是讓應用程式不再主動獲取外部資源,而是被動等待第三方的注入,那麼在編寫程式的時候,一旦遇到需要外部資源的地方,就會開一個視窗,提供給容器一個注入的途徑,讓容器注入進來,細節這裡就不過多贅述了,自己去找Spring聊吧。  下面用IOC/DI和工廠方法來實現一個樣例對比一下。

用IOC/DI來實現一個類Person:

public class Person {

    private String name;
    //依賴檔案操作物件
    private FileUtil fileUtil;

    private int age;

    //提供set方法,供外部注入
    public void setFileUtil(FileUtil fileUtil){
        this.fileUtil = fileUtil;
    }
    
    public void opFile(String fileurl){
        fileUtil.createFile(fileurl);
    }
    

}

這就是IOC/DI思想來實現的一個類,我依賴FileUtil,沒事,我不管,我提供給你一個set的注入途徑,剩下的我不管了,我就預設我用FileUtil的時候,它是真真切切存在在堆中的物件。

 

下面用廠方法來搞上面的例子:

public abstract class Person {

    private String name;

    private int age;

    //交給子類去實現我的依賴
    public abstract FileUtil getFileutil();

    public void opFile(String fileurl){
        getFileutil().createFile(fileurl);
    }


}

 

仔細體會這兩種寫法,對比他們的實現,在思想層面來看,會發現,工廠方法模式和IOC/DI的思想是相似的,都是“主動變被動”,“主位換從位”,從而獲得了更加靈活的程式結構。

 

 

 

設計模式彙總目錄

 

相關文章