定義一個用於創建對象的接口,讓子類決定實例化哪一個類。Factory Method使一個類的實例化延遲到其子類。
工廠方法即Factory Method,是一種對象創建型模式。
工廠方法的目的是使得創建對象和使用對象是分離的,並且客戶端總是引用抽象工廠和抽象產品:
┌─────────────┐ ┌─────────────┐
│ Product │ │ Factory │
└─────────────┘ └─────────────┘
▲ ▲
│ │
┌─────────────┐ ┌─────────────┐
│ ProductImpl │<─ ─ ─│ FactoryImpl │
└─────────────┘ └─────────────┘
我們以具體的例子來說:假設我們希望實現一個解析字符串到Number
的Factory
,可以定義如下:
public interface Factory {
Number parse(String s);
}
有了工廠接口,再編寫一個工廠的實現類:
public class NumberFactoryImpl implements NumberFactory {
public Number parse(String s) {
return new BigDecimal(s);
}
}
而產品接口是Number
,NumberFactoryImpl
返回的實際產品是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);
}
注意到上面的抽象工廠僅僅是一個接口,沒有任何代碼。同樣的,因爲HtmlDocument
和WordDocument
都比較複雜,現在我們並不知道如何實現它們,所以只有接口:
// Html文檔接口:
public interface HtmlDocument {
String toHtml();
void save(Path path) throws IOException;
}
// Word文檔接口:
public interface WordDocument {
void save(Path path) throws IOException;
}
這樣,我們就定義好了抽象工廠(AbstractFactory
)以及兩個抽象產品(HtmlDocument
和WordDocument
)。因爲實現它們比較困難,我們決定讓供應商來完成。
現在市場上有兩家供應商:FastDoc Soft的產品便宜,並且轉換速度快,而GoodDoc Soft的產品貴,但轉換效果好。我們決定同時使用這兩家供應商的產品,以便給免費用戶和付費用戶提供不同的服務。
我們先看看FastDoc Soft的產品是如何實現的。首先,FastDoc Soft必須要有實際的產品,即FastHtmlDocument
和FastWordDocument
:
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
創建了FastFactory
或GoodFactory
外,其餘代碼只引用了產品接口,並未引用任何實際產品(例如,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");
}
}
}
我們來看看FastFactory
和GoodFactory
創建的WordDocument
的實際效果:
注意:出於簡化代碼的目的,我們只支持兩種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() {
}
}
所以,單例模式的實現方式很簡單:
- 只有
private
構造方法,確保外部無法實例化; - 通過
private static
變量持有唯一實例,保證全局唯一性; - 通過
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模式一般以“約定”爲主,不會刻意實現它。