在面試中通過工廠模式來證明自己的能力
在面試中,候選人經常會被問到,你在專案裡用到過哪些設計模式?對此,你可以按本文給出的步驟,系統地通過工廠模式展示自己在設計思想方面的能力。
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訂單的建造者物件)的呼叫方式,通過傳入的具體的建造者物件(不是抽象的建造者物件)來完成建造。
上述的敘述是給大家做個參考,其實根據實際的專案需求敘述建造者模式並不困難,一般來說,很多面試官會多問句,建造者模式和工廠模式有什麼差別?這在前文裡也說過了,大家可以通過專案需求詳細說