淺談簡單工廠模式、工廠方法模式、抽象工廠模式
淺談簡單工廠模式、工廠方法模式、抽象工廠模式
一、概述
簡單說說對這幾個設計模式的理解。名字相近,一幾度弄不清這都是啥跟啥。
都叫xx工廠模式,顧名思義,工廠是要生產產品(在程式設計中就是類的例項物件)的。而這三種模式都是生產型模式,區別在於生產例項的方式、時機以及所能夠達到程式設計原則約束、靈活性不同。
二、前情須知
上一段如何的最後一句話“所能夠達到程式設計原則約束、靈活性不同”怎麼理解呢?這還得說說程式設計原則的事。這裡我們著重關注一下“開放封閉”原則,該原則強調要能夠對現有軟體進行功能上的進行拓展,但不允許或者儘量少修改原有程式碼。
那麼,假如你是個有心人,這裡應該就去思考——為什麼要遵守這一原則呢
說到程式設計原則,插一句。當初我懵懵懂懂,問我的領導——程式設計的規範、原則很多,但是咱們在做東西的時候也沒記得都遵守了啊。領導是這樣回答我的——在時間足夠,人力足夠,資金足夠的條件下,我可以做得要多優雅有多優雅。但是現實情況往往是,時間、人力、資金等一系列資源都不夠的情況下去搞開發,那就只能是有多少資源,做成什麼樣了。
三、從初學者Style說起
還以上述情景為例,如果一個初學者來編寫這個薪資管理系統,那麼八成會弄成這個樣子:
package origin; public class Salary { public static void main(String[]args) { // 員工身份 String identity = "實習生"; // 稅前工資(實際情況值裡應該是一個傳入值) int beforeTaxSalary = 17000; // 經過“薪資管理系統”的計算得到實際薪資 double realSalary = calculateSalary(identity,beforeTaxSalary); System.out.println(realSalary); } /** * 根據員工身份和稅前工資計算應發工資 * * @param identity * 員工身份(對應薪資計算方法) * @param beforeTaxSalary * 稅前工資 * @return 實際工資 */ private static double calculateSalary(Stringidentity, int beforeTaxSalary) { // 如果是實習生 if (identity.equals("實習生")) { // 實習生的薪資演算法 return 0.8 *beforeTaxSalary; } else if (identity.equals("正式員工")) { return beforeTaxSalary; } return beforeTaxSalary; } }
觀看上述“初學者Style”的程式碼,我們會發現,如果要進行功能拓展(比如新增一個臨時工薪資計算方式),那麼我們小將上述程式碼檔案中的calculateSalary方法開啟,然後在裡面繼續寫else if語句。這就印證了我在文章的第二部分前情須知裡說說的,萬一讓一個心術不正的人去開發維護拓展功能,那將是多麼危險的一件事呢。
四、簡單工廠模式遵守了開放原則
下面我們來看看簡單工廠模式是怎麼解決上述危險問題的。
簡單工廠抽象出了每種薪資計算方式的主要功能——根據員工身份和稅前薪資計算應發工資。所以將薪資計算方式都分割開來寫在單獨的檔案中,這些檔案都實現抽象即可。之後每次進行功能拓展,只要新增實現了該抽象的類檔案即可。繪製成UML圖就是下面這樣:
Ps:該模式只遵循了開放原則,並不遵循封閉原則。
以下是程式碼實現:
package factory.simple;
/**
* 薪資計算
*
* @author lxp
*
*/
public abstract class AbstractSalaryCalculation {
/**
* 稅前薪資
*/
private int beforeTaxSalary;
/**
* 薪資計算方法
*
* @return
*/
public abstract double calculateSalary();
public AbstractSalaryCalculation(int beforeTaxSalary) {
this.beforeTaxSalary =beforeTaxSalary;
}
public int getBeforeTaxSalary() {
return beforeTaxSalary;
}
public void setBeforeTaxSalary(int beforeTaxSalary) {
this.beforeTaxSalary =beforeTaxSalary;
}
}
package factory.simple;
/**
* 正式員工的薪資計算類
*
* @author lxp
*
*/
public class EmployeeCalculationextends AbstractSalaryCalculation {
public EmployeeCalculation(int beforeTaxSalary) {
super(beforeTaxSalary);
}
/**
* 正式員工的薪資計算方式
*/
@Override
public double calculateSalary() {
return this.getBeforeTaxSalary();
}
}
package factory.simple;
/**
* 實習生的薪資計算類
*
* @author lxp
*
*/
public class PracticeCalculationextends AbstractSalaryCalculation {
public PracticeCalculation(int beforeTaxSalary) {
super(beforeTaxSalary);
}
/**
* 實習生的薪資計算方式
*/
@Override
public double calculateSalary() {
return 0.8 *this.getBeforeTaxSalary();
}
}
package factory.simple;
/**
* 簡單工廠,用來生成薪資計算方式例項。
*
* @author lxp
*
*/
public class SimpleFactory {
private Stringidentify;
public static AbstractSalaryCalculation getSalaryCalculation(Stringidentify, int beforeTaxSalary) {
// 此處不用switch,因為1.6以下不相容。
AbstractSalaryCalculation salaryCalculation =null;
if (identify.equals("實習生")) {
salaryCalculation = new PracticeCalculation(beforeTaxSalary);
} else if (identify.equals("正式員工")) {
salaryCalculation = new EmployeeCalculation(beforeTaxSalary);
}
return salaryCalculation;
}
}
package factory.simple.test;
import factory.simple.AbstractSalaryCalculation;
import factory.simple.SimpleFactory;
public class TestSimpleFactory {
public static void main(String[] args) {
String practiceIdentify = "實習生";
String employeeIdentify = "正式員工";
int salary = 3000;
// 不同的身份,工廠建立的計算方式例項不同,結果也不一樣。
AbstractSalaryCalculation salaryCalculation1 = SimpleFactory.getSalaryCalculation(practiceIdentify, salary);
System.out.println(salaryCalculation1.calculateSalary());
// 如果在有新的計算方式,只需新增實現了AbstractSalaryCalculation的類檔案,然後修改主函式即可。
AbstractSalaryCalculation salaryCalculation2 = SimpleFactory.getSalaryCalculation(employeeIdentify, salary);
System.out.println(salaryCalculation2.calculateSalary());
}
}
五、工廠方法模式遵從了開放-封閉原則
在上一節中說道,簡單工廠模式遵從了開放原則,但沒有遵從封閉原則,不能算是真正意義上解決了我們之前提到的問題。那麼接下來進化到工廠方法模式,它我們來看看工廠方法模式是如何徹底解決該問題的呢?呵呵,我個人覺得這種方法真是……唉。
先來分析一下為啥每拓展一項新的功能就要改程式碼呢?因為無論是“初學者style”還是後來的“簡單工廠模式”,都需要在類檔案裡進行一些判斷,就是if else的那些東西。每沒新新增一種計算方式,就需要開啟這些檔案,新增新的ifelse分支。所以造成了不遵從封閉原則。那麼工廠方法模式其實就是將簡單工廠進行更高度的抽象,拆分成一個個的生成薪資計算方式的工廠,具體使用哪一個,就讓使用者(也就是客戶端)自己去選取好了。這樣一來,就徹底遵從了開放-封閉原則。
當然啦,如此一來也引入了別的麻煩——每拓展一個新的功能,就不僅僅需要新增該功能的類檔案,還需要新增生產該類的工廠的類檔案。UML圖如下:
那麼相應的,程式碼如下:
package factory.method.factory;
import factory.method.calculation.AbstractSalaryCalculation;
/**
* 生產工廠的介面
*
* @author lxp
*
*/
public interface ICreateSalaryCalculation {
AbstractSalaryCalculation createSalaryCalculation(int beforeTaxSalary);
}
package factory.method.factory;
import factory.method.calculation.AbstractSalaryCalculation;
import factory.method.calculation.PracticeCalculation;
public class CreatePracticeCalculationFactory implements ICreateSalaryCalculation {
@Override
public AbstractSalaryCalculation createSalaryCalculation(int beforeTaxSalary) {
AbstractSalaryCalculation calculation = new PracticeCalculation(beforeTaxSalary);
return calculation;
}
}
package factory.method.factory;
import factory.method.calculation.AbstractSalaryCalculation;
import factory.method.calculation.EmployeeCalculation;
public class CreateEmployeeCalculationFactory implements ICreateSalaryCalculation {
@Override
public AbstractSalaryCalculation createSalaryCalculation(int beforeTaxSalary) {
AbstractSalaryCalculation calculation = new EmployeeCalculation(beforeTaxSalary);
return calculation;
}
}
package factory.method.calculation;
/**
* 薪資計算
*
* @author lxp
*
*/
public abstract class AbstractSalaryCalculation {
/**
* 稅前薪資
*/
private int beforeTaxSalary;
/**
* 薪資計算方法
*
* @return
*/
public abstract double calculateSalary();
public AbstractSalaryCalculation(int beforeTaxSalary) {
this.beforeTaxSalary =beforeTaxSalary;
}
public int getBeforeTaxSalary() {
return beforeTaxSalary;
}
public void setBeforeTaxSalary(int beforeTaxSalary) {
this.beforeTaxSalary =beforeTaxSalary;
}
}
package factory.method.calculation;
/**
* 實習生的薪資計算類
*
* @author lxp
*
*/
public class PracticeCalculationextends AbstractSalaryCalculation {
public PracticeCalculation(int beforeTaxSalary) {
super(beforeTaxSalary);
}
/**
* 實習生的薪資計算方式
*/
@Override
public double calculateSalary() {
return 0.8 *this.getBeforeTaxSalary();
}
}
package factory.method.calculation;
/**
* 正式員工的薪資計算類
*
* @author lxp
*
*/
public class EmployeeCalculationextends AbstractSalaryCalculation {
public EmployeeCalculation(int beforeTaxSalary) {
super(beforeTaxSalary);
}
/**
* 正式員工的薪資計算方式
*/
@Override
public double calculateSalary() {
return this.getBeforeTaxSalary();
}
}
package factory.method.calculation.test;
import factory.method.calculation.AbstractSalaryCalculation;
import factory.method.factory.CreateEmployeeCalculationFactory;
import factory.method.factory.ICreateSalaryCalculation;
public class TestMethodFactory {
public static void main(String[] args) {
// 例項化建立薪資計算方式的工廠,如果想拓展功能,新增一種薪資計算方式,只要新增實現了ICreateSalaryCalculation介面的檔案和實現了AbstractSalaryCalculation的檔案即可。客戶端自己決定要採取的計算方法。
ICreateSalaryCalculation factory = new CreateEmployeeCalculationFactory();
// 工廠創建出薪資計算方式
AbstractSalaryCalculation salaryCalculation = factory.createSalaryCalculation(5000);
// 計算薪資
double realSalary = salaryCalculation.calculateSalary();
System.out.println(realSalary);
}
}
其實,工廠方法這種設計思想很普遍,像我們常用的軟體中的外掛,就是給開發外掛的廠商留下一個介面,這個接口裡定義了可以拓展的功能的抽象,然後各個廠商實現這個介面,就可以將自己的功能整合進去啦~\(≧▽≦)/~然後還有就是大家常用的框架裡是不是經常出現XXXFactory之類的呢?嘻嘻,也是啦。
六、抽象工廠模式遵從了開放-封閉原則且不需要再客戶端進行判斷
話說上一小節中提到的模式不過是把判斷挪到了客戶端,讓客戶端進行判斷到底該用那種方式計算薪資。
寫到這裡我才知道,原來這三個設計模式名字中都帶有“工廠”,但是抽象工廠模式與前兩個並沒有什麼太的的關係(我個人是這麼認為的),不過抽象工廠模式通過“反射+配置檔案”的確徹底遵從了開放-封閉原則,只不過你需要多一些開銷啦——開發功能類、開發對應的工廠類、新增配置。
為了全文的連貫性,我們還是用上面的例子。不過這回換成了不是一家公司,而是多家公司的薪資計算了。老規矩,我們先上UML圖:
程式碼如下:
package factory.abstracted.interfaze;
/**
* 實習生的薪資計算介面
*
* @author 劉欣普
*
*/
public interface IPracticeCalculation {
double calculatePracticeSalary(int beforeTaxSalary);
}
package factory.abstracted.interfaze;
/**
* 正式員工的薪資計算介面
*
* @author 劉欣普
*
*/
public interface IEmployeeCalculation {
double calculateEmployeeSalary(int beforeTaxSalary);
}
package factory.abstracted.calculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public class CPracticeCalculationimplements IPracticeCalculation {
@Override
public double calculatePracticeSalary(int beforeTaxSalary) {
return beforeTaxSalary * 0.8;
}
}
package factory.abstracted.calculation;
import factory.abstracted.interfaze.IEmployeeCalculation;
public class CEmployeeCalculationimplements IEmployeeCalculation {
@Override
public double calculateEmployeeSalary(int beforeTaxSalary) {
return beforeTaxSalary;
}
}
package factory.abstracted.calculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public class BPracticeCalculationimplements IPracticeCalculation {
@Override
public double calculatePracticeSalary(int beforeTaxSalary) {
return beforeTaxSalary * 0.8;
}
}
package factory.abstracted.calculation;
import factory.abstracted.interfaze.IEmployeeCalculation;
public class BEmployeeCalculationimplements IEmployeeCalculation {
@Override
public double calculateEmployeeSalary(int beforeTaxSalary) {
return beforeTaxSalary;
}
}
package factory.abstracted.calculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public class APracticeCalculationimplements IPracticeCalculation {
/**
* A公司的實習生薪資
*/
@Override
public double calculatePracticeSalary(int beforeTaxSalary) {
return beforeTaxSalary * 0.8;
}
}
package factory.abstracted.calculation;
import factory.abstracted.interfaze.IEmployeeCalculation;
public class AEmployeeCalculationimplements IEmployeeCalculation {
/**
* A公司的正式員工薪資
*/
@Override
public double calculateEmployeeSalary(int beforeTaxSalary) {
return beforeTaxSalary;
}
}
package factory.abstracted.bean;
import factory.abstracted.interfaze.IEmployeeCalculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public abstract class AbstractCompany {
/**
* 建立計算實習生薪資的例項
*
* @return
*/
public abstract IPracticeCalculation createPracticeCalculation();
/**
* 建立計算正式員工薪資的例項
*
* @return
*/
public abstract IEmployeeCalculation createEmployeeCalculation();
}
package factory.abstracted.bean;
import factory.abstracted.calculation.AEmployeeCalculation;
import factory.abstracted.calculation.APracticeCalculation;
import factory.abstracted.interfaze.IEmployeeCalculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public class ACompanyextends AbstractCompany {
@Override
public IPracticeCalculation createPracticeCalculation() {
return new APracticeCalculation();
}
@Override
public IEmployeeCalculation createEmployeeCalculation() {
return new AEmployeeCalculation();
}
}
package factory.abstracted.bean;
import factory.abstracted.calculation.BEmployeeCalculation;
import factory.abstracted.calculation.BPracticeCalculation;
import factory.abstracted.interfaze.IEmployeeCalculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public class BCompanyextends AbstractCompany {
@Override
public IPracticeCalculation createPracticeCalculation() {
return new BPracticeCalculation();
}
@Override
public IEmployeeCalculation createEmployeeCalculation() {
return new BEmployeeCalculation();
}
}
package factory.abstracted.bean;
import factory.abstracted.calculation.CEmployeeCalculation;
import factory.abstracted.calculation.CPracticeCalculation;
import factory.abstracted.interfaze.IEmployeeCalculation;
import factory.abstracted.interfaze.IPracticeCalculation;
public class CCompanyextends AbstractCompany {
@Override
public IPracticeCalculation createPracticeCalculation() {
return new CPracticeCalculation();
}
@Override
public IEmployeeCalculation createEmployeeCalculation() {
return new CEmployeeCalculation();
}
}
package factory.abstracted.test;
import factory.abstracted.bean.ACompany;
import factory.abstracted.bean.AbstractCompany;
public class TestAbstractedFactory {
public static void main(String[]args) {
/**
*注意下面這一行程式碼,當需要切換“產品系列”的時候,直接換掉這裡就可以了。
* 注意,利用配置檔案可以不需要修改程式碼,寫成類似於
* AbstractCompany Company = Class.forName("XXX");此處的“XXX”取自配置檔案
*/
AbstractCompany Company = new ACompany();
System.out.println("該公司的正式員工工資是" +Company.createEmployeeCalculation().calculateEmployeeSalary(3000));
System.out.println("該公司的實習生 工資是" +Company.createPracticeCalculation().calculatePracticeSalary(3000));
}
}
抽象工廠模式的有點是易於切換產品系類,但是缺點是當我需要新增新的“產品(薪資計算方式,例如UML圖中畫的臨時工薪資計算方式)”的時候,需要新增的不僅僅是“產品”,還有生產該種產品的“工廠”。如果在客戶端想呼叫,需要在配置檔案中新增該工廠類資訊以便反射獲取例項。
其實該設計模式很常見,我們常用的hibernate框架就運用了這種設計模式。Hibernate運用配置決定使用哪種資料庫,又運用配置將類的DAO例項關聯。
ps:如果圖片看不清楚,請檢視原版。