大型Java進階專題(四) 設計模式之工廠模式
阿新 • • 發佈:2020-03-17
## 前言
今天開始我們專題的第三課了,開始對設計模式進行講解,本章節介紹:瞭解設計模式的由來,介紹設計模式能幫我們解決那些問題以及剖析工廠模式的歷史由來及應用場景。本章節參考資料書籍《Spring 5核心原理》中的第一篇 Spring 內功心法(Spring中常用的設計模式)(沒有電子檔,都是我取其精華並結合自己的理解,一個字一個字手敲出來的)。
## 回顧軟體設計原則
在講設計模式之前,我們一定要先了解軟體設計原則。現在先來回顧一下軟體設計七大原則:
**開閉原則**:對擴充套件開放,對修改關閉。
**依賴倒置原則**:通過抽象使各個類或者模組不相互影響,實現鬆耦合。
**單一職責原則**:一個類、介面、方法只做一件事。
**介面隔離原則**:儘量保證介面的純潔性,客戶端不應該依賴不需要的介面。
**迪米特法則**:又叫最少知道原則,一個類對其所依賴的類知道得越少越好。
**里氏替換原則**:子類可以擴充套件父類的功能但不能改變父類原有的功能。
**合成複用原則**:儘量使用物件組合、聚合,而不使用繼承關係達到程式碼複用的目的。
## 為什麼要從設計模式開始
平時我們寫的程式碼雖然滿足了需求但往往不利於專案的開發與維護,通過合理運用設計模式,可以讓我們以後維護更方便,因此學會對程式碼的重構是非常重要的。Spring中對設計模式運用可謂是淋漓盡致,比如:
工廠模式:BeanFactory
裝飾器模式:BeanWrapper
代理模式:AopProxy
委派模式:DispatcherServlet
策略模式:HandlerMapping
介面卡模式:HandlerAdapter
模板模式:JdbcTemplate
觀察者模式:ContextLoaderListener
需要特別宣告的是,設計模式從來都不是單個設計模式獨立使用的。在實際應用中,通常是多個設計模式混合使用, 你中有我, 我中有你。
##工廠模式詳解
### 簡單工廠模式
簡單工廠模式(Simple Factory Pattern)是指由一個工廠物件決定創建出哪一種產品類的例項,但它不屬於GOF,23種設計模式。簡單工廠適用於工廠類負責建立的物件較少的場景,且客戶端只需要傳入對應的引數,工廠就生產對應的物件,對於如何建立物件的邏輯呼叫方不需要關心。
我們以種植水果為例,先建立種植水果介面IFruit:
```java
public interface IFruit {
/**
* 種植水果方法
*/
void plant();
}
```
```java
//實現種植蘋果
public class Apple implements IFruit {
public void plant() {
System.out.println("種植蘋果");
}
}
```
```java
//實現種植橙子
public class Orange implements IFruit {
public void plant() {
System.out.println("種植橙子");
}
}
```
我們再看下呼叫方程式碼,當想種植某一種水果時候:
```java
public static void main(String[] args) {
//種植蘋果
IFruit fruit = new Apple();
fruit.plant();
}
```
如果此時又要換成種植橙子:
```java
public static void main(String[] args) {
//IFruit fruit = new Apple();
//種植橙子
IFruit fruit = new Orange();
fruit.plant();
}
```
父類IFruit指向子類Apple的引用,呼叫方程式碼需要依賴Aplle,這樣隨著種植水果的業務越來越多了,呼叫方的程式碼就會變得越來越臃腫了。我們要想辦法把這種依賴減弱,把建立細節隱藏起來。雖然目前程式碼中,我們建立的的物件並不複雜,但是從打碼設計角度來講不易擴充套件。我們用簡單工廠對程式碼進行優化。
建立PlantFruitsFactory工廠:
```java
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;
}
}
}
```
修改呼叫方程式碼:
```java
public class DemoTest {
public static void main(String[] args) {
IFruit fruit = PlantFruitsFactory.PlantFruit("Apple");
fruit.plant();
}
}
```
下面我們看下類圖:
![](https://img2020.cnblogs.com/blog/874710/202003/874710-20200317100246242-791358277.png)
呼叫方不再直接關心建立細節,只需要傳入指定引數給工廠,工廠負責建立對應的物件給呼叫方。雖然基本實現了工廠模式的邏輯,但是如果隨著業務的擴充套件,需要建立種植更多品種的水果時,工廠方法每次都需要跟著修改,不符合開閉原則。所以我們還可以再優化下工廠類:
```java
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的具體建立類:
```java
//原始碼中的方法
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介面:
```java
//工廠介面
public interface IFruitFactory {
IFruit create();
}
```
```java
//生成蘋果的工廠
public class AppleFactory implements IFruitFactory {
@Override
public IFruit create() {
return new Apple();
}
}
```
```java
//生成橙子的工廠
public class OrangeFactory implements IFruitFactory {
@Override
public IFruit create() {
return new Orange();
}
}
```
```java
//呼叫方程式碼
public class DemoTest {
public static void main(String[] args) {
//建立蘋果工廠 生產蘋果
IFruitFactory fruitFactory = new AppleFactory();
fruitFactory.create().plant();
//建立橙子工廠 生成橙子
IFruitFactory orangeFactory = new OrangeFactory();
orangeFactory.create().plant();
}
}
```
現在我們看下類圖:
![](https://img2020.cnblogs.com/blog/874710/202003/874710-20200317100319243-1706564550.png)
工廠方法適用於以下場景:
1、建立物件需要大量重複的程式碼。
2、客戶端(應用層)不依賴於產品類例項如何被建立、實現等細節。
3、一個類通過其子類來指定建立哪個物件。
工廠方法也有缺點:
1、類的個數容易過多,增加複雜度。
2、增加了系統的抽象性和理解難度。
### 抽象工廠模式
抽象工廠模式(Abastract Factory Pattern)是指提供一個建立一系列相關或相互依賴產品族產品等級結構一個產品等級結構物件的介面,無須指定他們具體的類。客戶端(應用層)不依賴於產品類例項如何被建立、實現等細節,強調的是一系列相關的產品物件(屬於同一產品族)一起使用建立物件需要大量重複的程式碼。需要提供一個產品類的庫,所有的產品以同樣的接口出現,從而使客戶端不依賴於具體實現。
還是以水果為例,現在水果工廠不僅種植水果還開始加工水果。相當於現在的業務變更為同一個水果類不單純只是種植水果的功能。在增加兩個介面IPlant種植水果和IProcess加工水果。
```java
public interface IPlant {
//種植產品
void plant();
}
```
```java
public interface IProcess {
//加工產品
void process();
}
```
然後建立一個抽象工廠FruitFactory:
```java
/**
* 抽象工廠是使用者的主入口
* 在 Spring 中應用得最為廣泛的一種設計模式
* 易於擴充套件
*/
public interface FruitFactory {
IPlant createPlant();
IProcess createProcess();
}
```
然後建立蘋果產品線:
```java
//種植蘋果
public class ApplePlant implements IPlant {
@Override
public void plant() {
System.out.println("種植蘋果");
}
}
```
```java
//加工蘋果
public class AppleProcess implements IProcess {
@Override
public void process() {
System.out.println("加工蘋果");
}
}
```
建立蘋果工廠:
```java
public class AppleFactory implements FruitFactory {
@Override
public IPlant createPlant() {
return new ApplePlant();
}
@Override
public IProcess createProcess() {
return new AppleProcess();
}
}
```
對應的橙子也是一樣的。
上面的程式碼完整地描述了兩個產品族蘋果和橙子,也描述了兩個產品等級種植和加工。抽象工廠非常完美清晰地描述這樣一層複雜的關係。但是,不知道大家有沒有發現,如果我們再繼續擴充套件產品等級,將出售Sale也加入產品中,那麼我們的程式碼從抽象工廠,到具體工廠要全部調整,很顯然不符合開閉原則。因此抽象工廠也是有缺點的:
1、規定了所有可能被建立的產品集合,產品族中擴充套件新的產品困難,需要修改抽象工廠的介面。
2、增加了系統的抽象性和理解難度。
###總結
工廠模式的三種方式,沒有絕對的好壞,合適的場景使用合適的模式,實際應用中,我們千萬不能犯強迫症甚至有潔癖。在實際需求中產品等級結構升級是非常正常的一件事情。我們可以根據實際情況,只要不是頻繁升級,可以不遵循開閉原則。程式碼每半年升級一次或者每年升級一次又有何不