項目管理必備及MyBatis入門---工廠模式

定義一個用於創建對象的接口,讓子類決定實例化哪一個類。Factory Method使一個類的實例化延遲到其子類。

工廠方法即Factory Method,是一種對象創建型模式。

工廠方法的目的是使得創建對象和使用對象是分離的,並且客戶端總是引用抽象工廠和抽象產品:

┌─────────────┐      ┌─────────────┐
│   Product   │      │   Factory   │
└─────────────┘      └─────────────┘
       ▲                    ▲
       │                    │
┌─────────────┐      ┌─────────────┐
│ ProductImpl │<─ ─ ─│ FactoryImpl │
└─────────────┘      └─────────────┘

我們以具體的例子來說:假設我們希望實現一個解析字符串到NumberFactory,可以定義如下:

public interface Factory {
    Number parse(String s);
}

有了工廠接口,再編寫一個工廠的實現類:

public class NumberFactoryImpl implements NumberFactory {
    public Number parse(String s) {
        return new BigDecimal(s);
    }
}

而產品接口是NumberNumberFactoryImpl返回的實際產品是BigDecimal

那麼客戶端如何創建NumberFactoryImpl呢?通常我們會在接口Factory中定義一個靜態方法getFactory()來返回真正的子類:

public interface NumberFactory {
    // 創建方法:
    Number parse(String s);

    // 獲取工廠實例:
    static NumberFactory getFactory() {
        return impl;
    }

    static NumberFactory impl = new NumberFactoryImpl();
}

在客戶端中,我們只需要和工廠接口NumberFactory以及抽象產品Number打交道:

NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");

調用方可以完全忽略真正的工廠NumberFactoryImpl和實際的產品BigDecimal,這樣做的好處是允許創建產品的代碼獨立地變換,而不會影響到調用方。

有的童鞋會問:一個簡單的parse()需要寫這麼複雜的工廠嗎?實際上大多數情況下我們並不需要抽象工廠,而是通過靜態方法直接返回產品,即:

public class NumberFactory {
    public static Number parse(String s) {
        return new BigDecimal(s);
    }
}

這種簡化的使用靜態方法創建產品的方式稱爲靜態工廠方法(Static Factory Method)。靜態工廠方法廣泛地應用在Java標準庫中。例如:

Integer n = Integer.valueOf(100);

Integer既是產品又是靜態工廠。它提供了靜態方法valueOf()來創建Integer。那麼這種方式和直接寫new Integer(100)有何區別呢?我們觀察valueOf()方法:

public final class Integer {
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    ...
}

它的好處在於,valueOf()內部可能會使用new創建一個新的Integer實例,但也可能直接返回一個緩存的Integer實例。對於調用方來說,沒必要知道Integer創建的細節。

 工廠方法可以隱藏創建產品的細節,且不一定每次都會真正創建產品,完全可以返回緩存的產品,從而提升速度並減少內存消耗。

如果調用方直接使用Integer n = new Integer(100),那麼就失去了使用緩存優化的可能性。

我們經常使用的另一個靜態工廠方法是List.of()

List<String> list = List.of("A", "B", "C");

這個靜態工廠方法接收可變參數,然後返回List接口。需要注意的是,調用方獲取的產品總是List接口,而且並不關心它的實際類型。即使調用方知道List產品的實際類型是java.util.ImmutableCollections$ListN,也不要去強制轉型爲子類,因爲靜態工廠方法List.of()保證返回List,但也完全可以修改爲返回java.util.ArrayList。這就是里氏替換原則:返回實現接口的任意子類都可以滿足該方法的要求,且不影響調用方。

 總是引用接口而非實現類,能允許變換子類而不影響調用方,即儘可能面向抽象編程。

List.of()類似,我們使用MessageDigest時,爲了創建某個摘要算法,總是使用靜態工廠方法getInstance(String)

MessageDigest md5 = MessageDigest.getInstance("MD5");
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");

調用方通過產品名稱獲得產品實例,不但調用簡單,而且獲得的引用仍然是MessageDigest這個抽象類。

抽象工廠

提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的類。

抽象工廠模式(Abstract Factory)是一個比較複雜的創建型模式。

抽象工廠模式和工廠方法不太一樣,它要解決的問題比較複雜,不但工廠是抽象的,產品是抽象的,而且有多個產品需要創建,因此,這個抽象工廠會對應到多個實際工廠,每個實際工廠負責創建多個實際產品:

                                ┌────────┐
                             ─ >│ProductA│
┌────────┐    ┌─────────┐   │   └────────┘
│ Client │─ ─>│ Factory │─ ─
└────────┘    └─────────┘   │   ┌────────┐
                   ▲         ─ >│ProductB│
           ┌───────┴───────┐    └────────┘
           │               │
      ┌─────────┐     ┌─────────┐
      │Factory1 │     │Factory2 │
      └─────────┘     └─────────┘
           │   ┌─────────┐ │   ┌─────────┐
            ─ >│ProductA1│  ─ >│ProductA2│
           │   └─────────┘ │   └─────────┘
               ┌─────────┐     ┌─────────┐
           └ ─>│ProductB1│ └ ─>│ProductB2│
               └─────────┘     └─────────┘

這種模式有點類似於多個供應商負責提供一系列類型的產品。我們舉個例子:

假設我們希望爲用戶提供一個Markdown文本轉換爲HTML和Word的服務,它的接口定義如下:

public interface AbstractFactory {
    // 創建Html文檔:
    HtmlDocument createHtml(String md);
    // 創建Word文檔:
    WordDocument createWord(String md);
}

注意到上面的抽象工廠僅僅是一個接口,沒有任何代碼。同樣的,因爲HtmlDocumentWordDocument都比較複雜,現在我們並不知道如何實現它們,所以只有接口:

// Html文檔接口:
public interface HtmlDocument {
    String toHtml();
    void save(Path path) throws IOException;
}

// Word文檔接口:
public interface WordDocument {
    void save(Path path) throws IOException;
}

這樣,我們就定義好了抽象工廠(AbstractFactory)以及兩個抽象產品(HtmlDocumentWordDocument)。因爲實現它們比較困難,我們決定讓供應商來完成。

現在市場上有兩家供應商:FastDoc Soft的產品便宜,並且轉換速度快,而GoodDoc Soft的產品貴,但轉換效果好。我們決定同時使用這兩家供應商的產品,以便給免費用戶和付費用戶提供不同的服務。

我們先看看FastDoc Soft的產品是如何實現的。首先,FastDoc Soft必須要有實際的產品,即FastHtmlDocumentFastWordDocument

public class FastHtmlDocument implements HtmlDocument {
    public String toHtml() {
        ...
    }
    public void save(Path path) throws IOException {
        ...
    }
}

public class FastWordDocument implements WordDocument {
    public void save(Path path) throws IOException {
        ...
    }
}

然後,FastDoc Soft必須提供一個實際的工廠來生產這兩種產品,即FastFactory

public class FastFactory implements AbstractFactory {
    public HtmlDocument createHtml(String md) {
        return new FastHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new FastWordDocument(md);
    }
}

這樣,我們就可以使用FastDoc Soft的服務了。客戶端編寫代碼如下:

// 創建AbstractFactory,實際類型是FastFactory:
AbstractFactory factory = new FastFactory();
// 生成Html文檔:
HtmlDocument html = factory.createHtml("#Hello\nHello, world!");
html.save(Paths.get(".", "fast.html"));
// 生成Word文檔:
WordDocument word = fastFactory.createWord("#Hello\nHello, world!");
word.save(Paths.get(".", "fast.doc"));

如果我們要同時使用GoodDoc Soft的服務怎麼辦?因爲用了抽象工廠模式,GoodDoc Soft只需要根據我們定義的抽象工廠和抽象產品接口,實現自己的實際工廠和實際產品即可:

// 實際工廠:
public class GoodFactory implements AbstractFactory {
    public HtmlDocument createHtml(String md) {
        return new GoodHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new GoodWordDocument(md);
    }
}

// 實際產品:
public class GoodHtmlDocument implements HtmlDocument {
    ...
}

public class GoodWordDocument implements HtmlDocument {
    ...
}

客戶端要使用GoodDoc Soft的服務,只需要把原來的new FastFactory()切換爲new GoodFactory()即可。

注意到客戶端代碼除了通過new創建了FastFactoryGoodFactory外,其餘代碼只引用了產品接口,並未引用任何實際產品(例如,FastHtmlDocument),如果把創建工廠的代碼放到AbstractFactory中,就可以連實際工廠也屏蔽了:

public interface AbstractFactory {
    public static AbstractFactory createFactory(String name) {
        if (name.equalsIgnoreCase("fast")) {
            return new FastFactory();
        } else if (name.equalsIgnoreCase("good")) {
            return new GoodFactory();
        } else {
            throw new IllegalArgumentException("Invalid factory name");
        }
    }
}

我們來看看FastFactoryGoodFactory創建的WordDocument的實際效果:

worddoc

注意:出於簡化代碼的目的,我們只支持兩種Markdown語法:以#開頭的標題以及普通正文。

生成器

將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。

生成器模式(Builder)是使用多個“小型”工廠來最終創建出一個完整對象。

當我們使用Builder的時候,一般來說,是因爲創建這個對象的步驟比較多,每個步驟都需要一個零部件,最終組合成一個完整的對象。

我們仍然以Markdown轉HTML爲例,因爲直接編寫一個完整的轉換器比較困難,但如果針對類似下面的一行文本:

# this is a heading

轉換成HTML就很簡單:

<h1>this is a heading</h1>

因此,我們把Markdown轉HTML看作一行一行的轉換,每一行根據語法,使用不同的轉換器:

  • 如果以#開頭,使用HeadingBuilder轉換;
  • 如果以>開頭,使用QuoteBuilder轉換;
  • 如果以---開頭,使用HrBuilder轉換;
  • 其餘使用ParagraphBuilder轉換。

這個HtmlBuilder寫出來如下:

public class HtmlBuilder {
    private HeadingBuilder headingBuilder = new HeadingBuilder();
    private HrBuilder hrBuilder = new HrBuilder();
    private ParagraphBuilder paragraphBuilder = new ParagraphBuilder();
    private QuoteBuilder quoteBuilder = new QuoteBuilder();

    public String toHtml(String markdown) {
        StringBuilder buffer = new StringBuilder();
        markdown.lines().forEach(line -> {
            if (line.startsWith("#")) {
                buffer.append(headingBuilder.buildHeading(line)).append('\n');
            } else if (line.startsWith(">")) {
                buffer.append(quoteBuilder.buildQuote(line)).append('\n');
            } else if (line.startsWith("---")) {
                buffer.append(hrBuilder.buildHr(line)).append('\n');
            } else {
                buffer.append(paragraphBuilder.buildParagraph(line)).append('\n');
            }
        });
        return buffer.toString();
    }
}

注意觀察上述代碼,HtmlBuilder並不是一次性把整個Markdown轉換爲HTML,而是一行一行轉換,並且,它自己並不會將某一行轉換爲特定的HTML,而是根據特性把每一行都“委託”給一個XxxBuilder去轉換,最後,把所有轉換的結果組合起來,返回給客戶端。

這樣一來,我們只需要針對每一種類型編寫不同的Builder。例如,針對以#開頭的行,需要HeadingBuilder

public class HeadingBuilder {
    public String buildHeading(String line) {
        int n = 0;
        while (line.charAt(0) == '#') {
            n++;
            line = line.substring(1);
        }
        return String.format("<h%d>%s</h%d>", n, line.strip(), n);
    }
}

 注意:實際解析Markdown是帶有狀態的,即下一行的語義可能與上一行相關。這裏我們簡化了語法,把每一行視爲可以獨立轉換。

可見,使用Builder模式時,適用於創建的對象比較複雜,最好一步一步創建出“零件”,最後再裝配起來。

JavaMail的MimeMessage就可以看作是一個Builder模式,只不過Builder和最終產品合二爲一,都是MimeMessage

Multipart multipart = new MimeMultipart();
// 添加text:
BodyPart textpart = new MimeBodyPart();
textpart.setContent(body, "text/html;charset=utf-8");
multipart.addBodyPart(textpart);
// 添加image:
BodyPart imagepart = new MimeBodyPart();
imagepart.setFileName(fileName);
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "application/octet-stream")));
multipart.addBodyPart(imagepart);

MimeMessage message = new MimeMessage(session);
// 設置發送方地址:
message.setFrom(new InternetAddress("[email protected]"));
// 設置接收方地址:
message.setRecipient(Message.RecipientType.TO, new InternetAddress("[email protected]"));
// 設置郵件主題:
message.setSubject("Hello", "UTF-8");
// 設置郵件內容爲multipart:
message.setContent(multipart);

很多時候,我們可以簡化Builder模式,以鏈式調用的方式來創建對象。例如,我們經常編寫這樣的代碼:

StringBuilder builder = new StringBuilder();
builder.append(secure ? "https://" : "http://")
       .append("www.liaoxuefeng.com")
       .append("/")
       .append("?t=0");
String url = builder.toString();

由於我們經常需要構造URL字符串,可以使用Builder模式編寫一個URLBuilder,調用方式如下:

String url = URLBuilder.builder() // 創建Builder
        .setDomain("www.liaoxuefeng.com") // 設置domain
        .setScheme("https") // 設置scheme
        .setPath("/") // 設置路徑
        .setQuery(Map.of("a", "123", "q", "K&R")) // 設置query
        .build(); // 完成build

原型

用原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象。

原型模式,即Prototype,是指創建新對象的時候,根據現有的一個原型來創建。

我們舉個例子:如果我們已經有了一個String[]數組,想再創建一個一模一樣的String[]數組,怎麼寫?

實際上創建過程很簡單,就是把現有數組的元素複製到新數組。如果我們把這個創建過程封裝一下,就成了原型模式。用代碼實現如下:

// 原型:
String[] original = { "Apple", "Pear", "Banana" };
// 新對象:
String[] copy = Arrays.copyOf(original, original.length);

對於普通類,我們如何實現原型拷貝?Java的Object提供了一個clone()方法,它的意圖就是複製一個新的對象出來,我們需要實現一個Cloneable接口來標識一個對象是“可複製”的:

public class Student implements Cloneable {
    private int id;
    private String name;
    private int score;

    // 複製新對象並返回:
    public Object clone() {
        Student std = new Student();
        std.id = this.id;
        std.name = this.name;
        std.score = this.score;
        return std;
    }
}

使用的時候,因爲clone()的方法簽名是定義在Object中,返回類型也是Object,所以要強制轉型,比較麻煩:

Student std1 = new Student();
std1.setId(123);
std1.setName("Bob");
std1.setScore(88);
// 複製新對象:
Student std2 = (Student) std1.clone();
System.out.println(std1);
System.out.println(std2);
System.out.println(std1 == std2); // false

實際上,使用原型模式更好的方式是定義一個copy()方法,返回明確的類型:

public class Student {
    private int id;
    private String name;
    private int score;

    public Student copy() {
        Student std = new Student();
        std.id = this.id;
        std.name = this.name;
        std.score = this.score;
        return std;
    }
}

原型模式應用不是很廣泛,因爲很多實例會持有類似文件、Socket這樣的資源,而這些資源是無法複製給另一個對象共享的,只有存儲簡單類型的“值”對象可以複製。

單例

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

單例模式(Singleton)的目的是爲了保證在一個進程中,某個類有且僅有一個實例。

因爲這個類只有一個實例,因此,自然不能讓調用方使用new Xyz()來創建實例了。所以,單例的構造方法必須是private,這樣就防止了調用方自己創建實例,但是在類的內部,是可以用一個靜態字段來引用唯一創建的實例的:

public class Singleton {
    // 靜態字段引用唯一實例:
    private static final Singleton INSTANCE = new Singleton();

    // private構造方法保證外部無法實例化:
    private Singleton() {
    }
}

那麼問題來了,外部調用方如何獲得這個唯一實例?

答案是提供一個靜態方法,直接返回實例:

public class Singleton {
    // 靜態字段引用唯一實例:
    private static final Singleton INSTANCE = new Singleton();

    // 通過靜態方法返回實例:
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // private構造方法保證外部無法實例化:
    private Singleton() {
    }
}

或者直接把static變量暴露給外部:

public class Singleton {
    // 靜態字段引用唯一實例:
    public static final Singleton INSTANCE = new Singleton();

    // private構造方法保證外部無法實例化:
    private Singleton() {
    }
}

所以,單例模式的實現方式很簡單:

  1. 只有private構造方法,確保外部無法實例化;
  2. 通過private static變量持有唯一實例,保證全局唯一性;
  3. 通過public static方法返回此唯一實例,使外部調用方能獲取到實例。

Java標準庫有一些類就是單例,例如Runtime這個類:

Runtime runtime = Runtime.getRuntime();

有些童鞋可能聽說過延遲加載,即在調用方第一次調用getInstance()時才初始化全局唯一實例,類似這樣:

public class Singleton {
    private static Singleton INSTANCE = null;

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

    private Singleton() {
    }
}

遺憾的是,這種寫法在多線程中是錯誤的,在競爭條件下會創建出多個實例。必須對整個方法進行加鎖:

public synchronized static Singleton getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

但加鎖會嚴重影響併發性能。還有些童鞋聽說過雙重檢查,類似這樣:

public static Singleton getInstance() {
    if (INSTANCE == null) {
        synchronized (Singleton.class) {
            if (INSTANCE == null) {
                INSTANCE = new Singleton();
            }
        }
    }
    return INSTANCE;
}

然而,由於Java的內存模型,雙重檢查在這裏不成立。要真正實現延遲加載,只能通過Java的ClassLoader機制完成。如果沒有特殊的需求,使用Singleton模式的時候,最好不要延遲加載,這樣會使代碼更簡單。

另一種實現Singleton的方式是利用Java的enum,因爲Java保證枚舉類的每個枚舉都是單例,所以我們只需要編寫一個只有一個枚舉的類即可:

public enum World {
    // 唯一枚舉:
	INSTANCE;

	private String name = "world";

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

枚舉類也完全可以像其他類那樣定義自己的字段、方法,這樣上面這個World類在調用方看來就可以這麼用:

String name = World.INSTANCE.getName();

使用枚舉實現Singleton還避免了第一種方式實現Singleton的一個潛在問題:即序列化和反序列化會繞過普通類的private構造方法從而創建出多個實例,而枚舉類就沒有這個問題。

那我們什麼時候應該用Singleton呢?實際上,很多程序,尤其是Web程序,大部分服務類都應該被視作Singleton,如果全部按Singleton的寫法寫,會非常麻煩,所以,通常是通過約定讓框架(例如Spring)來實例化這些類,保證只有一個實例,調用方自覺通過框架獲取實例而不是new操作符:

@Component // 表示一個單例組件
public class MyService {
    ...
}

因此,除非確有必要,否則Singleton模式一般以“約定”爲主,不會刻意實現它。

 

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