在面試中,候選人經常會被問到,你在項目裡用到過哪些設計模式?對此,你可以按本文給出的步驟,系統地通過工廠模式展示自己在設計思想方面的能力。
1 通過工廠模式屏蔽建立細節
工廠模式(Factory Method)是用來向使用者屏蔽建立對象的細節。之前我們在講SAX解析XML檔案時,已經用到過工廠模式,當時我們是通過如下代碼用SAXParserFacotry這個工廠對象來建立用于解析的parse對象,代碼如下所示。
1 SAXParserFactory factory = SAXParserFactory.newInstance();
2 SAXParser parser = factory.newSAXParser();
作為使用者,我們隻要能得到parser對象進行後繼的解析動作,至于parser對象是如何建立的,我們不需要,也不應管。如果不用工廠模式,那麼我們還得親自關注如何建立parser對象,比如得考慮建立時傳入的參數,以及是否改用“池”的方式來建立進而提升效率。
這樣親力親為的後果是,會讓使用和建立parser對象的代碼耦合度很高,這樣一旦建立parser的方法發生改變,比如日後需要傳入不同的參數,那麼使用parser的代碼也需要對應修改。
大家别以為增加修改量沒什麼大不了,如果我們在某個子產品裡修改了代碼,哪怕這個修改點再小,也得經過完整的測試才能把這段代碼放入生産環境,這是需要工作量的。如果我們把“使用”和“建立”對象放在一個子產品裡,那麼“使用”部分的代碼也得測試(雖然沒改),但我們通過了工廠模式分離了兩者,那麼隻需要測“建立”子產品,就可以減少工作量了。
下面我們先來看下工廠模式的實作代碼,比如我們要編寫(建立)Java和資料庫方面的兩本書,先在第1行建構一個Book的基類,在第4和第7行建立兩個子類,而且我們可以把一些通用性的方法(比如“查資料”)放入Book類裡。
1 class Book {
2 public book(){ }
3 }
4 public class JavaBook extends Book {
5 public JavaBook(){System.out.println("Write Java Book");}
6 }
7 public class DBBook extends Book{
8 public DBBook(){System.out.println("Write DB Book"); }
9 }
随後我們通過如第10行的接口來定義建立動作,根據需求,我們可以在第11和17行實作這個接口,在其中分别實作“編寫Java書”和“編寫資料庫書”的代碼。
10 interface BookFactory { Book createBook(); }
11 public class JavaFactory implements BookFactory{
12 public JavaBook createBook(){
13 //省略其它編寫Java書的代碼
14 return new JavaBook();
15 }
16 }
17 public class DBFactory implements BookFactory{
18 public DBBook createBook() {
19 //省略其它編寫資料庫書的代碼
20 return new DBBook();
21 }
22 }
在上述代碼裡,我們提供了“建立”的方法,下面我們給出了“調用”的代碼,從第2和第4行的代碼中我們能看到,這裡外部對象可以通過兩種不同的createBook方法分别得到Java和資料庫書。
1 BookFactory javaFactory = new JavaFactory ();
2 JavaBook javaBook = javaFactory.createBook();
3 BookFactory dbFactory = new DBFactory ();
4 DBBook dbBook = dbFactory.createBook();
2 簡單工廠模式違背了開閉原則
大家在通過上文,舉例講清楚工廠模式後,可以立即說出這個結論。具體舉例如下。
在上述的案例中,如果遇到新需求,需要再建立C語言的書,首先可以在Book父類下再建立一個CBook子類,随後可以在BookFactory接口下再建立一個新的工廠來建立,代碼如下。
1 public class CBook extends Book { //建構一個新的類
2 public CBook(){System.out.println("Write C Book");}
3 }
4 public class CFactory implements BookFactory{
5 public CBook createBook() {
6 //省略其它寫C語言書的代碼
7 return new CBook();
8 }
9 }
對于這個修改需求,我們并沒有修改原有的建立Java和資料庫書籍相關的代碼,而是通過添加新的子產品來實作,這種做法很好地符合了“開閉原則”。
開閉原則(Open Closed Principle,也叫OCP)和設計模式無關,它是一種設計架構的原則,其核心思想是,系統(或子產品或方法)應當對擴充開放,對修改關閉,比如對于上述案例,遇到擴充了,我們沒有修改現有代碼,進而可以避免測試不相幹的子產品。
我們就用簡單工廠為例,來看下沒采用開閉原則的後果,比如我們還是要建立Java和資料庫方面的書,那麼是在一個方法裡根據參數的不同來傳回不同種的類型。
1 public class BookFactory {
2 public Book create(String type) {
3 switch (type) {
4 case "Java": return new JavaBook();
5 case "DB":return new DBBook();
6 //要擴充的話,隻能加在這裡
7 case "C":return new CBook();
8 default: return null;
9 }
10 }
11 }
如果要加新類型的書,隻能是新加一個case,一旦有修改,那麼我們得改動第2行的create方法,這樣一來,create方法(乃至BookFactory類)對修改就不關閉了。如果大家對此不了解,可以回顧下工廠模式的案例,當時遇到這個需求,我們是通過添加CFactory類來實作的,原來的BookFactory和DBFactory并沒有改動(它們對修改關閉了)。
對比一下兩者的差别,由于簡單工廠模式沒遵循開閉原則,那麼一旦添加C語言的書籍,那麼就影響到其它不相幹的Java和DB書籍了(這兩部分的case代碼也得随之測試),這也是為什麼簡單工廠模式适用場景比較少的原因。
3 抽象工廠和一般工廠模式的差別
抽象工廠是對一般工廠模式的擴充,比如我們在寫java和資料庫方面的書籍時,需要添加錄制講解視訊的方法,也就是說,在Java書和資料庫書這兩個産品裡,我們不僅要包含文稿,還得包含視訊。
具體到生産Java書和資料庫書的這兩個工廠裡,我們要生産多類産品,不僅得包括文稿,還得包括代碼,此時就可以使用抽象模式,示例代碼如下。
1 class Video { //視訊的基類
2 public Video(){ }
3 }
4 public class JavaVideo extends Video { 省略定義動作 }
5 public class DBBook extends Video { 省略定義動作 }
在第1行裡,我們建立了視訊的基類,在第4和第5行裡,建立了針對Java和資料庫書視訊的兩個類。
6 abstract class CreateBook{ //抽象工廠
7 public abstract Book createBook();//編寫文稿
8 public abstract Book createVideo();//錄制視訊
9 }
10 //具體建立java書的工廠
11 class CreateJavaBook extends CreateBook{
12 public JavaBook createBook() {省略編寫文稿的具體動作}
13 public JavaVideo createVideo() {省略錄制視訊的具體動作}
14 }
15 //具體建立資料庫書的工廠
16 class CreateDBBook extends CreateBook{
17 public DBBook createBook() {省略編寫文稿的具體動作}
18 public DBVideo createVideo() {省略錄制視訊的具體動作}
19 }
在第6行裡,我們定義了一個抽象工廠,在其中定義了建立視訊和書籍的兩個方法,在第11和16行,我們通過繼承這個抽象工廠,實作了生産兩個具體Java和資料庫書籍的工廠。
和一般工廠相比,抽象工廠的頂層類一般是抽象類(也就是抽象工廠名稱的來源),但和一般工廠模式相比,沒有優劣之分,隻看哪種模式更能适應需求。比如要在同一類産品(比如書)裡生産多個子産品(比如文稿和視訊),那麼就可以通過抽象工廠模式,而如果需要生産的産品裡隻有主部件(比如文稿),而不需要附屬産品(比如視訊),那麼就可以用一般工廠模式。
4 再進一步分析建造者模式和工廠模式的差別
建造者模式和工廠模式都是關注于“建立對象”,在面試時,我們一般會問它們的差别。通過工廠模式,我們一般都是建立一個(或一類)産品,而不關心産品的組成部分,建造者模式也是用來建立一個産品,但它不僅建立産品,更專注這個産品的元件群組成過程。
通過下面的代碼,我們來看下建造者模式的用法,大家可以對比下建造者和工廠模式的差别。
1 //定義一個待生産的産品,比如帶視訊講解的書
2 public class BookwithVideo {
3 //其中包括了稿件和視訊兩個元件
4 Book PaperBook;
5 Video Video;
6 }
7 //定義一個抽象的建造者
8 public abstract class Builder {
9 public abstract Book createPaperBook();//編寫稿件
10 public abstract Video createVideo();//錄制視訊
11 }
12 //定義一個具體的建造者,用來建立Java書
13 public class JavaBookProduct extends Builder {
14 private BookwithVideo bookWithVideo = new BookwithVideo();
15 //通過這個方法傳回組裝後的書(稿件加視訊)
16 public BookWithVideo getBook(){return bookWithVideo;}
17 //編寫稿件
18 public void setPaperBook() {
19 //創造Java文稿,并賦予javaBook對象
20 bookWithVideo.book = javaBook;
21 }
22 //錄制視訊
23 public void setVideo() {
24 錄制Java書的視訊,并賦予javaVideo對象
25 bookWithVideo.video = javaVideo;
26 }
27 }
28 //定義一個具體的資料庫書籍的建造者
29 public class DBBookProduct extends Builder {
30 private BookwithVideo bookWithVideo = new BookwithVideo();
31 //通過這個方法傳回組裝後的書(稿件加視訊)
32 public BookWithVideo getBook(){return bookWithVideo;}
33 //紙質書
34 public void setPaperBook() {
35 寫資料庫書的文稿,并賦予dbBook對象
36 bookWithVideo.book = dbBook;
37 }
38 //錄制視訊
39 public void setVideo() {
40 錄制資料庫書的視訊,并賦予dbVideo對象
41 bookWithVideo.video = dbVideo;
42 }
43 }
在第8行裡,我們定義了一個抽象的創造者類Builder,在第13和29這兩行裡,我們通過繼承Builder這個創造者類建立了兩個實體創造者,分别用來創造Java和資料庫的書籍。
在每一個創造者裡,我們通過了setPaperBook方法創造文稿,通過setVideo建立視訊,并把創造好的文稿和視訊分别賦予bookWithVideo對象裡的兩個文稿和視訊元件。
看到這裡,似乎和工廠模式差不多,由于建造者模式會偏重于元件的建立過程,是以會通過如下的總控類來組裝對象,而工廠模式偏重于“建立産品“的這個結果,而不關注産品中組裝各元件的過程,是以一般不會有總控類。
44 //總控類
45 public class Director {
46 void productBook(Builder builder){
47 builder.setPaperBook();
48 builder.setVideo();
49 }
50 }
在總控類裡的第46行裡,我們定義了用來建立書的productBook方法,請注意這個方法是抽象的builder類,通過下面的代碼,我們能看到如何通過上述定義的總控類和建造者類來動态地建立不同種類的對象。
1 Director director = new Director();
2 Builder javaBookBuild = new JavaBookProduct();
3 Builder dbBookBuilder = new DBBookProduct();
4 director.productBook(javaBookBuild);
5 director.productBook(dbBookBuilder);
在第1行裡,我們定義了一個總控類,在第2和第3行裡,我們定義了具體的建立Java和資料庫書籍的建造者對象,在第4和第5行裡,分别傳入了javaBookBuilder和dbBookBuilder這兩個建造者對象,這樣在總控類的productBook方法裡,會根據傳入參數類型的不同,分别建造java和資料庫書籍。
我們經常通過建造者模式來建立項目裡的業務對象,是以候選人在他們的項目裡一般都會用到這種模式,在面試中也經常聽到候選人用這種模式來舉例,這裡列一種比較好的回答。
第一,這位候選人用電商平台的訂單來舉例,首先他建立了一個訂單的基類,在其中包括了商品清單、總價錢、總積分和發貨位址這四個元件。
第二,通過繼承這個訂單基類,建立了兩類訂單,分别是“一般使用者的訂單”和“VIP客戶的訂單”,它們的算總價和算總積分的業務邏輯是不同的。
第三,定義了一個抽象的建造者對象,在其中定義了諸如“統計商品”和“算總價”等的方法。
第四,通過繼承抽象的建造者,定義了兩個具體的建造者,分别用來建造“一般訂單”和“VIP訂單”,在每個具體的建造者對象裡,建立商品清單、總價錢、總積分和發貨位址,并把它們組裝成一個訂單。
第五,也是關鍵點,需要建立一個總控類(這也是建造者模式的核心,也是和工廠模式的差别點),在其中提供一個productOrder(Builder builder)方法,它的參數是抽象的建造者。
至此構造了建造者模式的全部代碼,在需要建立訂單時,則可以通過productOrder(VIP訂單的建造者對象)的調用方式,通過傳入的具體的建造者對象(不是抽象的建造者對象)來完成建造。
上述的叙述是給大家做個參考,其實根據實際的項目需求叙述建造者模式并不困難,一般來說,很多面試官會多問句,建造者模式和工廠模式有什麼差别?這在前文裡也說過了,大家可以通過項目需求詳細說明。