大型Java進階專題(四) 設計模式之工廠模式
- 2020 年 3 月 17 日
- 筆記
前言
今天開始我們專題的第三課了,開始對設計模式進行講解,本章節介紹:了解設計模式的由來,介紹設計模式能幫我們解決那些問題以及剖析工廠模式的歷史由來及應用場景。本章節參考資料書籍《Spring 5核心原理》中的第一篇 Spring 內功心法(Spring中常用的設計模式)(沒有電子檔,都是我取其精華並結合自己的理解,一個字一個字手敲出來的)。
回顧軟體設計原則
在講設計模式之前,我們一定要先了解軟體設計原則。現在先來回顧一下軟體設計七大原則:
開閉原則:對擴展開放,對修改關閉。
依賴倒置原則:通過抽象使各個類或者模組不相互影響,實現松耦合。
單一職責原則:一個類、介面、方法只做一件事。
介面隔離原則:盡量保證介面的純潔性,客戶端不應該依賴不需要的介面。
迪米特法則:又叫最少知道原則,一個類對其所依賴的類知道得越少越好。
里氏替換原則:子類可以擴展父類的功能但不能改變父類原有的功能。
合成復用原則:盡量使用對象組合、聚合,而不使用繼承關係達到程式碼復用的目的。
為什麼要從設計模式開始
平時我們寫的程式碼雖然滿足了需求但往往不利於項目的開發與維護,通過合理運用設計模式,可以讓我們以後維護更方便,因此學會對程式碼的重構是非常重要的。Spring中對設計模式運用可謂是淋漓盡致,比如:
工廠模式:BeanFactory
裝飾器模式:BeanWrapper
代理模式:AopProxy
委派模式:DispatcherServlet
策略模式:HandlerMapping
適配器模式:HandlerAdapter
模板模式:JdbcTemplate
觀察者模式:ContextLoaderListener
需要特別聲明的是,設計模式從來都不是單個設計模式獨立使用的。在實際應用中,通常是多個設計模式混合使用, 你中有我, 我中有你。
工廠模式詳解
簡單工廠模式
簡單工廠模式(Simple Factory Pattern)是指由一個工廠對象決定創建出哪一種產品類的實例,但它不屬於GOF,23種設計模式。簡單工廠適用於工廠類負責創建的對象較少的場景,且客戶端只需要傳入對應的參數,工廠就生產對應的對象,對於如何創建對象的邏輯調用方不需要關心。
我們以種植水果為例,先創建種植水果介面IFruit:
public interface IFruit { /** * 種植水果方法 */ void plant(); }
//實現種植蘋果 public class Apple implements IFruit { public void plant() { System.out.println("種植蘋果"); } }
//實現種植橙子 public class Orange implements IFruit { public void plant() { System.out.println("種植橙子"); } }
我們再看下調用方程式碼,當想種植某一種水果時候:
public static void main(String[] args) { //種植蘋果 IFruit fruit = new Apple(); fruit.plant(); }
如果此時又要換成種植橙子:
public static void main(String[] args) { //IFruit fruit = new Apple(); //種植橙子 IFruit fruit = new Orange(); fruit.plant(); }
父類IFruit指向子類Apple的引用,調用方程式碼需要依賴Aplle,這樣隨著種植水果的業務越來越多了,調用方的程式碼就會變得越來越臃腫了。我們要想辦法把這種依賴減弱,把創建細節隱藏起來。雖然目前程式碼中,我們創建的的對象並不複雜,但是從打碼設計角度來講不易擴展。我們用簡單工廠對程式碼進行優化。
創建PlantFruitsFactory工廠:
public static class PlantFruitsFactory { public IFruit PlantFruit(String fruitName) { //這裡使用的if判斷,用switch一樣的道理 if ("Apple".equals(fruitName)){ return new Apple(); }else if ("Orange".equals(fruitName)){ return new Orange(); }else { return null; } } }
修改調用方程式碼:
public class DemoTest { public static void main(String[] args) { IFruit fruit = PlantFruitsFactory.PlantFruit("Apple"); fruit.plant(); } }
下面我們看下類圖:
調用方不再直接關心創建細節,只需要傳入指定參數給工廠,工廠負責創建對應的對象給調用方。雖然基本實現了工廠模式的邏輯,但是如果隨著業務的擴展,需要創建種植更多品種的水果時,工廠方法每次都需要跟著修改,不符合開閉原則。所以我們還可以再優化下工廠類:
public class PlantFruitsFactory { //包路徑的前綴 private static final String PACKAGE_PATH = "com.study.demo."; public static IFruit PlantFruit(String fruitName){ try { return (IFruit) Class.forName(PACKAGE_PATH+fruitName).newInstance(); } catch (Exception e) { e.printStackTrace(); } return null; } }
這樣即使有新需求添加新的水果品種,我們也不需要修改工廠類的程式碼了,只需要新建一個對應的類,工廠會根據反射創建對應的對象給調用方了。(這裡其實還可以優化,可以將方法的入參改為對應的Class對象,這樣就不需要強轉了,這裡就不做演示了。)
簡單工廠模式在JDK源碼也是無處不在,現在我們來舉個例子,例如Calendar類,看
Calendar.getInstance()方法,下面打開的是Calendar的具體創建類:
//源碼中的方法 private static Calendar createCalendar(TimeZone zone, Locale aLocale) { CalendarProvider provider = LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale) .getCalendarProvider(); if (provider != null) { try { return provider.getInstance(zone, aLocale); } catch (IllegalArgumentException iae) { // fall back to the default instantiation } } Calendar cal = null; if (aLocale.hasExtensions()) { String caltype = aLocale.getUnicodeLocaleType("ca"); if (caltype != null) { switch (caltype) { case "buddhist": cal = new BuddhistCalendar(zone, aLocale); break; case "japanese": cal = new JapaneseImperialCalendar(zone, aLocale); break; case "gregory": cal = new GregorianCalendar(zone, aLocale); break; } } } if (cal == null) { // If no known calendar type is explicitly specified, // perform the traditional way to create a Calendar: // create a BuddhistCalendar for th_TH locale, // a JapaneseImperialCalendar for ja_JP_JP locale, or // a GregorianCalendar for any other locales. // NOTE: The language, country and variant strings are interned. if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") { cal = new BuddhistCalendar(zone, aLocale); } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja" && aLocale.getCountry() == "JP") { cal = new JapaneseImperialCalendar(zone, aLocale); } else { cal = new GregorianCalendar(zone, aLocale); } } return cal; }
當然工廠模式也有他的缺點:工廠類的職責相對過重,不易於擴展過於複雜的產品結構。
工廠方法模式
工廠方法模式(FatoryMethod Pattern)是指定義一個創建對象的介面,但讓實現這個介面的類來決定實例化哪個類,工廠方法讓類的實例化推遲到子類中進行。在工廠方法模式中用戶只需要關心所需產品對應的工廠,無須關心創建細節,而且加入新的產品符合開閉原則。
工廠方法模式主要解決產品擴展的問題,在簡單工廠中,隨著產品鏈的豐富,如果每個課程的創建邏輯有區別的話,工廠的職責會變得越來越多,有點像萬能工廠,並不便於維護。根據單一職責原則我們將職能繼續拆分,專人干專事。Apple由Apple工廠創建,Orange由 Orange工廠創建,對工廠本身也做一個抽象。來看程式碼,先創建IFruitFactory介面:
//工廠介面 public interface IFruitFactory { IFruit create(); }
//生成蘋果的工廠 public class AppleFactory implements IFruitFactory { @Override public IFruit create() { return new Apple(); } }
//生成橙子的工廠 public class OrangeFactory implements IFruitFactory { @Override public IFruit create() { return new Orange(); } }
//調用方程式碼 public class DemoTest { public static void main(String[] args) { //創建蘋果工廠 生產蘋果 IFruitFactory fruitFactory = new AppleFactory(); fruitFactory.create().plant(); //創建橙子工廠 生成橙子 IFruitFactory orangeFactory = new OrangeFactory(); orangeFactory.create().plant(); } }
現在我們看下類圖:
工廠方法適用於以下場景:
1、創建對象需要大量重複的程式碼。
2、客戶端(應用層)不依賴於產品類實例如何被創建、實現等細節。
3、一個類通過其子類來指定創建哪個對象。
工廠方法也有缺點:
1、類的個數容易過多,增加複雜度。
2、增加了系統的抽象性和理解難度。
抽象工廠模式
抽象工廠模式(Abastract Factory Pattern)是指提供一個創建一系列相關或相互依賴產品族產品等級結構一個產品等級結構對象的介面,無須指定他們具體的類。客戶端(應用層)不依賴於產品類實例如何被創建、實現等細節,強調的是一系列相關的產品對象(屬於同一產品族)一起使用創建對象需要大量重複的程式碼。需要提供一個產品類的庫,所有的產品以同樣的介面出現,從而使客戶端不依賴於具體實現。
還是以水果為例,現在水果工廠不僅種植水果還開始加工水果。相當於現在的業務變更為同一個水果類不單純只是種植水果的功能。在增加兩個介面IPlant種植水果和IProcess加工水果。
public interface IPlant { //種植產品 void plant(); }
public interface IProcess { //加工產品 void process(); }
然後創建一個抽象工廠FruitFactory:
/** * 抽象工廠是用戶的主入口 * 在 Spring 中應用得最為廣泛的一種設計模式 * 易於擴展 */ public interface FruitFactory { IPlant createPlant(); IProcess createProcess(); }
然後創建蘋果產品線:
//種植蘋果 public class ApplePlant implements IPlant { @Override public void plant() { System.out.println("種植蘋果"); } }
//加工蘋果 public class AppleProcess implements IProcess { @Override public void process() { System.out.println("加工蘋果"); } }
創建蘋果工廠:
public class AppleFactory implements FruitFactory { @Override public IPlant createPlant() { return new ApplePlant(); } @Override public IProcess createProcess() { return new AppleProcess(); } }
對應的橙子也是一樣的。
上面的程式碼完整地描述了兩個產品族蘋果和橙子,也描述了兩個產品等級種植和加工。抽象工廠非常完美清晰地描述這樣一層複雜的關係。但是,不知道大家有沒有發現,如果我們再繼續擴展產品等級,將出售Sale也加入產品中,那麼我們的程式碼從抽象工廠,到具體工廠要全部調整,很顯然不符合開閉原則。因此抽象工廠也是有缺點的:
1、規定了所有可能被創建的產品集合,產品族中擴展新的產品困難,需要修改抽象工廠的介面。
2、增加了系統的抽象性和理解難度。
總結
工廠模式的三種方式,沒有絕對的好壞,合適的場景使用合適的模式,實際應用中,我們千萬不能犯強迫症甚至有潔癖。在實際需求中產品等級結構升級是非常正常的一件事情。我們可以根據實際情況,只要不是頻繁升級,可以不遵循開閉原則。程式碼每半年升級一次或者每年升級一次又有何不可呢?