1. 程式人生 > >Java 設計模式(一):簡單工廠模式

Java 設計模式(一):簡單工廠模式

參考連結:簡單工廠模式-Simple Factory Pattern

1. 模式概述

定義:定義一個工廠類,它可以根據引數的不同返回不同類的例項,被建立的例項通常都具有共同的父類。因為在簡單工廠模式中用於建立例項的方法是靜態方法,因此簡單工廠模式又被稱為靜態工廠方法模式。

簡單工廠模式並不屬於GoF 23個經典設計模式,但通常將它作為學習其他工廠模式的基礎,它的設計思想很簡單,其基本流程如下:首先將需要建立的各種不同物件(例如各種不同的Shape物件)的相關程式碼封裝到不同的類中,這些類稱為具體產品類,而將它們公共的程式碼進行抽象和提取後封裝在一個抽象產品類中,每一個具體產品類都是抽象產品類的子類;然後提供一個工廠類用於建立各種產品,在工廠類中提供一個建立產品的工廠方法,該方法可以根據所傳入的引數不同建立不同的具體產品物件;客戶端只需呼叫工廠類的工廠方法並傳入相應的引數即可得到一個產品物件。

簡單工廠模式結構比較簡單,其核心是工廠類的設計,其結構如圖所示:
在這裡插入圖片描述

在簡單工廠模式中包含如下幾個角色:

  • Factory(工廠類):負責實現建立所有產品例項的內部邏輯,在工廠類中提供靜態的工廠方法,返回抽象產品。
  • Product(抽象產品):所有具體產品的父類,封裝各種產品物件的共有方法。
  • ConcreteProduct(具體產品類):工廠類的建立目標。

在簡單工廠模式中,客戶端通過工廠類來建立一個產品類的例項,而無須直接使用new關鍵字來建立物件。

在使用簡單工廠模式時,首先需要對產品類進行重構,根據實際情況設計一個產品層次結構,將所有產品類公共的程式碼移至抽象產品類,並在抽象產品類中宣告一些抽象方法,以供不同的具體產品類來實現,典型的抽象產品類程式碼如下所示:

public abstract class Shape {
    // 公共方法
    public void show() {
        
    }
    // 抽象方法
    public abstract void draw();
}

具體產品類繼承抽象產品類並實現抽象方法,不同的具體產品類可以提供不同的實現,典型的具體產品類程式碼如下所示:

public class Circle extends Shape {

    @Override
    public void draw() {
        System.out.println("Inside Circle::draw() method."
); } }
public class Rectangle extends Shape {

    @Override
    public void draw() {
        System.out.println("Inside Rectangle::draw() method.");
    }
}
public class Square extends Shape {

    @Override
    public void draw() {
        System.out.println("Inside Square::draw() method.");
    }
}

簡單工廠模式的核心工廠類負責建立產品,提供一個靜態工廠方法給客戶端使用,典型的工廠類程式碼如下所示:

public class ShapeFactory {

    public static Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            return new Rectangle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        }
        return null;
    }
}

在客戶端程式碼中,我們通過傳入不同的引數給工廠方法來建立對應的產品,典型的程式碼如下所示:

public class Client {

    public static void main(String[] args) {
        Shape rectangle = ShapeFactory.getShape("Rectangle");
        rectangle.draw();

        Shape circle = ShapeFactory.getShape("Circle");
        circle.draw();

        Shape square = ShapeFactory.getShape("Square");
        square.draw();
    }
}

2. 方案改進

當我們更換一個Shape物件都需要修改客戶端程式碼中靜態工廠方法的引數,客戶端程式碼將要重新編譯,這對於客戶端而言,違反了“開閉原則”,有沒有一種方法能夠在不修改客戶端程式碼的前提下更換具體產品物件呢?

我們可以將靜態工廠方法的引數儲存在XML或properties格式的配置檔案中,如下config.xml所示:

<?xml version="1.0"?>
<config>
  <chartType>square</chartType>
</config>

在通過一個工具類XMLUtil來讀取配置檔案中的字串引數,XMLUtil類的程式碼如下所示:

public class XMLUtil {

    public static String getChartType() {
        try {
            // 建立文件物件
            DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = dFactory.newDocumentBuilder();
            Document doc;
            // config.xml的路徑
            doc = builder.parse(new File("config.xml"));
            // 獲取包含圖表型別的文字節點
            NodeList nl = doc.getElementsByTagName("shapeType");
            Node classNode = nl.item(0).getFirstChild();
            String shapeType = classNode.getNodeValue().trim();
            return shapeType;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

客戶端通過讀取配置檔案中的shapeType建立對應的產品,客戶端修改程式碼如下:

public class Client {

    public static void main(String[] args) {
        String shapeType = XMLUtil.getChartType();
        Shape shape =  ShapeFactory.getShape(shapeType);
        shape.draw();
    }
}

優化之後,如果需要更換具體圖表物件,只需修改配置檔案config.xml,無須修改任何原始碼,符合“開閉原則”。

還有另外一種情況,當系統中需要引入新產品時,比如引入新產品Triangle,由於靜態工廠方法通過所傳入引數的不同來建立不同的產品,那麼勢必要在工廠類中新增一個if…else判斷來返回Triangle例項,違背了“開閉原則”。

在工廠類中新增if…else判斷的程式碼如下:

public class ShapeFactory {

    public static Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            return new Rectangle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        } else if (shapeType.equalsIgnoreCase("TRIANGLE")) {
            return new Triangle();
        }
        return null;
    }
}

一旦產品型別較多時,工廠類會變得不易維護。

我們可以通過反射機制解加增加新產品時需要改動靜態工廠方法的缺點,修改靜態工廠方法程式碼如下:

public static <T> T getClass(Class<? extends T> clazz) {
    T obj = null;
    try {
        obj = (T) Class.forName(clazz.getName()).newInstance();
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    return obj;
}

對應的客戶端通過傳入具體的產品類建立不同的產品,客戶端程式碼修改如下:

public class Client {

    public static void main(String[] args) {
        String shape = ShapeFactory.getClass(Circle.class);
        shape.draw();
    }
}

反射機制可以結合配置檔案來更換具體的產品物件,而不需要改動客戶端程式碼。

3. 模式總結

簡單工廠模式提供專門的工廠類用於建立物件,將物件的建立和物件的使用分離開。

  1. 主要優點:
    ① 工廠類包含必要的判斷邏輯,可以決定在什麼時候建立哪一個產品類的例項,客戶端可以免除直接建立產品物件的職責,而僅僅“消費”產品,簡單工廠模式實現了物件建立和使用的分離。
    ② 客戶端無須知道所建立的具體產品類的類名,只需要知道具體產品類所對應的引數即可,對於一些複雜的類名,通過簡單工廠模式可以在一定程度減少使用者的記憶量。
    ③ 通過引入配置檔案,可以在不修改任何客戶端程式碼的情況下更換和增加新的具體產品類,在一定程度上提高了系統的靈活性。
  2. 主要缺點:
    ① 由於工廠類集中了所有產品的建立邏輯,如果工廠類不能工作,整個系統都要受到影響。
    ② 當系統中需要引入新產品時,由於靜態工廠方法通過所傳入引數的不同來建立不同的產品,這必定要修改工廠類的原始碼(反射方式除外)。
    ③ 在產品型別較多時,有可能造成工廠邏輯過於複雜,不利於系統的擴充套件和維護。
  3. 使用場景:
    ① 工廠類負責建立的物件比較少時,不會造成工廠方法中的業務邏輯太過複雜,這個時候可以考慮使用簡單工廠。
    ② 客戶端對於如何建立物件並不關心。