如今比較好的工具主要有Enterprise Architect和Visual Paradigm等大型UML設計工具。他們都美觀使用,功能強大,支持 UML 到各類編程語言的正向和逆向生成。java
Enterprise Architect的IDE整合插件比較舊,並且只支持32位的eclipse。在整合的過程當中特別容易出現問題。因此我選用Visual Paradigm做爲開發工具。數據庫
Visual Paradigm還支持鼠標手勢,不再用爲找不到控件發愁了。編程
繼承關係:繼承指的是一個類(稱爲子類、子接口)繼承另外的一個類(稱爲父類、父接口)的功能,並能夠增長它本身的新功能的能力。java中用關鍵字extends明確標識。設計模式
具體表現:框架
UML類圖:在UML的類圖中,繼承用一條帶空心三角箭頭的實線表示,從子類指向父類,或者子接口指向父接口。dom
實現關係:實現指的是一個class類實現interface接口(能夠是多個)的功能,實現是類與接口之間最多見的關係。Java中此類關係經過關鍵字implements明確標識eclipse
具體表現:編程語言
UML類圖:在UML的類圖中,實現用一條帶空心三角箭頭的虛線表示,從類指向實現的接口。編輯器
依賴關係:其中一個模型元素是獨立的,另外一個模型元素不是獨立的,它依賴於獨立的模型元素,若是獨立的模型元素改變了,將影響依賴於它的模型元素。函數
具體表現:
UML類圖:在UML的類圖中,用帶箭頭的虛線鏈接有依賴關係的兩個類,箭頭指向獨立的類
關聯關係:關聯體現的是兩個類之間語義級別的一種強依賴關係,好比我和個人朋友,這種關係比依賴更強、不存在依賴關係的偶然性、關係也不是臨時性的,通常是長期性的,並且雙方的關係通常是平等的。關聯能夠是單向、雙向的。
具體表現:
UML類圖:在UML類圖設計中,關聯關係用由關聯類A指向被關聯類B的帶箭頭實線表示,在關聯的兩端能夠標註關聯雙方的角色和多重性標記。
依賴關係和關聯關係的區別(有待商榷):
語義上:關聯關係是一種更強的依賴關係
代碼上:
- 關聯關係是做爲成員變量。
- 依賴關係每每是一個類做爲參數出如今另外一個類方法的參數表中,利用該參數發消息。
聚合關係: 聚合是關聯關係的一種特例,它體現的是總體與部分的關係,即has-a的關係。此時總體與部分之間是可分離的,它們能夠具備各自的生命週期,部分能夠屬於多個總體對象,也能夠爲多個總體對象共享。
具體表現:和關聯關係是一致的,只能從語義級別來區分。
UML類圖:在UML類圖設計中,聚合關係以空心菱形加實線箭頭表示。
組合關係:組合也是關聯關係的一種特例,它體現的是一種contains-a的關係,這種關係比聚合更強,也稱爲強聚合。它一樣體現總體與部分間的關係,但此時總體與部分是不可分的,總體的生命週期結束也就意味着部分的生命週期結束,「同生共死」。
具體表現:和關聯關係是一致的,只能從語義級別來區分。
UML類圖:在UML類圖設計中,組合關係以實心菱形加實線箭頭表示。
Sunny軟件公司欲基於Java語言開發一套圖表庫,該圖表庫能夠爲應用系統提供各類不一樣外觀的圖表,例如柱狀圖、餅狀圖、折線圖等。Sunny軟件公司圖表庫設計人員但願爲應用系統開發人員提供一套靈活易用的圖表庫,並且能夠較爲方便地對圖表庫進行擴展,以便可以在未來增長一些新類型的圖表。
初始設計方案,將全部圖表的實現代碼封裝在一個Chart類中,其框架代碼以下所示:
class Chart { private String type; //圖表類型 public Chart(Object[][] data, String type) { this.type = type; if (type.equalsIgnoreCase("histogram")) { //初始化柱狀圖 } else if (type.equalsIgnoreCase("pie")) { //初始化餅狀圖 } else if (type.equalsIgnoreCase("line")) { //初始化折線圖 } } public void display() { if (this.type.equalsIgnoreCase("histogram")) { //顯示柱狀圖 } else if (this.type.equalsIgnoreCase("pie")) { //顯示餅狀圖 } else if (this.type.equalsIgnoreCase("line")) { //顯示折線圖 } } }
初始方案的問題分析:
Chart
類包含多個圖表的功能,違反單一職責原則,不利於類的重用和維護。Chart
類,違反開閉原則,不利於類的重用和維護。if else
語句,代碼至關冗長,代碼越長,閱讀難度、維護難度和測試難度也越大;並且大量條件語句的存在還將影響系統的性能,程序在執行過程當中須要作大量的條件判斷。new
關鍵字來直接建立Chart對象,Chart
類與客戶端類耦合度較高,對象的建立和使用沒法分離。Chart
對象以前可能還須要進行大量初始化設置,例如設置柱狀圖的顏色、高度等,若是在Chart
類的構造函數中沒有提供一個默認設置,那就只能由客戶端來完成初始設置,這些代碼在每次建立Chart
對象時都會出現,致使代碼的重複。簡單工廠模式並不屬於GoF 23個經典設計模式,但一般將它做爲學習其餘工廠模式的基礎,它的設計思想很簡單,其基本流程以下:
簡單工廠模式(Simple Factory Pattern):定義一個工廠類,它能夠根據參數的不一樣返回不一樣類的實例,被建立的實例一般都具備共同的父類。由於在簡單工廠模式中用於建立實例的方法是靜態(static)方法,所以簡單工廠模式又被稱爲靜態工廠方法(Static Factory Method)模式,它屬於類建立型模式。
簡單工廠模式的要點在於:當你須要什麼,只須要傳入一個正確的參數,就能夠獲取你所須要的對象,而無須知道其建立細節。簡單工廠模式結構比較簡單,其核心是工廠類的設計,其結構如圖所示:
在簡單工廠模式結構圖中包含以下幾個角色:
在簡單工廠模式中,客戶端經過工廠類來建立一個產品類的實例,而無須直接使用new關鍵字來建立對象,它是工廠模式家族中最簡單的一員。
在使用簡單工廠模式時,首先須要對產品類進行重構,不能設計一個一應俱全的產品類,而需根據實際狀況設計一個產品層次結構,將全部產品類公共的代碼移至抽象產品類,並在抽象產品類中聲明一些抽象方法,以供不一樣的具體產品類來實現,典型的抽象產品類代碼以下所示:
abstract class Product{ //公有方法,一致的表現 public void method(){ //實現共同有的方法 } //不一樣的產品有着不一樣的表現 public void methodDiff(){ //在具體產品中實現 } }
在具體產品類中實現了抽象產品類中聲明的抽象業務方法,不一樣的具體產品類能夠提供不一樣的實現,典型的具體產品類代碼以下所示:
public class ConcreteProductA extends Product{ //特有表現的方法實現 public void methodDiff(){ //在具體產品中實現 } } public class ConcreteProductB{ ////特有表現的方法實現 public void methodDiff(){ //在具體產品中實現 } }
簡單工廠模式的核心是工廠類,在沒有工廠類以前,客戶端通常會使用new關鍵字來直接建立產品對象,而在引入工廠類以後,客戶端能夠經過工廠類來建立產品,在簡單工廠模式中,工廠類提供了一個靜態工廠方法供客戶端使用,根據所傳入的參數不一樣能夠建立不一樣的產品對象,典型的工廠類代碼以下所示:
public class Factory{ public static Product craeteProduct(String type){ Product prodcut = null; if(type.equalsIgnoreCase("A")){ prodcut = new ConcreteProductA(); //設置初始化設置,例如本例中的顏色,高度 } else if(type.equalsIgnoreCase("B")){ prodcut = new ConcreteProductA(); //設置初始化設置,例如本例中的顏色,高度 } return prodcut; } }
在客戶端代碼中,咱們經過調用工廠類的工廠方法便可獲得產品對象,典型代碼以下所示:
public class Client{ public static void main(String[] args){ Prodcut product; prodcut = Factory.createProduct("A");//經過工廠建立產品類 a.method();//使用一致表現的方法 a.methodDiff();//使用表現不一樣的方法 } }
爲了將Chart類的職責分離,同時將Chart對象的建立和使用分離,Sunny軟件公司開發人員決定使用簡單工廠模式對圖表庫進行重構,重構後的結構如圖所示:
Chart
接口充當抽象產品類,其子類HistogramChart
、PieChart
和LineChart
充當具體產品類,ChartFactory
充當工廠類。完整代碼以下所示:
interface class Chart{ //展現的抽象方法 public void display(); } public class HistogramChart implements Chart{ //構造函數 public HistogramChart(){ System.out.println("建立HistogramChart!"); } public void display(){ //HistogramChart展現的具體實現 System.out.println("展現HistogramChart!"); } } public class LineChart implements Chart{ //構造函數 public LineChart(){ System.out.println("建立LineChart!"); } public void display(){ //LineChart 展現的具體實現 System.out.println("展現LineChart!"); } } public class PieChart implements Chart{ //構造函數 public PieChart(){ System.out.println("建立PieChart!"); } public void display(){ //PieChart 展現的具體實現 System.out.println("展現PieChart!"); } } //工廠 public class ChartFactory{ public static Chart getChart(String chartType){ Chart chart = null; if(chartType.equalsIgnoreCase("histograme")){ chart = new HistogramChart(); } if(chartType.equalsIgnoreCase("line")){ chart = new LineChart(); } if(chartType.equalsIgnoreCase("pie")){ chart = new PieChart(); } return chart; } }
客戶端:
public class Clinet { public static void main(String[] args) { Chart chart; chart = ChartFactory.getChart("histograme"); chart.display(); chart = ChartFactory.getChart("line"); chart.display(); chart = ChartFactory.getChart("pie"); chart.display(); } }
輸出:
建立HistogramChart! 展現HistogramChart! 建立LineChart! 展現LineChart! 建立PieChart! 展現PieChart!
Sunny軟件公司開發人員發如今建立具體Chart對象時,每更換一個Chart對象都須要修改客戶端代碼中靜態工廠方法的參數,客戶端代碼將要從新編譯,這對於客戶端而言,違反了「開閉原則」。
有沒有一種方法可以在不修改客戶端代碼的前提下更換具體產品對象呢?答案是確定的,下面將介紹一種經常使用的實現方式。
靜態工廠方法的參數存儲在XML
或properties
格式的配置文件中,以下config.xml
所示:
<?xml version="1.0"?> <config> <chartType>histogram</chartType> </config>
再經過一個工具類XMLUtil來讀取配置文件中的字符串參數,XMLUtil類的代碼以下所示:
import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.SAXException; import java.io.*; public class XMLUtil { //該方法用於從XML配置文件中提取圖表類型,並返回類型名 public static String getChartType() { try { //建立文檔對象 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File("config.xml")); //獲取包含圖表類型的文本節點 NodeList nl = doc.getElementsByTagName("chartType"); Node classNode = nl.item(0).getFirstChild(); String chartType = classNode.getNodeValue().trim(); return chartType; } catch(Exception e) { e.printStackTrace(); return null; } } }
客戶端調用以下:
class Client { public static void main(String args[]) { Chart chart; String type = XMLUtil.getChartType(); //讀取配置文件中的參數 chart = ChartFactory.getChart(type); //建立產品對象 chart.display(); } }
不難發現,在上述客戶端代碼中不包含任何與具體圖表對象相關的信息,若是須要更換具體圖表對象,只需修改配置文件config.xml,無須修改任何源代碼,符合 開閉原則 。
思考:在上述設計中,須要添加新的Chart的時候,仍是符合 開閉原則 的嗎?該怎麼修改?
簡單工廠模式的主要優勢以下:
簡單工廠模式的主要缺點以下:
適用場景 在如下狀況下能夠考慮使用簡單工廠模式:
使用簡單工廠模式設計一個能夠建立不一樣幾何形狀(如圓形、方形和三角形等)的繪圖工具,每一個幾何圖形都具備繪製draw()和擦除erase()兩個方法,要求在繪製不支持的幾何圖形時,提示一個UnSupportedShapeException。
簡單工廠設計類圖:
代碼實現以下:
public interface Shape { /** * 繪圖接口 */ public void draw(); /** * 擦除圖形接口 */ public void erase(); } public class Circle implements Shape { public Circle(){ } public void finalize() throws Throwable { } /** * 繪圖接口 */ public void draw(){ } /** * 擦除圖形接口 */ public void erase(){ } } ... public class UnSupportedShapeException extends Exception { public UnSupportedShapeException() { super(); } public UnSupportedShapeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public UnSupportedShapeException(String message, Throwable cause) { super(message, cause); } public UnSupportedShapeException(String message) { super(message); } public UnSupportedShapeException(Throwable cause) { super(cause); } } public class ShapeFactory { /** * * [[@param](http://my.oschina.net/u/2303379)](http://my.oschina.net/u/2303379) shapeType * [[@throws](http://my.oschina.net/throws)](http://my.oschina.net/throws) UnSupportedShapeException */ public static Shape getShape(String shapeType) throws UnSupportedShapeException { Shape shape; if (shapeType.equalsIgnoreCase("circle")) { shape = new Circle(); } else if (shapeType.equalsIgnoreCase("rectangle")) { shape = new Rectangle(); } else if (shapeType.equalsIgnoreCase("triangle")) { shape = new Triangle(); } else { throw new UnSupportedShapeException(); } return shape; } }
客戶端代碼:
public class Client { public static void main(String[] args) { Shape shape; try { shape = ShapeFactory.getShape("circle"); shape.draw(); shape.erase(); } catch (UnSupportedShapeException e) { e.printStackTrace(); } } }
簡單工廠模式雖然簡單,但存在一個很嚴重的問題。
當系統中須要引入新產品時,因爲靜態工廠方法經過所傳入參數的不一樣來建立不一樣的產品,這一定要修改工廠類的源代碼,將違背「開閉原則」,如何實現增長新產品而不影響已有代碼?
工廠方法模式應運而生。
日誌記錄器的設計。
Sunny軟件公司欲開發一個系統運行日誌記錄器(Logger),該記錄器能夠經過多種途徑保存系統的運行日誌,如經過文件記錄或數據庫記錄,用戶能夠經過修改配置文件靈活地更換日誌記錄方式。在設計各種日誌記錄器時,Sunny公司的開發人員發現須要對日誌記錄器進行一些初始化工做,初始化參數的設置過程較爲複雜,並且某些參數的設置有嚴格的前後次序,不然可能會發生記錄失敗。如何封裝記錄器的初始化過程並保證多種記錄器切換的靈活性是Sunny公司開發人員面臨的一個難題。
Sunny公司的開發人員經過對該需求進行分析,發現該日誌記錄器有兩個設計要點:
封裝日誌記錄器的初始化過程
,這些初始化工做較爲複雜,例如須要初始化其餘相關的類,還有可能須要讀取配置文件(例如鏈接數據庫或建立文件),致使代碼較長,若是將它們都寫在構造函數中,會致使構造函數龐大,不利於代碼的修改和維護;更換日誌記錄方式
,在客戶端代碼中須要提供一種靈活的方式來選擇日誌記錄器,儘可能在不修改源代碼的基礎上更換或者增長日誌記錄方式。Sunny公司開發人員最初使用簡單工廠模式對日誌記錄器進行了設計,初始結構如圖所示:
LoggerFactory充當建立日誌記錄器的工廠,提供了工廠方法createLogger()用於建立日誌記錄器,Logger是抽象日誌記錄器接口,其子類爲具體日誌記錄器。其中,工廠類LoggerFactory代碼片斷以下所示:
//日誌記錄器工廠 class LoggerFactory { //靜態工廠方法 public static Logger createLogger(String args) { if(args.equalsIgnoreCase("db")) { //鏈接數據庫,代碼省略 //建立數據庫日誌記錄器對象 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } else if(args.equalsIgnoreCase("file")) { //建立日誌文件 //建立文件日誌記錄器對象 Logger logger = new FileLogger(); //初始化文件日誌記錄器,代碼省略 return logger; } else { return null; } } }
在簡單工廠模式中只提供一個工廠類,該工廠類處於對產品類進行實例化的中心位置,它須要知道每個產品對象的建立細節,並決定什麼時候實例化哪個產品類。
簡單工廠模式**最大的缺點是當有新產品要加入到系統中時,必須修改工廠類,須要在其中加入必要的業務邏輯,這違背了「開閉原則」。**此外,在簡單工廠模式中,全部的產品都由同一個工廠建立,工廠類職責較重,業務邏輯較爲複雜,具體產品與工廠類之間的耦合度高,嚴重影響了系統的靈活性和擴展性,而工廠方法模式則能夠很好地解決這一問題。
在工廠方法模式中,咱們再也不提供一個統一的工廠類來建立全部的產品對象,而是針對不一樣的產品提供不一樣的工廠,系統提供一個與產品等級結構對應的工廠等級結構
。工廠方法模式定義以下:
工廠方法模式
(Factory Method Pattern):定義一個用於建立對象的接口,讓子類決定將哪個類實例化。工廠方法模式讓一個類的實例化延遲到其子類。工廠方法模式又簡稱爲工廠模式(Factory Pattern),又可稱做虛擬構造器模式(Virtual Constructor Pattern)或多態工廠模式(Polymorphic Factory Pattern)。工廠方法模式是一種類建立型模式。
工廠方法模式提供一個抽象工廠接口來聲明抽象工廠方法,而由其子類來具體實現工廠方法,建立具體的產品對象。工廠方法模式結構如圖:
在工廠方法模式結構圖中包含以下幾個角色:
與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠能夠是接口,也能夠是抽象類或者具體類
,其典型代碼以下所示:
interface Factory { public Product factoryMethod(); }
在抽象工廠中聲明瞭工廠方法但並未實現工廠方法,具體產品對象的建立由其子類負責,客戶端針對抽象工廠編程,可在運行時再指定具體工廠類,具體工廠類實現了工廠方法,不一樣的具體工廠能夠建立不一樣的具體產品,其典型代碼以下所示:
class ConcreteFactory implements Factory { public Product factoryMethod() { return new ConcreteProduct(); } }
在實際使用時,具體工廠類在實現工廠方法時除了建立具體產品對象以外,還能夠負責產品對象的初始化工做以及一些資源和環境配置工做,例如鏈接數據庫、建立文件等。
在客戶端代碼中,只需關心工廠類便可,不一樣的具體工廠能夠建立不一樣的產品,典型的客戶端類代碼片斷以下所示:
...... Factory factory; factory = new ConcreteFactory(); //可經過配置文件實現 Product product; product = factory.factoryMethod(); ......
能夠經過配置文件來存儲具體工廠類ConcreteFactory
的類名,更換新的具體工廠時無須修改源代碼,系統擴展更爲方便。
思考:工廠方法模式中的工廠方法可否爲靜態方法?爲何? 不能夠爲靜態方法。由於工廠方法模式的核心是引入抽象工廠角色,若是使用靜態方法,工廠不能在運行時肯定具體的工廠,影響系統的擴展性。
Sunny公司開發人員決定使用工廠方法模式來設計日誌記錄器,其基本結構如圖所示:
Logger
接口充當抽象產品,其子類FileLogger
和DatabaseLogger
充當具體產品,LoggerFactory
接口充當抽象工廠,其子類FileLoggerFactory
和DatabaseLoggerFactory
充當具體工廠。完整代碼以下所示:
//日誌記錄器接口:抽象產品 interface Logger { public void writeLog(); } //數據庫日誌記錄器:具體產品 class DatabaseLogger implements Logger { public void writeLog() { System.out.println("數據庫日誌記錄。"); } } //文件日誌記錄器:具體產品 class FileLogger implements Logger { public void writeLog() { System.out.println("文件日誌記錄。"); } } //日誌記錄器工廠接口:抽象工廠 interface LoggerFactory { public Logger createLogger(); } //數據庫日誌記錄器工廠類:具體工廠 class DatabaseLoggerFactory implements LoggerFactory { public Logger createLogger() { //鏈接數據庫,代碼省略 //建立數據庫日誌記錄器對象 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } } //文件日誌記錄器工廠類:具體工廠 class FileLoggerFactory implements LoggerFactory { public Logger createLogger() { //建立文件日誌記錄器對象 Logger logger = new FileLogger(); //建立文件,代碼省略 return logger; } }
客戶端測試:
class Client { public static void main(String args[]) { LoggerFactory factory; Logger logger; factory = new FileLoggerFactory(); //可引入配置文件實現 logger = factory.createLogger(); logger.writeLog(); } }
爲了讓系統具備更好的靈活性和可擴展性,Sunny公司開發人員決定對日誌記錄器客戶端代碼進行重構,使得能夠在不修改任何客戶端代碼的基礎上更換或增長新的日誌記錄方式。
在客戶端代碼中將再也不使用new關鍵字來建立工廠對象,而是將具體工廠類的類名存儲在配置文件(如XML文件)中,經過讀取配置文件獲取類名字符串,再使用Java的反射機制,根據類名字符串生成對象
。在整個實現過程當中須要用到兩個技術:Java反射機制與配置文件讀取。軟件系統的配置文件一般爲XML
文件,咱們可使用DOM
(Document Object Model)、SAX
(Simple API for XML)、StAX
(Streaming API for XML)等技術來處理XML文件。
Java反射
(Java Reflection)是指在程序運行時獲取已知名稱的類或已有對象的相關信息的一種機制,包括類的方法、屬性、父類等信息,還包括實例的建立和實例類型的判斷等。在反射中使用最多的類是Class
,Class類的實例表示正在運行的Java應用程序中的類和接口,其forName(String className)
方法能夠返回與帶有給定字符串名的類或接口相關聯的 Class對象,再經過Class對象的newInstance()
方法建立此對象所表示的類的一個新實例,即經過一個類名字符串獲得類的實例。如建立一個字符串類型的對象,其代碼以下:
Class clazz = Class.forName("String"); Object object = clazz.newInstance(); return object;
在JDK中還提供了java.lang.reflect
包,封裝了其餘與反射相關的類。
Sunny公司開發人員建立了以下XML格式的配置文件config.xml
用於存儲具體日誌記錄器工廠類類名:
<?xml version="1.0"?> <config> <className>FileLoggerFactory</className> </config>
爲了讀取該配置文件並經過存儲在其中的類名字符串反射生成對象,Sunny公司開發人員開發了一個名爲XMLUtil的工具類,其詳細代碼以下所示:
//工具類XMLUtil.java import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.SAXException; import java.io.*; public class XMLUtil { //該方法用於從XML配置文件中提取具體類類名,並返回一個實例對象 public static Object getBean() { try { //建立DOM文檔對象 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File("config.xml")); //獲取包含類名的文本節點 NodeList nl = doc.getElementsByTagName("className"); Node classNode=nl.item(0).getFirstChild(); String cName=classNode.getNodeValue(); //經過類名生成實例對象並將其返回 Class c=Class.forName(cName); Object obj=c.newInstance(); return obj; } catch(Exception e) { e.printStackTrace(); return null; } } }
使用
Class.forName
時,必定要注意包名(路徑默認爲src)。
有了XMLUtil
類後,能夠對日誌記錄器的客戶端代碼進行修改,再也不直接使用new關鍵字來建立具體的工廠類,而是將具體工廠類的類名存儲在XML文件中,再經過XMLUtil
類的靜態工廠方法getBean()
方法進行對象的實例化,代碼修改以下:
class Client { public static void main(String args[]) { LoggerFactory factory; Logger logger; factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回類型爲Object,須要進行強制類型轉換 logger = factory.createLogger(); logger.writeLog(); } }
引入XMLUtil類和XML配置文件後,若是要增長新的日誌記錄方式,只須要執行以下幾個步驟:
LoggerFactory
,並實現其中的工廠方法createLogger()
,設置好初始化參數和環境變量,返回具體日誌記錄器對象;config.xml
,將新增的具體日誌記錄器工廠類的類名字符串替換原有工廠類類名字符串;經過上述重構可使得系統更加靈活,因爲不少設計模式都關注系統的可擴展性和靈活性,所以都定義了抽象層,在抽象層中聲明業務方法,而將業務方法的實現放在實現層中。
有人說:能夠在客戶端代碼中直接經過反射機制來生成產品對象,在定義產品對象時使用抽象類型,一樣能夠確保系統的靈活性和可擴展性,增長新的具體產品類無須修改源代碼,只須要將其做爲抽象產品類的子類再修改配置文件便可,根本不須要抽象工廠類和具體工廠類。 試思考這種作法的可行性?若是可行,這種作法是否存在問題?爲何?
首先談談咱們爲何使用工廠方法?爲了在添加產品的時候不須要修改代碼,知足「開閉原則」。本方法雖然也是能夠知足開閉原則的,可是某些產品的構造並非一鼓作氣的,須要必定的參數配置,使用反射,只能知足簡單產品的狀況,產品複雜了就處理不了了。即便是能夠處理,並定會有不少額外的穿件準備工做散落在代碼中,對待媽的可讀性有很大的影響,也會形成建立蔓延。參考建立對象與使用對象——談談工廠的做用 ,該文中提到:與一個對象相關的職責一般有三類:
對象自己所具備的職責、建立對象的職責和使用對象的職責
。將這三種職責分離是好的變成習慣。對代碼的複用有好處。兩個類A和B之間的關係應該僅僅是A建立B或是A使用B,而不能兩種關係都有。
Sunny公司開發人員經過進一步分析,發現能夠經過多種方式來初始化日誌記錄器,例如能夠爲各類日誌記錄器提供默認實現;還能夠爲數據庫日誌記錄器提供數據庫鏈接字符串,爲文件日誌記錄器提供文件路徑;也能夠將參數封裝在一個Object類型的對象中,經過Object對象將配置參數傳入工廠類。此時,能夠提供一組重載的工廠方法,以不一樣的方式對產品對象進行建立。固然,對於同一個具體工廠而言,不管使用哪一個工廠方法,建立的產品類型均要相同。如圖:
引入重載方法後,抽象工廠LoggerFactory
的代碼修改以下:
interface LoggerFactory { public Logger createLogger(); public Logger createLogger(String args); public Logger createLogger(Object obj); }
具體工廠類DatabaseLoggerFactory代碼修改以下:
class DatabaseLoggerFactory implements LoggerFactory { public Logger createLogger() { //使用默認方式鏈接數據庫,代碼省略 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } public Logger createLogger(String args) { //使用參數args做爲鏈接字符串來鏈接數據庫,代碼省略 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } public Logger createLogger(Object obj) { //使用封裝在參數obj中的鏈接字符串來鏈接數據庫,代碼省略 Logger logger = new DatabaseLogger(); //使用封裝在參數obj中的數據來初始化數據庫日誌記錄器,代碼省略 return logger; } } //其餘具體工廠類代碼省略
在抽象工廠中定義多個重載的工廠方法,在具體工廠中實現了這些工廠方法,這些方法能夠包含不一樣的業務邏輯,以知足對不一樣產品對象的需求。
爲了進一步簡化客戶端的使用,還能夠對客戶端隱藏工廠方法,此時,在工廠類中將直接調用產品類的業務方法
,客戶端無須調用工廠方法建立產品,直接經過工廠便可使用所建立的對象中的業務方法。 若是對客戶端隱藏工廠方法,日誌記錄器的結構圖將修改成:
抽象工廠中的業務代碼主要實現默認的logger,而後調用默認logger的業務方法:
//改成抽象類 abstract class LoggerFactory { //在工廠類中直接調用日誌記錄器類的業務方法writeLog() public void writeLog() { Logger logger = this.createLogger(); logger.writeLog(); } public abstract Logger createLogger(); }
客戶端代碼修改以下:
class Client { public static void main(String args[]) { LoggerFactory factory; factory = (LoggerFactory)XMLUtil.getBean(); factory.writeLog(); //直接使用工廠對象來調用產品對象的業務方法 } }
主要優勢:
主要缺點:
適用場景:
使用工廠方法模式設計一個程序來讀取各類不一樣類型的圖片格式,針對每一種圖片格式都設計一個圖片讀取器,如GIF圖片讀取器用於讀取GIF格式的圖片、JPG圖片讀取器用於讀取JPG格式的圖片。需充分考慮系統的靈活性和可擴展性。
此題中,圖片讀取器不知道它具體要讀的圖片格式,咱們可使用工廠方法,抽象出讀圖器和圖片。並針對每種圖片有相關的讀圖器具體實現。 類圖以下:
工廠方法模式經過引入工廠等級結構,解決了簡單工廠模式中工廠類職責過重
的問題,但因爲工廠方法模式中的每一個工廠只生產一類產品,可能會致使系統中存在大量的工廠類
,勢必會增長系統的開銷。此時,咱們能夠考慮將一些相關的產品組成一個「產品族
」,由同一個工廠來統一輩子產,這就是咱們本文將要學習的抽象工廠模式的基本思想。
Sunny軟件公司欲開發一套界面皮膚庫,能夠對Java桌面軟件進行界面美化。爲了保護版權,該皮膚庫源代碼不打算公開,而只向用戶提供已打包爲jar文件的class字節碼文件。用戶在使用時能夠經過菜單來選擇皮膚,不一樣的皮膚將提供視覺效果不一樣的按鈕、文本框、組合框等界面元素,其結構示意圖如圖所示:
該皮膚庫須要具有良好的靈活性和可擴展性,用戶能夠自由選擇不一樣的皮膚,開發人員能夠在不修改既有代碼的基礎上增長新的皮膚。
Sunny軟件公司的開發人員針對上述要求,決定使用工廠方法模式進行系統的設計,爲了保證系統的靈活性和可擴展性,提供一系列具體工廠來建立按鈕、文本框、組合框等界面元素,客戶端針對抽象工廠編程,初始結構如圖所示:
在圖中,提供了大量工廠來建立具體的界面組件,能夠經過配置文件更換具體界面組件從而改變界面風格。可是,此設計方案存在以下問題:
如何減小系統中類的個數並保證客戶端每次始終只使用某一種風格的具體界面組件?這是Sunny公司開發人員所面臨的兩個問題,顯然,工廠方法模式沒法解決這兩個問題,抽象工廠模式可讓這些問題迎刃而解。
產品等級結構:產品等級結構即產品的繼承結構,如一個抽象類是電視機,其子類有海爾電視機、海信電視機、TCL電視機,則抽象電視機與具體品牌的電視機之間構成了一個產品等級結構,抽象電視機是父類,而具體品牌的電視機是其子類。
產品族:在抽象工廠模式中,產品族是指由同一個工廠生產的,位於不一樣產品等級結構中的一組產品,如海爾電器工廠生產的海爾電視機、海爾電冰箱,海爾電視機位於電視機產品等級結構中,海爾電冰箱位於電冰箱產品等級結構中,海爾電視機、海爾電冰箱構成了一個產品族。
理解:產品等級結構至關於同種類型的產品,繼承於同一個基類的產品(例子中的:SpringButton和SummerButton屬於統一產品等級);產品族(不是字面上的意思)至關於同一個工廠生產的產品(SpringButton,SpringTextField和SpringComboBox屬於同一產品族,都是SpringFactory生產)。
在上圖中,不一樣顏色的多個正方形、圓形和橢圓形分別構成了三個不一樣的產品等級結構,而相同顏色的正方形、圓形和橢圓形構成了一個產品族,每個形狀對象都位於某個產品族,並屬於某個產品等級結構。圖中一共有五個產品族,分屬於三個不一樣的產品等級結構。咱們只要指明一個產品所處的產品族以及它所屬的等級結構,就能夠惟一肯定這個產品。
當系統所提供的工廠生產的具體產品並非一個簡單的對象,而是多個位於不一樣產品等級結構、屬於不一樣類型的具體產品時就可使用抽象工廠模式。
抽象工廠模式是全部形式的工廠模式中最爲抽象和最具通常性的一種形式。 抽象工廠模式與工廠方法模式最大的區別在於,工廠方法模式針對的是一個產品等級結構,而抽象工廠模式須要面對多個產品等級結構,一個工廠等級結構能夠負責多個不一樣產品等級結構中的產品對象的建立。當一個工廠等級結構能夠建立出分屬於不一樣產品等級結構的一個產品族中的全部對象時,抽象工廠模式比工廠方法模式更爲簡單、更有效率。抽象工廠模式示意圖如圖所示:
每個具體工廠能夠生產屬於一個產品族的全部產品,例如生產顏色相同的正方形、圓形和橢圓形,所生產的產品又位於不一樣的產品等級結構中。若是使用工廠方法模式,如圖所示結構須要提供15個具體工廠,而使用抽象工廠模式只須要提供5個具體工廠,極大減小了系統中類的個數。
抽象工廠模式爲建立一組對象提供了一種解決方案。與工廠方法模式相比,抽象工廠模式中的具體工廠不僅是建立一種產品,它負責建立一族產品(即一個工廠生產不一樣種類的產品)。
抽象工廠模式
(Abstract Factory Pattern):提供一個建立一系列相關或相互依賴對象的接口,而無須指定它們具體的類。抽象工廠模式又稱爲Kit模式,它是一種對象建立型模式。
在抽象工廠模式中,每個具體工廠都提供了多個工廠方法用於產生多種不一樣類型的產品,這些產品構成了一個產品族,抽象工廠模式結構如圖所示:
在抽象工廠模式結構圖中包含以下幾個角色:
AbstractFactory
(抽象工廠):它聲明瞭一組用於建立一族產品的方法,每個方法對應一種產品。ConcreteFactory
(具體工廠):它實現了在抽象工廠中聲明的建立產品的方法,生成一組具體產品,這些產品構成了一個產品族,每個產品都位於某個產品等級結構中。AbstractProduct
(抽象產品):它爲每種產品聲明接口,在抽象產品中聲明瞭產品所具備的業務方法。ConcreteProduct
(具體產品):它定義具體工廠生產的具體產品對象,實現抽象產品接口中聲明的業務方法。在抽象工廠中,聲明瞭多個工廠方法,用於建立不一樣類型的產品,抽象工廠能夠是接口,也能夠是抽象類或者具體類,其典型代碼以下所示:
abstract class AbstractFactory { public abstract AbstractProductA createProductA(); //工廠方法一 public abstract AbstractProductB createProductB(); //工廠方法二 ...... }
具體工廠實現了抽象工廠,每個具體的工廠方法能夠返回一個特定的產品對象,而同一個具體工廠所建立的產品對象構成了一個產品族。對於每個具體工廠類,其典型代碼以下所示:
class ConcreteFactory1 extends AbstractFactory { //工廠方法一 public AbstractProductA createProductA() { return new ConcreteProductA1(); } //工廠方法二 public AbstractProductB createProductB() { return new ConcreteProductB1(); } ...... }
與工廠方法模式同樣,抽象工廠模式也可爲每一種產品提供一組重載的工廠方法,以不一樣的方式對產品對象進行建立。
思考:抽象工廠模式是否符合「開閉原則」?【從增長新的產品等級結構和增長新的產品族兩方面進行思考】 增長新的產品等級結構:不符合。增長新的產品族意味着工廠 加 生產一種產品,須要修改代碼。 增長新的產品族:符合。須要增長一個工廠和新的產品等級所有類(新的產品等級:繼承原來的每一個等級結構中基類的產品類)。
一個產品族惟一對應一個工廠
SkinFactory接口充當抽象工廠,其子類SpringSkinFactory和SummerSkinFactory充當具體工廠,接口Button、TextField和ComboBox充當抽象產品,其子類SpringButton、SpringTextField、SpringComboBox和SummerButton、SummerTextField、SummerComboBox充當具體產品。完整代碼以下所示:
//在本實例中咱們對代碼進行了大量簡化,實際使用時,界面組件的初始化代碼較爲複雜,還須要使用JDK中一些已有類,爲了突出核心代碼,在此只提供框架代碼和演示輸出。 //按鈕接口:抽象產品 interface Button { public void display(); } //Spring按鈕類:具體產品 class SpringButton implements Button { public void display() { System.out.println("顯示淺綠色按鈕。"); } } //Summer按鈕類:具體產品 class SummerButton implements Button { public void display() { System.out.println("顯示淺藍色按鈕。"); } } //文本框接口:抽象產品 interface TextField { public void display(); } //Spring文本框類:具體產品 class SpringTextField implements TextField { public void display() { System.out.println("顯示綠色邊框文本框。"); } } //Summer文本框類:具體產品 class SummerTextField implements TextField { public void display() { System.out.println("顯示藍色邊框文本框。"); } } //組合框接口:抽象產品 interface ComboBox { public void display(); } //Spring組合框類:具體產品 class SpringComboBox implements ComboBox { public void display() { System.out.println("顯示綠色邊框組合框。"); } } //Summer組合框類:具體產品 class SummerComboBox implements ComboBox { public void display() { System.out.println("顯示藍色邊框組合框。"); } } //界面皮膚工廠接口:抽象工廠 interface SkinFactory { public Button createButton(); public TextField createTextField(); public ComboBox createComboBox(); } //Spring皮膚工廠:具體工廠 class SpringSkinFactory implements SkinFactory { public Button createButton() { return new SpringButton(); } public TextField createTextField() { return new SpringTextField(); } public ComboBox createComboBox() { return new SpringComboBox(); } } //Summer皮膚工廠:具體工廠 class SummerSkinFactory implements SkinFactory { public Button createButton() { return new SummerButton(); } public TextField createTextField() { return new SummerTextField(); } public ComboBox createComboBox() { return new SummerComboBox(); } }
爲了讓系統具有良好的靈活性和可擴展性,咱們引入了配置文件,配置文件config.xml
中存儲了具體工廠類的類名,代碼以下所示:
<?xml version="1.0"?> <config> <className>SpringSkinFactory</className> </config>
工具類XMLUtil
和配置文件,其中,XMLUtil
類的代碼以下所示:
import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.SAXException; import java.io.*; public class XMLUtil { //該方法用於從XML配置文件中提取具體類類名,並返回一個實例對象 public static Object getBean() { try { //建立文檔對象 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File("config.xml")); //獲取包含類名的文本節點 NodeList nl = doc.getElementsByTagName("className"); Node classNode=nl.item(0).getFirstChild(); String cName=classNode.getNodeValue(); //經過類名生成實例對象並將其返回 Class c=Class.forName(cName); Object obj=c.newInstance(); return obj; } catch(Exception e) { e.printStackTrace(); return null; } } }
客戶端調用
class Client { public static void main(String args[]) { //使用抽象層定義 SkinFactory factory; Button bt; TextField tf; ComboBox cb; factory = (SkinFactory)XMLUtil.getBean(); bt = factory.createButton(); tf = factory.createTextField(); cb = factory.createComboBox(); bt.display(); tf.display(); cb.display(); } }
客戶端調用結果:
顯示淺綠色按鈕。 顯示綠色邊框文本框。 顯示綠色邊框組合框。
若是須要更換皮膚,只需修改配置文件便可,在實際環境中,咱們能夠提供可視化界面,例如菜單或者窗口來修改配置文件,用戶無須直接修改配置文件。若是須要增長新的皮膚,只需增長一族新的具體組件並對應提供一個新的具體工廠,修改配置文件便可使用新的皮膚,原有代碼無須修改,符合「開閉原則」。
在真實項目開發中,咱們一般會爲配置文件提供一個可視化的編輯界面,相似Struts框架中的
struts.xml
編輯器,你們能夠自行開發一個簡單的圖形化工具來修改配置文件,實現真正的純界面操做。
Sunny公司使用抽象工廠模式設計了界面皮膚庫,該皮膚庫能夠較爲方便地增長新的皮膚,可是如今遇到一個很是嚴重的問題:因爲設計時考慮不全面,忘記爲單選按鈕(RadioButton)提供不一樣皮膚的風格化顯示,致使不管選擇哪一種皮膚,單選按鈕都顯得那麼「格格不入」。
Sunny公司的設計人員決定向系統中增長單選按鈕,可是發現原有系統竟然不可以在符合「開閉原則」的前提下增長新的組件,緣由是抽象工廠SkinFactory
中根本沒有提供建立單選按鈕的方法,若是須要增長單選按鈕,首先須要修改抽象工廠接口SkinFactory
,在其中新增聲明建立單選按鈕的方法,而後逐個修改具體工廠類,增長相應方法以實如今不一樣的皮膚中建立單選按鈕,此外還須要修改客戶端,不然單選按鈕沒法應用於現有系統。
怎麼辦?答案是抽象工廠模式沒法解決該問題,這也是抽象工廠模式最大的缺點。
在抽象工廠模式中,增長新的產品族很方便,可是增長新的產品等級結構很麻煩,抽象工廠模式的這種性質稱爲「開閉原則」的傾斜性。
「開閉原則」要求系統對擴展開放,對修改封閉,經過擴展達到加強其功能的目的,對於涉及到多個產品族與多個產品等級結構的系統,其功能加強包括兩方面:
正由於抽象工廠模式存在「開閉原則」的傾斜性,它以一種傾斜的方式來知足「開閉原則」,爲增長新產品族提供方便,但不能爲增長新產品結構提供這樣的方便,所以要求設計人員在設計之初就可以全面考慮,不會在設計完成以後向系統中增長新的產品等級結構,也不會刪除已有的產品等級結構,不然將會致使系統出現較大的修改,爲後續維護工做帶來諸多麻煩。
抽象工廠模式是工廠方法模式的進一步延伸,因爲它提供了功能更爲強大的工廠類而且具有較好的可擴展性,在軟件開發中得以普遍應用,尤爲是在一些框架和API類庫的設計中,例如在Java語言的AWT
(抽象窗口工具包)中就使用了抽象工廠模式,它使用抽象工廠模式來實如今不一樣的操做系統中應用程序呈現與所在操做系統一致的外觀界面。抽象工廠模式也是在軟件開發中最經常使用的設計模式之一。
主要優勢
主要缺點:
增長新的產品等級結構麻煩,須要對原有系統進行較大的修改,甚至須要修改抽象層代碼,這顯然會帶來較大的不便,違背了「開閉原則」。
適用場景:
Sunny軟件公司欲推出一款新的手機遊戲軟件,該軟件可以支持Symbian、Android和Windows Mobile等多個智能手機操做系統平臺,針對不一樣的手機操做系統,該遊戲軟件提供了不一樣的遊戲操做控制(OperationController)類和遊戲界面控制(InterfaceController)類,並提供相應的工廠類來封裝這些類的初始化過程。軟件要求具備較好的擴展性以支持新的操做系統平臺,爲了知足上述需求,試採用抽象工廠模式對其進行設計。
因爲操做系統是變化因素,而遊戲相關類比較固定(只有兩個),爲了方便不一樣操做系統的移植,設計UML圖以下: