1. 程式人生 > >(三)簡單工廠模式詳解

(三)簡單工廠模式詳解

           作者:zuoxiaolong8810(左瀟龍),轉載請註明出處。

            上一章我們著重討論了代理模式,以及其實現原理,相信如果你看完了整篇博文,應該就對代理模式很熟悉了。

            本章我們討論簡單工廠模式,LZ當初不小心誇下海口說不和網路上傳播的教學式模式講解雷同,所以現在感覺寫一篇博文壓力頗大。

            如何來介紹簡單工廠呢,LZ著實費了不少心思,這個模式本身不復雜,但其實越是不復雜的模式越難寫出特點,因為它太簡單。

            為了便於各位看後面例子的時候容易理解,LZ這裡先給出引自其它地方的簡單工廠模式的定義。

            定義:從設計模式的型別上來說,簡單工廠模式是屬於建立型模式,又叫做靜態工廠方法(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也會盡量舉一些實際應用的例子。

              好了,本期的簡單工廠模式就到這裡吧,感謝各位的收看!

              下期預告,能不能先不預告。。。