設計模式之 簡單工廠模式詳解
定義:從設計模式的類型上來說,簡單工廠模式是屬於創建型模式,又叫做靜態工廠方法(Static Factory Method)模式,但不屬於23種GOF設計模式之一。簡單工廠模式是由一個工廠對象決定創建出哪一種產品類的實例。簡單工廠模式是工廠模式家族中最簡單實用的模式,可以理解為是不同工廠模式的一個特殊實現。
定義中最重要的一句話就是,由一個工廠對象決定創建出哪一種產品類的實例,這個LZ在下面會專門舉一個現實應用中的例子去展現。
另外給出簡單工廠模式的類圖,本類圖以及上面的定義都引自百度百科。
可以看出,上面總共有三種類,一個是工廠類Creator,一個是產品接口IProduct,一個便是具體的產品,例如產品A和產品B,這之中,工廠類負責整個創建產品的邏輯判斷,所以為了使工廠類能夠知道我們需要哪一種產品,我們需要在創建產品時傳遞給工廠類一個參數,去表明我們想要創建哪種產品。
下面LZ將上面的類圖轉化為更為簡單的JAVA代碼,便於清晰的展示上面類圖中各個類之間的關系。
首先是產品接口。
public interface IProduct { public void method(); }
兩個具體的產品。
public class ProductA implements IProduct{ public void method() { System.out.println("產品A方法"); } }
public class ProductB implements IProduct{ public void method() { System.out.println("產品B方法"); } }
下面是工廠類。
public class Creator { private Creator(){} public static IProduct createProduct(String productName){ if (productName == null) { return null; } if (productName.equals("A")) { return new ProductA(); }else if (productName.equals("B")) { return new ProductB(); }else { return null; } } }
最終客戶端調用,並顯示結果。
public class Client { public static void main(String[] args) { IProduct product1 = Creator.createProduct("A"); product1.method(); IProduct product2 = Creator.createProduct("B"); product2.method(); } }
上面便是整個類圖轉換為JAVA代碼的簡單例子,將上面標準的簡單工廠模式的類圖和代碼給出,是為了一些新手先熟悉一下這個設計模式的大體框架,方便我們下面使用實際的例子去闡述的時候更加容易理解。
下面LZ就找一個各位基本上都使用過或者將來要使用的一個例子來說明簡單工廠模式,我們去模擬一個簡單的struts2的功能。
LZ會自己制作一個簡單的WEB項目來做例子,其中會忽略掉很多細節,目的是為了突出我們的簡單工廠模式。
眾所周知,我們平時開發web項目大部分是以spring作為平臺,來集成各個組件,比如集成struts2來完成業務層與表現層的邏輯,集成hibernate或者ibatis來完成持久層的邏輯。
struts2在這個過程當中提供了分離數據持久層,業務邏輯層以及表現層的責任,有了Struts2,我們不再需要servlet,而是可以將一個普通的Action類作為處理業務邏輯的單元,然後將表現層交給特定的視圖去處理,比如JSP,template等等。
我們來嘗試著寫一個非常非常簡單的web項目,來看看在最原始的時候,也就是沒有spring,struts2等等這些個開源框架的時候,我們都是怎麽過的。
由於LZ會省略WEB架構過程當中的很多細節,所以最好是各位親手做過一些項目,相對而言看起來會更有體會一些,不過LZ相信既然有興趣來看設計模式,應該都基本上有過這種鍛煉。
下面LZ把我們一個簡單的WEB項目中需要的類都列出來,並加上簡單的註釋。
import javax.servlet.http.HttpServlet; //假設這是一個小型的WEB項目,我們通常裏面會有這些類 //這個類在代理模式出現過,是我們的數據源連接池,用來生產數據庫連接。 class DataSource{} //我們一般會有這樣一個數據訪問的基類,這個類要依賴於數據源 class BaseDao{} //一般會有一系列這樣的DAO去繼承BaseDao,這一系列的DAO類便是數據持久層 class UserDao extends BaseDao{} class PersonDao extends BaseDao{} class EmployeeDao extends BaseDao{} //我們還會有一系列這樣的servlet,他們通常依賴於各個Dao類,這一系列servlet便是我們的業務層 class LoginServlet extends HttpServlet{} class LoginOutServlet extends HttpServlet{} class RegisterServlet extends HttpServlet{} //我們通常還會有HTML頁面或者JSP頁面,但是這個本次不在考慮範圍內,這便是表示層。
以上是我們小型WEB項目大體的結構,可以看到LZ寫了三個Servlet,沒有寫具體的實現到底如何,但是不難猜測,三個servlet的功能分別是進行登錄,註銷,以及註冊新用戶的功能。我們的servlet一般都是繼承自HttpServlet,因為我們在web.xml配置servlet時,所寫入的Class需要實現servlet接口,而我們通常采用的傳輸協議都是HTTP,所以HttpServlet就是我們最好的選擇了,它幫我們完成了基本的實現。
但是這樣我們有很多限制,比如我們一個servlet一般只能負責一個單一的業務邏輯,因為我們所有的業務邏輯通常情況下都集中在doPost這樣一個方法當中,可以想象下隨著業務的增加,我們的servlet數量會高速增加,這樣不僅項目的類會繼續增加,最最惡心的是,我們每添加一個servlet就要在web.xml裏面寫一個servlet配置。
但是如果我們讓一個Servlet負責多種業務邏輯的話,那我們需要在doPost方法中加入很多if判斷,去判斷當前的操作。
比如我們將上述三個servlet合一的話,你會在doPost出現以下代碼。
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //我們加入一個操作的參數,來讓servlet做出不同的業務處理 String operation = req.getParameter("operation"); if (operation.equals("login")) { System.out.println("login"); }else if (operation.equals("register")) { System.out.println("register"); }else if (operation.equals("loginout")) { System.out.println("loginout"); }else { throw new RuntimeException("invalid operation"); } }
這實在是非常爛的代碼,因為每次你新加一個操作,都要修改doPost這個方法,而且多個業務邏輯都集中在這一個方法當中,會讓代碼很難維護與擴展,最容易想到的就是下列做法。(小提示:如果你的項目中出現了這種代碼結構,請務必想辦法去掉它,你完全可以盡量忘掉Java裏還有elseif和swich)
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //我們加入一個操作的參數,來讓servlet做出不同的業務處理 String operation = req.getParameter("operation"); if (operation.equals("login")) { login(); }else if (operation.equals("register")) { register(); }else if (operation.equals("loginout")) { loginout(); }else { throw new RuntimeException("invalid operation"); } } private void login(){ System.out.println("login"); } private void register(){ System.out.println("register"); } private void loginout(){ System.out.println("loginout"); }
這樣會比第一種方式好一點,一個方法太長,實在不是什麽好征兆,等到你需要修改這部分業務邏輯的時候,你就會後悔你當初的寫法了,如果這段代碼不是親手寫的,那請你放心的在心中吐糟吧,因為這實在不是一個合格的程序員應該寫出的程序。
雖然我們已經將各個單一的業務邏輯拆分成方法,但這依然是違背單一原則這個小蘿莉的,因為我們的servlet應該只是處理業務邏輯,而不應該還要負責與業務邏輯不相關的處理方法定位這樣的責任,這個責任應該交給請求方,原本在三個servlet分別處理登陸,註銷和註冊的時候,其實就是這樣的,作為請求方,只要是請求LoginServlet,就說明請求的人是要登陸,處理這個請求的servlet不需要再出現有關判斷請求操作的代碼。
所以我們需要想辦法把判斷的業務邏輯交給請求方去處理,回想下struts2的做法,我們可以簡單模擬下struts2的做法。相信不少同學應該都用過struts2,那麽你肯定對以下配置很熟悉。
<filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
這是struts2最核心的filter,它的任務就是分派各個請求,根據請求的URL地址,去找到對應的處理該請求的Action。
我們來模擬一個分配請求的過濾器,它的任務就是根據用戶的請求去產生響應的servlet處理請求,而這些servlet其實就是上面的例子當中的productA和productB這類的角色,也就是具體的產品,而它們實現的接口正是Servlet這個抽象的產品接口。
我們用這個過濾器來消除servlet在web.xml的配置,幫我們加快開發的速度,我們寫出如下filter。
package com.web.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import com.web.factory.ServletFactory; //用來分派請求的filter public class DispatcherFilter implements Filter{ private static final String URL_SEPARATOR = "/"; private static final String SERVLET_PREFIX = "servlet/"; private String servletName; public void init(FilterConfig filterConfig) throws ServletException {} public void destroy() {} public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain) throws IOException, ServletException { parseRequestURI((HttpServletRequest) servletRequest); //這裏為了體現我們本節的重點,我們采用一個工廠來幫我們制造Action if (servletName != null) { //這裏使用的正是簡單工廠模式,創造出一個servlet,然後我們將請求轉交給servlet處理 Servlet servlet = ServletFactory.createServlet(servletName); servlet.service(servletRequest, servletResponse); }else { filterChain.doFilter(servletRequest, servletResponse); } } //負責解析請求的URI,我們約定請求的格式必須是/contextPath/servlet/servletName //不要懷疑約定的好處,因為LZ一直堅信一句話,約定優於配置 private void parseRequestURI(HttpServletRequest httpServletRequest){ String validURI = httpServletRequest.getRequestURI().replaceFirst(httpServletRequest.getContextPath() + URL_SEPARATOR, ""); if (validURI.startsWith(SERVLET_PREFIX)) { servletName = validURI.split(URL_SEPARATOR)[1]; } } }
這個filter需要在web.xml中加入以下配置,這個不多做介紹,直接貼上來。
<filter> <filter-name>dispatcherFilter</filter-name> <filter-class>com.web.filter.DispatcherFilter</filter-class> </filter> <filter-mapping> <filter-name>dispatcherFilter</filter-name> <url-pattern>/servlet/*</url-pattern> </filter-mapping>
LZ在filter中稍微加入了一些註釋,由於本章的重點是簡單工廠模式,所以我們這裏突出我們本章的主角,使用簡單工廠來創造servlet去處理客戶的請求,當然如果你是一個JAVA造詣比較深的程序猿,出於好奇進來一觀,或許會對這種簡單工廠模式的處理方式不屑一顧,不過我們不能偏離主題,我們的目的不是模擬一個struts2,而是介紹簡單工廠。
下面給出我們的主角,我們的servlet工廠,它就相當於上面的Creator。
package com.web.factory; import javax.servlet.Servlet; import com.web.exception.ServletException; import com.web.servlet.LoginServlet; import com.web.servlet.LoginoutServlet; import com.web.servlet.RegisterServlet; public class ServletFactory { private ServletFactory(){} //一個servlet工廠,專門用來生產各個servlet,而我們生產的依據就是傳入的servletName, //這個serlvetName由我們在filter截獲,傳給servlet工廠。 public static Servlet createServlet(String servletName){ if (servletName.equals("login")) { return new LoginServlet(); }else if (servletName.equals("register")) { return new RegisterServlet(); }else if (servletName.equals("loginout")) { return new LoginoutServlet(); }else { throw new ServletException("unknown servlet"); } } }
看到這裏,是不是有點感覺了呢,我們一步一步去消除servlet的XML配置的過程,其實就是在慢慢的寫出一個簡單工廠模式,只是在這之中,抽象的產品接口是現成的,也就是Servlet接口。
雖說這些個elseif並不是好代碼的征兆,不過這個簡單工廠最起碼幫我們解決了惡心的xml配置,說起來也算功不可沒。
現在我們可以請求/contextPath/servlet/login來訪問LoginServlet,而不再需要添加web.xml的配置,雖說這麽做,我們對修改是開放的,因為每增加一個servlet,我們都需要修改工廠類,去添加一個if判斷,但是LZ個人還是覺得我寧可寫if,也不想去copy那個當初讓我痛不欲生的xml標簽,雖說我剛才還說讓你忘掉elseif,我說過嗎?好吧。。我說過,但是這只是我們暫時的做法,我們可以有很多種做法去消除掉這些elseif。
簡單工廠是設計模式當中相對比較簡單的模式,它甚至都沒資格進入GOF的二十三種設計模式,所以可見它多麽卑微了,但就是這麽卑微的一個設計模式,也能真正的幫我們解決實際當中的問題,雖說這種解決一般只能針對規模較小的項目。
寫到這裏,簡單工廠模式當中出現的角色,已經很清晰了。我們上述簡單工廠當中設計到的類就是Servlet接口,ServletFactory以及各種具體的LoginServlet,RegisterServlet等等。
總結起來就是一個工廠類,一個產品接口(其實也可以是一個抽象類,甚至一個普通的父類,但通常我們覺得接口是最穩定的,所以基本不需要考慮普通父類的情況),和一群實現了產品接口的具體產品,而這個工廠類,根據傳入的參數去創造一個具體的實現類,並向上轉型為接口作為結果返回。
我們在這裏將上述穿插的簡單工廠模式抽離出來,註釋中有LZ個人的見解,幫助各位理解。
//相當於簡單工廠模式中的產品接口 interface Servlet{} //相當於簡單工廠模式中的抽象父類產品。 //註意,簡單工廠在網絡上的資料大部分為了簡單容易理解都是只規劃了一個產品接口,但這不代表就只能有一個,設計模式的使用要靈活多變。 class HttpServlet implements Servlet{} //具體的產品 class LoginServlet extends HttpServlet{} class RegisterServlet extends HttpServlet{} class LoginoutServlet extends HttpServlet{} //產品工廠 public class ServletFactory { private ServletFactory(){} //典型的創造產品的方法,一般是靜態的,因為工廠不需要有狀態。 public static Servlet createServlet(String servletName){ if (servletName.equals("login")) { return new LoginServlet(); }else if (servletName.equals("register")) { return new RegisterServlet(); }else if (servletName.equals("loginout")) { return new LoginoutServlet(); }else { throw new RuntimeException(); } } }
上面LZ已經將剛才的過程給抽離出來,各位可以對比下標準的簡單工廠代碼,就會發現它們其實是一模一樣的設計方式。
為了更加方便各位的對比,LZ這裏給出上面JAVA代碼的類圖。
上面便是我們例子當中有關Servlet創建時,出現的簡單工廠模式類圖,各位可以和第一次的標準類圖對比一下,它們的設計都是一樣的。
其實我們針對創建Servlet實例這一部分邏輯的控制依舊有很多很多的優化余地,但是限於本章介紹的內容,所以我們就適可而止。
LZ覺得想簡單工廠這種沒有什麽技術上的難度,純粹是依照一些業務場景而出現的設計模式,LZ就必須要創造出一個比較真實的業務場景或者現實中的例子,才能更好的詮釋。所以本次LZ先拿出了我們經常做的WEB項目,以後LZ也會盡量舉一些實際應用的例子。
設計模式之 簡單工廠模式詳解