從封裝變化的角度看設計模式——物件建立
阿新 • • 發佈:2020-07-10
## 封裝變化之物件建立
在物件建立的過程中,經常會出現的一個問題就是通過顯示地指定一個類來建立物件,從而導致緊耦合。這是因為建立物件時指定類名將使你受特定實現的約束而不是特定介面的約束。這會使未來的變化更加複雜。要避免這種情況,就應該間接地建立物件。
這種緊耦合的問題很大程度是由new關鍵字帶來的,由於new的緊耦合出現,使得緊耦合的類很難獨立地被複用,因為它們之間是相互依賴的。並且緊耦合產生單塊的系統,要改變或者刪掉一個類,就必須要理解和改變其他許多類。這也是導致系統難以維護和移植的一個重要原因。
所以可以通過“物件建立”模式繞開new,從而避免在物件建立(new)過程中所導致的緊耦合(依賴具體的類),以此支援物件建立的穩定。
那麼如何避免new呢?舉個例子!
```java
public void fun1(){
//...
Product p = new Product(); //改為:Product p = productFactory.createProduct();
//...
}
```
這樣的方式就是通過一個工廠呼叫一個方法來建立相應的產品,但是可能大家又會產生一個問題,這樣操作雖然解決了`Product`的new操作,但是對於`ProductFactory`而言不是也需要通過new來產生嗎?
對於這個問題,我想是很多人在接觸到設計模式的時候都會去思考的問題,既然`ProductFactory`還是要用到new,那工廠類還有存在的必要嗎?這時,我們可以會想到兩種解決方式,一是將`createProdyct()`方法寫成靜態方法,這樣呼叫的時候自然不需要new了。二是通過注入的方式,比如在應用類當中通過setter或是構造方法傳入一個工廠類的物件。
對於靜態方法而言,簡單地說,即使是使用靜態方法,那`Product p = ProductFactory.createProduct()`這樣依然是一種緊耦合的方式,因為工廠類無法替換,和直接new出產品區別不大。
對於注入方式,大家更多的是疑惑,既然可以傳入一個工廠類物件,那為什麼不直接傳入相應的產品,不是更簡單直接嗎?當然不是的,首先需要明白的是,工廠類的作用是作為一個籠子,這個籠子需要幫助我們束縛住 ‘未來的變化’ ,要知道一個產品的變化可能總是大於工廠的變化。在這種情況下,舉出一個最簡單的例子,你在編碼的過程中,可能會用到不只一個產品,那你就可能需要很多setter或者修改構造方法;但是如果這些產品都可以通過這個工廠來獲取,是不是就相當於用籠子關住了變化,使其在一個範圍中跳動。
在學習設計模式時,永遠要記住的一句話就是“設計模式是用來教會我們如何應對未來可能的變化”。如果你能夠確定自己的系統未來沒有變化,那自然用不到設計模式;或者你的系統未來全是變化,那也用不到設計模式,設計模式做的就是隔離穩定與變化,如果沒有穩定,那就用不到設計模式。
‘new’是一種硬編碼,究竟 ’硬‘ 在那裡,同樣一個簡單的理由,如果未來構造方法發生變化或者說構造引數增加(減少),而在原始碼中有很多地方都是通過new來獲取例項物件,找到並修改原始碼將會是一項很大的工作。
在解決這樣的 “物件建立” 問題中就有工廠方法、抽象工廠、原型模式和建造者模式等相關設計模式。
## 工廠方法(Factory Method)
1. **意圖**
定義一個用於建立物件的介面,讓子類決定例項化哪一個類。FactoryMethod使得一個類的例項化延遲到其子類。
2. **例項**
Factory Method相對於簡單工廠而言,完全遵循了“不改程式碼”的原則,但是其使用情形相比抽象工廠使用條件沒有那麼高,因此可以說是使用最多的建立型模式之一了。
考慮這樣一個應用,它可以向用戶顯示多種文件,比如word、pdf、txt等等。在這個框架中,首先,想到的可能就是應用簡單工廠模式。
```java
public interface Document{
public void open();
public void close();
public void save();
// ......
}
public class PdfDocument implements Document{
@Override
public void open(){
//open pdfDocument code
System.out.println("open pdf!");
}
@Override
public void close() {
System.out.println("close pdf!");
}
@Override
public void save() {
System.out.println("save pdf!");
}
// ......
}
public class TxtDocument implements Document{
//Txt實現程式碼,同PdfDocument
......
}
public class DocumentFactory{
public Document createDocument(String type){
if(type=="pdf"){
return new PdfDocument();
}else if(type=="txt"){
return new TxtDocument();
}else {
return null;
}
}
}
```
```java
//簡單工廠模式在客戶類當中的呼叫
public class Client {
public static void main(String[] args) {
DocumentFactory factory
= new DocumentFactory();
Document pdfDocument
= factory.createDocument("pdf");
pdfDocument.open();
pdfDocument.save();
pdfDocument.close();
Document txtDocument
= factory.createDocument("txt");
txtDocument.open();
txtDocument.save();
txtDocument.close();
}
}
```
這樣簡單工廠模式,在不考慮未來新文件型別的情況下,確實是一種不錯的實現方法。但是在後續的擴充套件過程當中,如果需要增加新的文件類,就需要去修改`DocumentFactory`中的`createDocument()`方法,增加新的類別,並且客戶還必須知道這些類別才能使用。
為了應對這種情況,就出現了工廠方法。工廠方法就直接將工廠抽象出來,每個產品對應一個工廠,消除工廠模式中的條件分支結構(其實還有一種消除條件語句的模式,就是之前“元件協作”當中的策略模式)。
```java
//Document部分不變
public interface Document{
public void open();
public void close();
public void save();
......
}
public class PdfDocument implements Document{
public void open(){
//open pdfDocument code
}
// close 和 save
......
}
public class TxtDocument implements Document{
//Txt實現程式碼
......
}
//並且後續可以擴充套件新的文件類
......
```
```java
//修改factory部分如下
public interface DocumentFactory{
public Document createDocument();
}
public class PdfDocumentFactory
implements DocumentFactory {
@Override
public Document createDocument() {
return new PdfDocument();
}
}
public class TxtDocumentFactory
implements DocumentFactory {
@Override
public Document createDocument() {
return new TxtDocument();
}
}
//如果後續有新的產品,直接再實現DocumentFactory,得到新的工廠
......
```
```java
//呼叫過程可做如下修改:
public class Client {
public static void main(String[] args) {
//利用多型性質,直接生成相應的factory子類
//消除了控制耦合
DocumentFactory factory = new PdfDocumentFactory();
Document pdfDocument
= factory.createDocument();
pdfDocument.open();
pdfDocument.save();
pdfDocument.close();
factory = new TxtDocumentFactory();
Document txtDocument
= factory.createDocument();
txtDocument.open();
txtDocument.save();
txtDocument.close();
}
}
```
有人可能會有疑問,這樣不是還沒完全消除new嗎?首先這裡的客戶類已經到最高的呼叫層次了,這個過程當中是必然會有new的出現,不然怎樣進行程式呼叫呢?
我們所說的消除new的過程是指main與factory之間,產生的一箇中間層次(如下面的App)中去消除new。
```java
//這樣的程式碼中,就消除了new的存在
//具體的注入過程可以由其他的形式完成,比如Spring中的DI
public class App{
private DocumentFactory factory;
public void setFactory(DocumentFactory factory) {
this.factory = factory;
}
public void operateDoc(){
Document document = factory.createDocument();
document.open();
document.save();
document.close();
}
}
//main中的程式碼是最高層次,也是變化最頻繁的層次,這裡是不可能消除new的
public class Client {
public static void main(String[] args) {
DocumentFactory factory = new PdfDocumentFactory();
App app = new App();
app.setFactory(factory);
app.operateDoc();
//同樣對於其他的工廠類也是可以採用同樣的方式呼叫。
......
}
}
```
這樣修改程式碼的好處在那裡呢?第一,顯而易見的就是完全實現了“開閉原則”的思想,擴充套件時不再需要去修改原始碼。第二,有些物件的建立過程可能比較複雜,因此如果直接在應用程式當中使用new或者其他形式建立很麻煩,通過工廠建立之後,就不再需要去關注那些複雜的建立過程。第三,通過new建立,始終是一種硬編碼的形式,如果在應用程式當中過多的使用這種方式,那麼一旦某物件的建立方式發生改變,修改原始碼必然是很繁瑣的。
3. **結構——類建立型模式**
![FactoryMethod .png](https://img2020.cnblogs.com/other/1218435/202007/1218435-20200710195827683-922519749.png)
4. **參與者**
+ **Product(Document)**
定義工廠方法中工廠建立的物件的介面。
+ **ConcreteProduct(PdfDocument、TxtDocument)**
實現Product的介面。
+ **Creator(DocumentFactory)**
宣告工廠方法——createProduct(),可以呼叫該方法返回一個Product型別的物件。
+ **ConcreteCreator(PdfDocumentFactory、TxtDocumentFactory)**
重定義工廠方法以返回具體的ConcreteProduct。
+ **Client(客戶類)**
使用工廠和產品,工廠方法模式中,客戶類也是一個重要的參與者,因為工廠方法主要的作用就是分離開客戶類與產品類之間的耦合關係,所以脫離客戶類去談工廠方法模式時,總會覺得差了些什麼東西,無法完全體會到工廠方法模式的優勢。
5. **適用性**
在下列情況下可以使用Factory Method模式:
+ 當一個類不知道它所必須建立的物件的類的時候。
+ 當一個類希望由它的子類來指定它所建立的物件的時候。
+ 當類將建立物件的職責委託給多個幫助子類中的某一個,並且你希望將哪一個幫助子類是代理者這一資訊區域性化的時候。
簡單地說,就是使用過程中只需要宣告一個抽象工廠類的引用,具體呼叫那個工廠去生成那個物件,是由呼叫者去確定的。
6. **相關模式**
Abstract Factory經常用工廠方法來實現,抽象工廠建立產品的過程就可以使用工廠方法來完成。
工廠方法通常在Template Method中被呼叫,這一點在“元件協作”當中也提到過。
7. **思考**
+ **Creator的兩種實現情況。**第一種情況,Creator只作為抽象層,也就是隻宣告介面,不做任何的實現;這種情況就必須要子類來實現。第二種情況,Creator作為一個具體的實現類,而不是抽象類,這種情況下,Creator可以提供一個預設的實現介面,這樣即使沒有子類重寫它,客戶可以通過這樣一個預設的實現完成任務。
+ **靈活運用工廠方法模式。**作為一種建立類模式,在任何需要生成複雜物件的地方,都可以使用工廠方法模式。有一點需要注意的地方就是複雜物件適合使用工廠方法模式,而簡單物件,特別是只需要通過 new 就可以完成建立的物件(這也是為什麼工廠方法講解用到的例子總是無法說服人的原因之一),無需使用工廠方法模式;因為使用工廠方法模式,就需要引入一個工廠類,會增加系統的複雜度。
## 抽象工廠(Abstract Factory)
1. **意圖**
提供一個建立一系列相關或相互依賴物件的介面,而無需指定他們具體的類。
2. **例項**
假定存在這樣一個服務層,該層當中需要做的就是訪問資料庫中的資料,並且執行一系列的相關操作。根據面向介面程式設計的思想,可以先作這樣一個程式碼編寫。
```java
//對資料庫進行訪問的三個介面
//先建立連線,再執行相關操作,最後返回相應結果
public interface DBConnection{}
public interface DBCommand{}
public interface DBDataReader{}
//對於MySql,可以建立以下實現
public class MySqlDBConnection implements DBConnection{}
public class MySqlDBCommand implements DBCommand{}
public class MySqlDBDataReader implements DBDataReader{}
//同樣對於Sql Server,Oricle也是這樣的實現
......
```
這樣的實現下,我們可以說是滿足了面向介面程式設計的一個思想;並且在實現中,我們可以為每個介面,按照工廠方法模式,為其建立一個工廠。
```java
//工廠介面
public interface DBConnectionFactory{
public DBConnection createDBConnetion();
}
public interface DBCommandFactory{
public DBCommand createDBCommand();
}
public interface DBDataReaderFactory{
public DBDataReader createDBDataReader();
}
//然後對於每個具體的資料庫,實現不同的具體工廠
//以MySql為例
public class MySqlDBConnetionFactory implements DBConnectionFactory {
@Override
public DBConnection createDBConnetion() {
return new MySqlDBConnection();
}
}
public class MySqlDBCommandFactory implements DBCommandFactory {
@Override
public DBDBCommand createDBCommand() {
return new MySqlDBCommand();
}
}
public class MySqlDataReaderFactory implements DataReaderFactory {
@Override
public DBDataReader createDataReader() {
return new MySqlDataReader();
}
}
//剩下的Orcle,Sql Server也是如此
......
```
工廠模式方法的呼叫就不再演示,區別和工廠方法中的`Document`例子中差別不大。
對於這樣的實現,雖然我們很好的利用了工廠方法模式,**但是也引入了工廠方法模式的一個弊端——大量的物件和類(本例當中,三個系列,每個系列三個產品,光產品就是9個子類;每個產品再對應一個工廠,一共就是18個子類)**。在使用的過程中,反而能夠明顯的感覺到系統複雜度不減反增。並且,`DBConnection`、`DBCommand`和`DBDataReader`明顯是有著一定的關係的,換句話說,MySql建立的`DBConnection`是和`MySqlDBCommand、MySqlDBDataReader`一起使用的,如果出現`MySqlDBConnection、OricleDBCommand、SqlServerDBDataReader`這種組合肯定是無法正常執行的。這時抽象工廠的出現,就很好的解決了這樣的問題。
```java
//首先,具體的產品類不會發生變化,簡化的主要是工廠層次
//先抽象出抽象工廠,將產品系列的建立方法合併到一個介面中
public interface DBFactory{
public DBConnection createDBConnetion();
public DBCommand createDBCommand();
public DBDataReader createDBDataReader();
}
//根據不同的具體工廠,建立具體的物件
public class MySqlDBFactory implements DBFactory {
@Override
public DBConnection createDBConnetion() {
return new MySqlDBConnection();
}
@Override
public DBCommand createDBCommand() {
return new MySqlDBCommand();
}
@Override
public DBDataReader createDBDataReader() {
return new MySqlDBDataReader();
}
}
//Oricle,sql server的工廠,同樣如此
......
```
抽象工廠主要是對工廠層次的簡化,這樣修改下來,對比工廠方法模式,減少了2/3的工廠子類建立,只需要3個工廠(有多少個產品系列就有多少個工廠子類)就可以完成產品的建立。
這樣的一種建立工廠方式,不僅減少了工廠的數量,而且使得產品的一致性得以保證,它可以保證,一次只能使用同一個系列當中的物件。
```java
public class Client {
public static void main(String[] args) {
DBFactory factory = new MySqlDBFactory();
App app = new App();
app.setFactory(factory);
app.operate();
//同樣對於其他的工廠類也是可以採用同樣的方式呼叫。
// ......
}
}
class App{
private DBFactory factory;
public void setFactory(DBFactory factory) {
this.factory = factory;
}
public void operate(){
DBConnection connection
= factory.createDBConnetion();
DBCommand command
= factory.createDBCommand();
DBDataReader reader
= factory.createDBDataReader();
//執行相關操作
.....
}
}
```
這樣的應用程式程式碼,在一定程度上就減少了工廠子類的數量,並且在`operate()`中保證了產品系列的一致性,使得`MysqlDBFactory`生成的產品,只會是與MySql相關的。
3. **結構——物件建立型模式**
![Abstract Factory.png](https://i.loli.net/2020/05/25/LwjCXKSPv3dniBs.png)
4. **參與者**
+ **AbstractFactory(DBFactory)**
宣告一個建立抽象產品物件的操作介面。
+ **ConcreteFactory(MySqlDBFactory)**
實現建立具體產品物件的操作。
+ **AbstractProduct(DBConnection、DBCommand、DBDataReader)**
為一類產品物件宣告一個介面。
+ **ConcreteProduct(MySqlDBConection、MySqlDBCommand、MySqlDBDataReader)**
定義一個將被相應的具體工廠建立的產品物件,並實現抽象產品的相應介面。
+ **Client**
呼叫抽象工廠和抽象產品提供的介面。在建立者模式當中,客戶類也是重要的參與成員,因為對建立模式的理解容易混亂的點正是在客戶類中的呼叫過程 (new) 產生的,關於這個問題,已經在前面做過很多解釋了,不再多說。
5. **適用性**
以下情況可以使用AbstractFactory模式:
+ 一個系統要獨立於它的產品的建立、組合和表示時。
+ 一個系統要由多個產品系列中的一個來配置時。
+ 當你要強調一系列相關的產品物件的設計以便進行聯合使用時。
+ 當你提供一個產品類庫,而只想顯示它們的介面而非實現時。
6. **相關模式**
Singleton:一個具體的工廠通常會是一個單件。因為在一個應用中,一般每個產品系列只需要一個具體工廠。
Factory Method:在Abstract Factory中,僅僅是宣告一個建立物件的介面,真正的建立過程是由具體工廠實現的。這時,可以為每一個方法對應的具體物件之間再定義一個工廠方法。但其問題就在於,這樣的做法就像是在工廠方法上再套上一層抽象工廠,從而又增加了系統的複雜度。
7. **思考**
+ **難以支援新種類的產品** 對於工廠模式而言,它的最大優點就在於保證了產品的一致性,但也正是如此,這就使得它的產品系列需要保持穩定,如果在後續的過程中出現新的產品,比如在例項當中需要增加一個新的功能系列,就需要去擴充套件`DBFactory`介面,並且涉及到`DBFactory`及其子類的改變。
+ **定義可擴充套件的工廠** 這是對於新種類產品建立靈活性的提高,但是不太安全;就是給建立 物件的操作增加一個引數,該引數來指定將被建立的物件種類。使用這種方式,抽象工廠就只需要一個“Create”方法和一個識別符號引數即可完成建立操作。但問題就在於,客戶需要完全瞭解所有的引數才能更好使用工廠建立自己所需要的物件。
## 原型模式(Prototype)
1. **意圖**
用原型例項指定物件的建立種類,並且通過拷貝這些原型建立新的物件。
2. **例項**
Prototype模式,有點像是對工廠方法模式中產品與工廠的合併。怎麼說呢?看下面的程式碼:
```java
//工廠方法模式中的產品類與工廠方法類
public interface Document{
public void open();
public void close();
public void save();
......
}
public interface DocumentFactory{
public Document createDocument();
}
```
這是在Factory Method中使用的建立方式,而原型做的事就是,不再用工廠來進行建立,而是轉而克隆的方式。變成下面這樣:
```java
//合併Document和DocumentFactory
public abstract class Document
implements Cloneable{
public void open();
public void close();
public void save();
......
//相當於Factory中的createDocument();
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
public class PdfDocument implements Document{
@Override
public void open(){
//open pdfDocument code
System.out.println("open pdf!");
}
@Override
public void close() {
System.out.println("close pdf!");
}
@Override
public void save() {
System.out.println("save pdf!");
}
//......
}
//文件類的實現與工廠方法中的一樣
......
```
那麼在具體的客戶類當中就是通過這樣一種方式來進行呼叫:
```java
public class App{
//在工廠方法模式當中,這裡是documentFactory
private Document prototype;
public void setDoucument(Document document){
prototype = document;
}
public void useDocument(){
//documentFactory.createDocument();
Document doc = prototype.clone();
//然後使用prototype克隆出來的doc進行操作
doc.open();
......
}
}
//在客戶類中呼叫表現
public class Client {
public static void main(String[] args) {
Document doc = new PdfDocument();
App app = new App();
app.setFactory(doc);
app.useDocument();
//同樣對於其他的工廠類也是可以採用同樣的方式呼叫。
//......
}
}
```
問題來了,為什麼不直接用原型(prototype),而是要多一步克隆?解決這個問題,首先要明白的是,我們**使用原型的目的不是將原型作為客戶類的一個屬性去使用,而是一個建立者**。既然是一個建立者,那麼在使用的過程中,就不只一個地方會用到同樣型別的物件;如果在不同的地方都直接使用原型,可能會在某個地方修改了原型的值,從而使得其他直接使用原型的方法出現不可預知的錯誤。
3. **結構——物件建立型模式**
![Prototype.png](https://i.loli.net/2020/05/25/yTKFGbJikCAUVN2.png)
4. **參與者**
+ **Prototype(Document)**
宣告一個克隆自身的介面。
+ **ConcretePrototype(PdfDocument...)**
繼承Prototype並實現克隆自身的介面。
+ **Client**
讓一個原型克隆自身從而建立一個新的物件。
5. **適用性**
+ 當一個系統應該獨立於它的產品建立、構成和表示時,可以使用Prototype模式。
+ 當要例項化的類是在執行時候指定時,比如動態裝載。
+ 為了避免建立一個產品類平行的工廠類層次時。
+ 當一個類的例項只能有幾種不同狀態組合中的一種時,建立相應數目的原型並克隆他們可以比每次用合適的狀態手工例項化該類更方便一些。
6. **相關模式**
Abstract Factory和Prototype在某種方面是相互競爭的,但是在某種情況下也是可以一起使用,比如,在抽象工廠中儲存一個被克隆的產品集合,在使用時,直接根據集合中的物件返回相應的產品。
大量使用Composite(組合模式)和Decorator(裝飾器模式)的設計上也可以採用Protorype來減少Composite或Decorator物件的建立。
7. **思考**
+ **減少了子類的構造**。例項當中就可以看出,原型模式簡化了工廠方法模式中的工廠類,因此,減少了許多子類的建立。
+ **資源優化**。類初始化可能需要消化非常多的資源,這個資源包括資料、硬體資源等等,使用原型模式就可以減少這樣一個初始化過程。
+ **深、淺拷貝的問題**。對於java而言,淺拷貝實現 Cloneable,深拷貝是通過實現 Serializable 讀取二進位制流。
## 建造者模式( Builder)
1. **意圖**
將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以有不同的表示。
2. **例項**
在遊戲場景當中,尤其是3d場景中,必不可少就是建築物,比如說房子。對房子的構建肯定不是一下全部構建完成的,而是會分成幾個部分,比如牆、窗戶、地板、房頂、門,一部分、一部分地去構建。
```java
public abstract class House{
//房子屬性,紋理、材質...
private Texture texture;
private Material material;
......
//牆、窗戶、地板、房頂、門
public Wall buildWall();
public Window buildWindow();
public Floor buildFloor();
public Door buildDoor();
public Roof buildRoof();
//房子構建過程
public void buildHouse(){
buildFloor();
Wall[] walls = new Wall[4];
for(i