一、創建型設計模式
創建型模式抽象了實例化過程,它們幫助一個系統獨立于如何創建、組合和表示它的那些對象。一個類創建型模式使用繼承改變被實例化的類,而一個對象創建型模式將實例化委托給另一個對象。
隨著系統演化得越來越依賴于對象復合而不是類繼承,創建型模式變得更為重要。當這種情況發生時,重心從對一組固定行為的硬編碼(hard-coding)轉移為定義一個較小的基本行為集,這些行為可以被組合成任意數目的更復雜的行為。這樣創建有特定行為的對象要求的不僅僅是實例化一個類。
在這些模式中有兩個不斷出現的主旋律。第一,它們都將關于該系統使用哪些具體的類的信息封裝起來。第二,它們隱藏了這些類的實例是如何被創建和放在一起的。整個系統關于這些對象所知道的是由抽象類所定義的接口。因此,創建型模式在什么被創建,誰創建它,它是怎樣被創建的,以及何時創建這些方面給予了很大的靈活性。它們允許用結構和功能差別很大的“產品”對象配置一個系統。配置可以是靜態的(即在編譯時指定),也可以是動態的(在運行時)。
(一)Abstract Factory 模式
1.模式名稱
Abstract Factory,也經常稱之為抽象工廠模式。
2.意圖解決的問題
在程序中創建一個對象似乎是不能再簡單的事情,其實不然。在大型系統開發中存在以下問題:
(1)object new ClassName 是最常見的創建對象方法,但這種方法造成類名的硬編碼,需要根據不同的運行環境動態加載相同接口但實現不同的類實例,這樣的創建方法就需要配合上復雜的判斷,實例化為不同的對象。
(2)為了適用于不同的運行環境,經常使用抽象類定義接口,并在不同的運行環境中實現這個抽象類的子類。普通的創建方式必然造成代碼同運行環境的強綁定,軟件產品無法移植到其他的運行環境。
抽象工廠模式就可以解決這樣的問題,根據不同的配置或上下文環境加載具有相同接口的不同類實例。
3.模式描述
就如同抽象工廠的名字一樣,Abstract Factory 類將接受 Client 的“訂單”——Client 發送過來的消息,使用不同的“車間”——不同的 Concrete Factory,根據已有的“產品模型”——Abstract Product,生產出特定的“產品”——Product。
不同的車間生產出不同的產品供客戶使用,車間與產品的關系是一一對應的。由于所有的產品都遵循產品模型——Abstract Product,具有相同的接口,所以這些產品都可以直接交付客戶使用。
在抽象工廠模式中,Abstract Factory 可以有多個類似于 Create Product()的虛方法,就如同一個工廠中有多條產品線一樣。Create Product1()創建產品線 1,Create Product2()創建產品線 2。
事實上,如果仔細觀察AbstractFactory 模式就可以發現,對于 Client 來說,最關注的就是在不同條件下獲得接口一致但實現不同的對象,只要避免類名的硬編碼,采用其他方式也可以實現。
所以也可以采用其他的方式實現。例如在 Java 中就可以采用接口的方式實現。
(1)定義抽象產品接口
// 抽象產品A
public interface AbstractProductA {void methodA();
}// 抽象產品B
public interface AbstractProductB {void methodB();
}
(2) 實現具體產品類
// 具體產品A1
public class ProductA1 implements AbstractProductA {@Overridepublic void methodA() {System.out.println("Product A1");}
}// 具體產品A2
public class ProductA2 implements AbstractProductA {@Overridepublic void methodA() {System.out.println("Product A2");}
}// 具體產品B1
public class ProductB1 implements AbstractProductB {@Overridepublic void methodB() {System.out.println("Product B1");}
}// 具體產品B2
public class ProductB2 implements AbstractProductB {@Overridepublic void methodB() {System.out.println("Product B2");}
}
(3)定義抽象工廠接口
public interface AbstractFactory {AbstractProductA createProductA();AbstractProductB createProductB();
}
(4) 實現具體工廠類
// 具體工廠1
public class ConcreteFactory1 implements AbstractFactory {@Overridepublic AbstractProductA createProductA() {return new ProductA1();}@Overridepublic AbstractProductB createProductB() {return new ProductB1();}
}// 具體工廠2
public class ConcreteFactory2 implements AbstractFactory {@Overridepublic AbstractProductA createProductA() {return new ProductA2();}@Overridepublic AbstractProductB createProductB() {return new ProductB2();}
}
(5)客戶端代碼
public class Client {public static void main(String[] args) {// 使用具體工廠1創建產品AbstractFactory factory1 = new ConcreteFactory1();AbstractProductA productA1 = factory1.createProductA();AbstractProductB productB1 = factory1.createProductB();productA1.methodA();productB1.methodB();// 使用具體工廠2創建產品AbstractFactory factory2 = new ConcreteFactory2();AbstractProductA productA2 = factory2.createProductA();AbstractProductB productB2 = factory2.createProductB();productA2.methodA();productB2.methodB();}
}
4.場景舉例
(1)場景描述
假設你正在開發一個報告生成工具,該工具允許用戶輸入數據并生成包含圖表和表格的報告。用戶可以選擇輸出格式為PDF或者Word文檔。每種輸出格式都需要特定風格的圖表和表格來確保視覺一致性。例如,PDF格式可能要求圖表使用矢量圖形以保持高質量打印效果,而Word文檔可能偏好位圖格式以便于編輯。
(2)遇到的困難
- 多格式支持:為了適應不同的輸出需求,你需要分別為PDF和Word文檔實現各自的圖表和表格生成邏輯。直接針對每個格式編寫代碼會導致大量的重復工作,并且維護成本高。
- 切換格式不便:如果想要添加對新格式的支持(比如HTML),你需要修改大量代碼來適配新的格式,這增加了出錯的可能性以及維護難度。
- 代碼耦合度高:在業務邏輯中直接創建具體圖表和表格類的實例會使代碼高度依賴于具體的實現細節,降低了代碼的靈活性和可復用性。
(3)抽象工廠模式如何解決問題
抽象工廠模式通過定義一組相關對象的接口,但讓子類決定究竟要實例化哪些類來解決上述問題。在這個例子中:
- 定義抽象工廠:首先,你可以定義一個抽象工廠ReportElementFactory,它聲明了創建圖表(createChart)和表格(createTable)的方法。
- 具體工廠實現:然后,針對每種輸出格式實現具體的工廠類,比如PdfElementFactory和WordElementFactory。這些工廠類實現了抽象工廠中的方法,用于創建符合各自格式規范的圖表和表格。
- 客戶端使用:當用戶選擇輸出格式后,系統根據所選格式選擇相應的具體工廠實例。之后,所有的圖表和表格都通過這個工廠實例來創建,而不需要知道它們具體的類型是什么。
例如,在生成PDF報告時,系統會使用PdfElementFactory來創建適合PDF格式的圖表和表格;而在生成Word文檔時,則使用WordElementFactory來創建適合Word格式的圖表和表格。
這樣做的好處是,當你需要添加對一種新格式(如HTML)的支持時,只需創建一個新的具體工廠類并實現相應的圖表和表格創建方法即可,無需改動現有的業務邏輯代碼。這種方式不僅簡化了代碼結構,還提高了系統的靈活性和擴展性,同時減少了維護成本。此外,由于業務邏輯與具體元素創建分離,提高了代碼的可復用性和靈活性。
為了更直觀地展示抽象工廠模式的應用效果,下面將分別給出不使用抽象工廠模式和使用抽象工廠模式的代碼對比。我們將以生成報告中的圖表和表格為例。
(4)不使用抽象工廠模式
在這個例子中,直接在業務邏輯中創建具體的圖表和表格對象,這將導致代碼高度依賴于具體實現細節,增加了維護成本。
// PDF格式圖表和表格
class PdfChart {public void display() {System.out.println("顯示PDF格式圖表");}
}class PdfTable {public void display() {System.out.println("顯示PDF格式表格");}
}// Word格式圖表和表格
class WordChart {public void display() {System.out.println("顯示Word格式圖表");}
}class WordTable {public void display() {System.out.println("顯示Word格式表格");}
}public class ReportGenerator {public static void generatePdfReport() {PdfChart chart = new PdfChart();PdfTable table = new PdfTable();chart.display();table.display();}public static void generateWordReport() {WordChart chart = new WordChart();WordTable table = new WordTable();chart.display();table.display();}public static void main(String[] args) {String reportType = "pdf"; // 假設用戶選擇了PDF報告if ("pdf".equals(reportType)) {generatePdfReport();} else if ("word".equals(reportType)) {generateWordReport();}}
}
(5)使用抽象工廠模式
通過引入抽象工廠模式,我們可以解耦具體圖表和表格的創建過程與業務邏輯,使得添加新格式支持變得更加容易。
首先,定義抽象產品和抽象工廠:
// 抽象產品:圖表和表格
interface Chart {void display();
}interface Table {void display();
}// 抽象工廠:用于創建圖表和表格
interface ReportElementFactory {Chart createChart();Table createTable();
}// 總結:增加了定義抽象產品的抽象類或接口的代碼,增加了定義抽象工程的抽象類或接口的代碼
然后,為每種格式實現具體的工廠和產品類:
// PDF格式的具體產品
class PdfChart implements Chart {@Overridepublic void display() {System.out.println("顯示PDF格式圖表");}
}class PdfTable implements Table {@Overridepublic void display() {System.out.println("顯示PDF格式表格");}
}// PDF格式的具體工廠
class PdfElementFactory implements ReportElementFactory {@Overridepublic Chart createChart() {return new PdfChart();}@Overridepublic Table createTable() {return new PdfTable();}
}// Word格式的具體產品
class WordChart implements Chart {@Overridepublic void display() {System.out.println("顯示Word格式圖表");}
}class WordTable implements Table {@Overridepublic void display() {System.out.println("顯示Word格式表格");}
}// Word格式的具體工廠
class WordElementFactory implements ReportElementFactory {@Overridepublic Chart createChart() {return new WordChart();}@Overridepublic Table createTable() {return new WordTable();}
}
// 總結:增加了具體產品要繼承或實現抽象產品的代碼,增加了具體工廠類的代碼
最后,在客戶端代碼中使用抽象工廠:
public class ReportGeneratorWithAbstractFactory {public static void generateReport(ReportElementFactory factory) {Chart chart = factory.createChart();Table table = factory.createTable();chart.display();table.display();}public static void main(String[] args) {String reportType = "pdf"; // 假設用戶選擇了PDF報告ReportElementFactory factory;if ("pdf".equals(reportType)) {factory = new PdfElementFactory();} else if ("word".equals(reportType)) {factory = new WordElementFactory();} else {throw new UnsupportedOperationException("不支持的報告類型");}generateReport(factory);}
}
// 總結:減少了各具體產品調用過程的代碼
5.效果
對比分析:
(1)代碼量
使用抽象工廠模式會引入更多的類和接口,從而增加了一些初始的代碼量,所以如果代碼不復雜,不要隨便引入抽象工廠模式。
使用抽象工廠模式通常意味著引入額外的抽象層(如抽象產品和抽象工廠),這增加了代碼的復雜性和理解難度,特別是對于那些不熟悉該模式的人來說。在項目初期或者規模較小的應用中,這種額外的復雜度可能并不值得。
減少的代碼為具體調用過程的代碼,從代碼量來看,如果接口的實現類有不少于3種,使用抽象工廠模式才能從中受益。
(2)一致性保證
使用抽象工廠模式之后,因為要繼承抽象工廠類和抽象產品類,可以為擴展編寫新的類型的功能提供參考。抽象工廠同時確保了產品族的一致性。比如,在我們的報告生成工具例子中,它確保了為特定輸出格式生成的所有圖表和表格都屬于同一風格。
雖然抽象工廠模式允許輕松地替換整個產品系列,但它也對新產品的添加施加了一定的限制。每當你想要向系統中添加一個新的產品族時,你必須更新抽象工廠接口以支持新的產品類型。如果系統已經非常龐大,這項工作可能會變得相當繁瑣。
(3)解耦合
抽象工廠模式通過將對象的創建過程與使用過程分離,減少了業務邏輯對具體實現的依賴。這意味著你可以更容易地替換或更新產品族中的任何一個部分,而不需要修改使用這些產品的業務邏輯代碼。
當然,有時開發者可能會傾向于使用抽象工廠模式解決那些實際上不需要這么復雜解決方案的問題。這樣做會導致不必要的復雜性和開銷。
(4)擴展性
當你需要支持新的產品系列時(例如,在我們的例子中添加HTML格式的支持),你只需要添加新的具體工廠和產品類即可,而無需改動現有的客戶端代碼。這使得系統更加靈活且易于擴展。
抽象工廠模式假定產品族中的所有產品都是配套使用的,這意味著如果你需要在運行時改變產品組合(例如,在同一個應用中同時使用來自不同產品族的對象),則會比較困難。
(5)濫用抽象工廠模式的例子
假設你正在開發一個簡單的命令行工具,這個工具的主要功能是從文本文件讀取數據并進行簡單處理。在這個場景下,如果你為了支持將來可能的“不同的文件格式”或“不同的輸出方式”,就提前設計了一個復雜的抽象工廠模式結構,那么這就屬于濫用抽象工廠模式的情況。具體來說:
- 為每個可能的文件輸入格式(比如CSV, JSON等)和輸出格式(控制臺輸出、文件輸出等)都設計了相應的具體工廠和產品。
- 在當前階段,你的應用程序僅需支持一種文件格式和一種輸出方式,因此這些額外的設計和抽象實際上是多余的。
- 這樣的設計不僅增加了項目的復雜度,而且使得維護成本上升,因為每次你想添加一個小的功能改進時,都需要考慮如何適應現有的復雜架構。
在這種情況下,直接實現必要的功能,而不使用任何設計模式可能是更合適的選擇。隨著項目的發展,如果確實需要支持多種輸入輸出格式,可以逐步引入更復雜的設計模式。這樣不僅可以保持項目的簡潔性,也能確保每一部分的設計都是必要的和有效的。總之,選擇設計模式應該基于實際需求,而不是預先假設的需求。
(6)總結
應用 Abstract Factory 模式可以實現對象可配置的、動態的創建。靈活運用 Abstract Factory 模式可以提高軟件產品的移植性,尤其是當軟件產品運行于多個平臺,或有不同的功能配置版本時,抽象工廠模式可以減輕移植和發布時的壓力,提高軟件的復用性。
(二)Builder 模式
1. 模式名稱
Builder模式(建造者模式)
2. 意圖解決的問題
Builder模式主要用于解決在創建復雜對象時,構造過程需要逐步構建的情況。當一個對象的創建算法應該獨立于其組成部分以及它們的裝配方式時,使用Builder模式是非常有用的。此外,它還允許對構造過程進行更精細的控制,而不只是簡單地調用構造函數。
具體來說,Builder模式可以解決以下問題:
- 當對象的創建過程非常復雜,包含多個步驟,并且不是所有步驟都是必需的時候。
- 需要生成不同表示的對象,這些對象之間可能只有細微的差異。
- 希望避免構造函數參數列表過長的問題,尤其是當許多參數是可選的時候,這可以提高代碼的可讀性和健壯性。
3. 模式描述
Builder模式是一種創建型設計模式,它提供了一種靈活的方式用于構建復雜的對象。該模式通過將一個復雜對象的構建與其表現分離,使得同樣的構建過程可以創建不同的表現形式。
核心組成
- Builder(抽象建造者):提供了一個接口或抽象類,定義了創建產品各個部分的方法。這使得你可以輕松地擴展新的具體建造者來支持不同類型的構建邏輯。
- ConcreteBuilder(具體建造者):實現Builder接口,定義并明確自己所負責創建的部分產品,構造和裝配該產品的特定部分。
- Director(指揮者):類封裝了使用建造者對象的構建算法。它允許你改變產品的內部表示而不影響客戶端代碼。此外,指揮者還提供了一種控制構建過程的方式,確保按照一定的順序進行構建。
- Product(產品角色):表示被構造的復雜對象。ConcreteBuilder創建該產品的內部表示并定義它的裝配過程,包含定義組成部件的類,包括將這些部件裝配成最終產品的接口。
工作流程
- 客戶端創建Director對象,并根據需求選擇合適的ConcreteBuilder。
- Director通知ConcreteBuilder開始構建產品。
- ConcreteBuilder處理產品的構建細節,并返回構建完成的產品給Director。
- 最終,Director將構建好的產品返回給客戶端。
(1) 定義產品類(Product)
public class Product {private List<String> parts = new ArrayList<>();public void add(String part) {parts.add(part);}public void show() {System.out.println("Product parts: " + String.join(", ", parts));}
}
(2)定義抽象建造者(Builder)
public interface Builder {void buildPartA();void buildPartB();Product getResult();
}
(3) 定義具體建造者(ConcreteBuilder)
public class ConcreteBuilder implements Builder {private Product product = new Product();@Overridepublic void buildPartA() {product.add("Part A");}@Overridepublic void buildPartB() {product.add("Part B");}@Overridepublic Product getResult() {return product;}
}
(4) 定義指揮者(Director)
public class Director {private Builder builder;public Director(Builder builder) {this.builder = builder;}public void construct() {builder.buildPartA();builder.buildPartB();}
}
(5) 客戶端代碼
public class Client {public static void main(String[] args) {// 創建具體建造者和指揮者Builder builder = new ConcreteBuilder();Director director = new Director(builder);// 指揮者指導建造者構建產品director.construct();// 獲取最終構建的產品Product product = ((ConcreteBuilder) builder).getResult();product.show(); // 輸出:Product parts: Part A, Part B}
}
(6)示例代碼
假設我們要構建一輛汽車,汽車的構造涉及多個步驟,如安裝引擎、車輪等。我們可以使用Builder模式來簡化這個過程:
// Product
class Car {private String engine;private int wheelCount;public void setEngine(String engine) { this.engine = engine; }public void setWheelCount(int wheelCount) { this.wheelCount = wheelCount; }@Overridepublic String toString() {return "Car with engine: " + engine + " and wheels: " + wheelCount;}
}// Builder
abstract class CarBuilder {protected Car car;public Car getCar() { return car; }public void createNewCarProduct() { car = new Car(); }public abstract void buildEngine();public abstract void buildWheels();
}// ConcreteBuilder
class SportsCarBuilder extends CarBuilder {@Overridepublic void buildEngine() { car.setEngine("V8"); }@Overridepublic void buildWheels() { car.setWheelCount(4); }
}// Director
class Director {private CarBuilder builder;public void setBuilder(CarBuilder builder) { this.builder = builder; }public Car constructCar() {builder.createNewCarProduct();builder.buildEngine();builder.buildWheels();return builder.getCar();}
}// 客戶端代碼
public class Client {public static void main(String[] args) {Director director = new Director();SportsCarBuilder sportsCarBuilder = new SportsCarBuilder();director.setBuilder(sportsCarBuilder);Car car = director.constructCar();System.out.println(car);}
}
4.場景舉例
(1)場景描述
假設我們正在開發一個餐廳點餐系統,顧客可以通過該系統選擇各種食品和飲料來組成他們的訂單。每個訂單可以包含多個菜品和飲料,每個項目都有自己的屬性(例如,漢堡可能有多種口味、配料可選;飲料有不同的大小和溫度選項)。最終,我們需要根據顧客的選擇構建一個完整的訂單。
(2)遇到的困難:
- 構造函數參數過多:如果使用傳統的構造方法來創建訂單對象,由于需要設置很多可選參數(如漢堡的口味、額外添加的配料、飲料的大小等),這將導致構造函數參數列表非常長且難以管理。
- 代碼可讀性和維護性差:當有大量參數時,調用構造函數變得復雜,難以理解各個參數的具體含義,尤其是在存在多個可選參數的情況下。此外,隨著業務邏輯的變化(比如新增或修改某些選項),構造函數也需要頻繁更新,增加了維護成本。
- 對象狀態一致性問題:在某些情況下,某些組合可能是無效的(例如,某款漢堡不支持某種特定的配料)。通過普通的構造方法,很難確保對象被正確地初始化為有效狀態。
(3)Builder模式如何解決問題:
Builder模式提供了一種靈活的方法來逐步構建復雜的對象,同時保持代碼的清晰度和易維護性。下面是如何應用Builder模式解決上述問題的例子。
定義產品類(Product)
public class Order {private final String burgerType;private final List<String> toppings;private final String drinkType;private final String drinkSize;// 私有構造器private Order(Builder builder) {this.burgerType = builder.burgerType;this.toppings = builder.toppings;this.drinkType = builder.drinkType;this.drinkSize = builder.drinkSize;}public static class Builder {private String burgerType;private final List<String> toppings = new ArrayList<>();private String drinkType;private String drinkSize;public Builder setBurgerType(String burgerType) {this.burgerType = burgerType;return this;}public Builder addTopping(String topping) {this.toppings.add(topping);return this;}public Builder setDrinkType(String drinkType) {this.drinkType = drinkType;return this;}public Builder setDrinkSize(String drinkSize) {this.drinkSize = drinkSize;return this;}public Order build() {// 可以在這里進行有效性檢查if (burgerType == null || drinkType == null || drinkSize == null) {throw new IllegalStateException("Order is missing required fields");}return new Order(this);}}
}
使用Builder模式創建對象
public class Main {public static void main(String[] args) {Order order = new Order.Builder().setBurgerType("Cheese Burger").addTopping("Lettuce").addTopping("Tomato").setDrinkType("Coke").setDrinkSize("Large").build();System.out.println(order);}
}
5.效果
在springboot的項目中Lombok的@Builder注解實現了一種簡化版的建造者模式(Builder Pattern),它主要用于簡化對象的創建過程,特別是對于那些有許多參數或可選參數的對象。
與傳統的建造者模式相比,Lombok的@Builder注解提供了更加簡潔和方便的使用方式,但它們的核心理念是一致的:都是為了提供一種更靈活、更易讀的方式來構造復雜對象。
區別分析
(1)實現細節
傳統建造者模式:需要顯式地定義一個建造者類,該類包含逐步構建產品所需的方法,并最終返回構建完成的產品實例。這通常涉及更多的樣板代碼,如定義抽象建造者接口、具體建造者類以及指揮者類等。
Lombok的@Builder注解:通過在類上添加@Builder注解,Lombok會在編譯時自動生成必要的建造者邏輯,包括建造者類、建造方法、以及構建完成后的調用方法。開發者不需要手動編寫這些樣板代碼,大大簡化了實現過程。
(2)使用場景
傳統建造者模式:適用于非常復雜的對象構建過程,尤其是當構建步驟較多且可能變化時。此外,它還允許你為不同的構建過程定義多個具體的建造者,從而支持更多樣化的對象配置。
Lombok的@Builder注解:更適合于具有多個屬性或有若干可選屬性的對象創建。它的主要目的是減少冗長的構造函數和setter方法,使代碼更加簡潔明了。
(3) 靈活性
傳統建造者模式:由于其結構化的設計,可以更容易地擴展和修改構建邏輯。例如,你可以輕松地添加新的構建步驟或者改變現有步驟的順序。
Lombok的@Builder注解:雖然提供了便捷性,但在靈活性方面略遜一籌。比如,如果你想對構建過程進行一些定制化處理,可能需要額外的工作來覆蓋Lombok生成的默認行為。
(4) 可讀性和維護性
傳統建造者模式:因為所有的構建邏輯都是顯式的,所以對于不熟悉該模式的人來說,理解起來可能會稍微困難一點。但是,一旦理解后,它的結構清晰,易于維護。
Lombok的@Builder注解:極大地提高了代碼的簡潔性和可讀性,減少了樣板代碼的數量。不過,這也意味著你需要了解Lombok的工作原理,否則直接閱讀源碼時可能不會立即明白背后發生了什么。
(5)總結
Builder模式解決了以下問題:
- 簡化了對象創建過程:通過鏈式調用的方式設置對象屬性,使得代碼更加簡潔明了。
- 提高了代碼的可讀性和維護性:每個屬性的設置都非常直觀,易于理解和修改。即使將來需要增加新的屬性或選項,也只需相應地擴展Builder類即可,而不需要改動已有的客戶端代碼。
- 確保對象的一致性:可以在Builder的build()方法中添加必要的驗證邏輯,確保生成的對象始終處于有效狀態。
總之,Builder模式非常適合用于構建具有多個屬性(尤其是那些有很多可選屬性)的復雜對象。它不僅簡化了對象的創建過程,還增強了代碼的靈活性和可維護性,特別是在處理復雜的數據結構時顯得尤為重要。
(三)Factory Mehod 工廠方法
1. 模式名稱
工廠方法模式(Factory Method Pattern)
2. 意圖解決的問題
工廠方法模式意圖解決的是對象創建過程中的問題,特別是在需要根據特定條件或環境來決定實例化哪一個類的情況下。它允許一個類的實例化被延遲到子類中進行,從而解決了直接在基類中硬編碼具體類名所帶來的緊耦合問題。具體來說,它試圖解決以下幾個方面的問題:
- 提高代碼的可擴展性:當需要添加新的產品類型時,無需修改現有的工廠邏輯,只需增加相應的具體產品和對應的工廠實現。
- 降低模塊間的依賴關系:通過將對象創建的具體細節隱藏在具體的工廠類中,客戶端代碼不需要知道如何創建具體的產品對象,只需要與抽象接口或抽象類交互。
- 支持不同的產品等級結構:工廠方法模式可以用于處理具有不同等級結構的產品族。
3. 模式描述
工廠方法模式定義了一個用于創建對象的接口,但讓子類決定實例化哪一個類。工廠方法使得一個類的實例化延遲到其子類。這個模式涉及到四個主要角色:
- 產品(Product):定義工廠方法所創建的對象的接口或基類。
- 具體產品(Concrete Product):實現了Product接口的具體類,是工廠方法真正創建的對象。
- 工廠(Creator):聲明了工廠方法,該方法返回一個Product類型的對象。Creator也可以定義一個工廠方法的默認實現,該實現返回一個默認的Concrete Product對象。
- 具體工廠(Concrete Creator):重寫工廠方法以返回一個Concrete Product實例。
(1) 定義產品接口(Product)
public interface Product {void operation();
}
(2) 實現具體產品類(ConcreteProduct)
public class ConcreteProduct implements Product {@Overridepublic void operation() {System.out.println("ConcreteProduct operation");}
}
(3) 定義創建者接口(Creator)
public abstract class Creator {// 工廠方法,返回一個Product類型的對象public abstract Product factoryMethod();// 其他業務方法,可以使用工廠方法創建的對象public void anOperation() {Product product = factoryMethod();product.operation();}
}
(4) 實現具體創建者類(ConcreteCreator)
public class ConcreteCreator extends Creator {@Overridepublic Product factoryMethod() {return new ConcreteProduct();}
}
(5) 使用示例
public class Main {public static void main(String[] args) {Creator creator = new ConcreteCreator();creator.anOperation(); // 輸出: ConcreteProduct operation}
}
4.場景舉例
1.場景描述
假設我們正在開發一個文檔編輯器,用戶可以在其中創建和編輯各種類型的文檔。為了滿足不同用戶的需求,我們需要支持將文檔導出為不同的格式,如PDF、Word、HTML等。每種導出格式都有其特定的實現邏輯。
2.遇到的困難:
- 擴展性差:如果直接在代碼中硬編碼所有可能的導出格式,每次添加新的導出格式時都需要修改現有的代碼,這違反了開閉原則(對擴展開放,對修改關閉),增加了維護成本。
- 緊耦合:如果客戶端代碼直接依賴具體的導出類,那么一旦需要替換或增加新的導出格式,就需要改動大量使用這些導出功能的地方,導致代碼的可維護性和靈活性降低。
- 難以測試:由于導出操作可能涉及復雜的文件處理和外部庫調用,如果將所有的邏輯集中在一個地方,會使單元測試變得困難。
3.Factory Method模式如何解決問題:
通過應用Factory Method模式,我們可以將導出格式的選擇延遲到子類中進行,這樣就可以根據不同的需求動態地選擇合適的導出方式,而不需要修改已有的代碼。
1. 定義產品接口(Product)
public interface DocumentExporter {void export(String content);
}
2. 實現具體產品類(Concrete Product)
public class PdfExporter implements DocumentExporter {@Overridepublic void export(String content) {System.out.println("Exporting to PDF: " + content);// 實現PDF導出邏輯}
}public class WordExporter implements DocumentExporter {@Overridepublic void export(String content) {System.out.println("Exporting to Word: " + content);// 實現Word導出邏輯}
}public class HtmlExporter implements DocumentExporter {@Overridepublic void export(String content) {System.out.println("Exporting to HTML: " + content);// 實現HTML導出邏輯}
}
3. 定義抽象創建者(Creator)
public abstract class ExportManager {// 工廠方法protected abstract DocumentExporter createExporter();public void exportDocument(String content) {DocumentExporter exporter = createExporter();exporter.export(content);}
}
4. 實現具體創建者(Concrete Creator)
public class PdfExportManager extends ExportManager {@Overrideprotected DocumentExporter createExporter() {return new PdfExporter();}
}public class WordExportManager extends ExportManager {@Overrideprotected DocumentExporter createExporter() {return new WordExporter();}
}public class HtmlExportManager extends ExportManager {@Overrideprotected DocumentExporter createExporter() {return new HtmlExporter();}
}
5. 使用示例
public class Main {public static void main(String[] args) {ExportManager pdfManager = new PdfExportManager();pdfManager.exportDocument("This is a test document.");ExportManager wordManager = new WordExportManager();wordManager.exportDocument("This is another test document.");ExportManager htmlManager = new HtmlExportManager();htmlManager.exportDocument("And this one is for the web.");}
}
5.效果
(1)Factory Method模式的效果
Factory Method模式解決了以下問題:
- 提高了代碼的擴展性:每當需要添加新的導出格式時,只需創建一個新的DocumentExporter實現和對應的ExportManager子類即可,無需修改現有的代碼。
- 降低了模塊間的依賴關系:客戶端代碼不再需要知道具體要使用哪個導出類,只需要與抽象接口DocumentExporter以及抽象創建者ExportManager交互,從而實現了松散耦合。
- 增強了代碼的可測試性:由于采用了工廠方法來創建對象,可以輕松地為測試提供模擬對象(Mock Object),從而簡化了單元測試的過程。
總之,工廠方法模式非常適合用于解決需要根據運行時條件動態創建對象的情況,它不僅提高了系統的靈活性和可維護性,還遵循了面向對象設計的基本原則,使得代碼更加清晰和易于理解。在這個例子中,它幫助我們有效地管理了文檔編輯器中不同導出格式的實現,同時也為未來的擴展提供了便利。
(2)工廠方法和抽象工廠的區別
工廠方法模式(Factory Method Pattern)和抽象工廠模式(Abstract Factory Pattern)都是創建型設計模式,它們的主要目的是提供一種機制來創建對象,而無需直接通過具體類實例化。盡管兩者有相似之處,但它們解決的問題和使用場景有所不同。
工廠方法模式
定義:定義一個用于創建對象的接口,但是讓子類決定將哪一個類實例化。工廠方法使一個類的實例化延遲到其子類。
核心思想:
- 單個產品等級結構:工廠方法模式通常處理的是單個產品等級結構的情況,即它專注于創建單一類型的產品。
- 靈活性:允許子類決定如何實例化產品,提供了更大的靈活性。
- 簡單性:相比于抽象工廠模式,工廠方法模式較為簡單,因為它只涉及一個產品的創建。
示例:
如果你的應用程序需要根據不同的操作系統顯示不同風格的窗口(如Windows風格、Mac風格),你可以為每個操作系統定義一個具體的工廠類(Concrete Creator),這些工廠類實現了創建窗口的方法(工廠方法)。這樣,客戶端代碼只需與抽象的工廠接口交互,而不需要知道具體是哪個平臺的窗口被創建了。
抽象工廠模式
定義:提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的類。
核心思想:
- 多個產品族:抽象工廠模式處理的是多個產品等級結構的情況,即它可以創建一組相關的對象,而不是單個對象。
- 一致性:確保所創建的對象家族之間的一致性,即一起工作的對象應該屬于同一個產品族。
- 復雜性:相比工廠方法模式,抽象工廠模式更加復雜,因為它不僅要創建一個產品,還需要管理一組產品的創建。
示例:
假設你正在開發一個跨平臺UI框架,需要同時支持Windows和Mac兩種風格的控件(如按鈕、文本框等)。這時,你可以定義一個抽象工廠接口,該接口包含創建各種控件的方法。然后,為每種平臺實現這個抽象工廠接口的具體工廠類(Concrete Factory),這些具體工廠負責創建符合特定平臺風格的所有控件。這樣,客戶端代碼只需要與抽象工廠接口交互,就能獲取到一套完整的、風格一致的控件集合。
(四)Prototype模式
1. 模式名稱
原型模式(Prototype Pattern)
2. 意圖解決的問題
原型模式的主要意圖是通過復制現有的實例來創建新的對象,而不是通過實例化類的方式。這種模式特別適用于創建對象的成本較高(如復雜的計算、耗時的數據加載等),或者當對象的創建過程需要隔離時。它允許我們快速地創建對象的一個副本,并且可以根據需要對這個副本進行修改,而不會影響到原始對象。
具體來說,原型模式試圖解決以下幾個問題:
- 提高對象創建效率:對于一些復雜對象的創建,可能涉及大量的資源分配和初始化操作,使用原型模式可以通過簡單的復制已有的對象來減少這些開銷。
- 避免構造函數的約束:有時候我們需要繞過構造函數的一些限制或復雜性,原型模式提供了一種替代方案。
- 支持動態創建對象:在運行時根據需要創建對象,而不依賴于具體的類名。
3. 模式描述
原型模式的核心在于使用一個原型實例來指定所要創建的對象類型,并通過復制這個原型來創建新的對象。它涉及到以下幾個關鍵角色:
- Prototype(原型):聲明一個克隆自身的接口。
- ConcretePrototype(具體原型):實現Prototype接口的類,定義了如何克隆自身的方法。
- Client(客戶端):讓一個原型對象克隆自身,從而創建一個新的對象。
(1) Prototype 接口
首先定義一個 Prototype 接口,其中包含 clone() 方法。
public interface Prototype {Prototype clone();
}
(2) ConcretePrototype1 類
實現 Prototype 接口的具體類 ConcretePrototype1,并重寫 clone() 方法。
public class ConcretePrototype1 implements Prototype {private String name;public ConcretePrototype1(String name) {this.name = name;}@Overridepublic Prototype clone() {return new ConcretePrototype1(this.name);}@Overridepublic String toString() {return "ConcretePrototype1{name='" + name + "'}";}
}
(3) ConcretePrototype2 類
另一個實現 Prototype 接口的具體類 ConcretePrototype2,同樣重寫 clone() 方法。
public class ConcretePrototype2 implements Prototype {private String name;public ConcretePrototype2(String name) {this.name = name;}@Overridepublic Prototype clone() {return new ConcretePrototype2(this.name);}@Overridepublic String toString() {return "ConcretePrototype2{name='" + name + "'}";}
}
(4) Client 類
客戶端代碼,用于演示如何使用原型模式創建對象。
public class Client {public static void main(String[] args) {// 創建原型實例Prototype prototype1 = new ConcretePrototype1("Prototype1");Prototype prototype2 = new ConcretePrototype2("Prototype2");// 使用原型實例創建新對象Prototype copy1 = prototype1.clone();Prototype copy2 = prototype2.clone();System.out.println(prototype1); // 輸出: ConcretePrototype1{name='Prototype1'}System.out.println(copy1); // 輸出: ConcretePrototype1{name='Prototype1'}System.out.println(prototype2); // 輸出: ConcretePrototype2{name='Prototype2'}System.out.println(copy2); // 輸出: ConcretePrototype2{name='Prototype2'}}
}
以上代碼實現了原型模式的基本結構。通過定義一個 Prototype 接口,并讓具體的 ConcretePrototype1 和 ConcretePrototype2 類實現該接口,我們可以在客戶端中通過調用 clone() 方法來快速復制對象,而無需直接調用構造函數。這樣不僅提高了對象創建的效率,還使得代碼更加靈活和可擴展。
(6)簡單示例
// Prototype 接口
public interface Prototype extends Cloneable {Prototype clone() throws CloneNotSupportedException;
}// ConcretePrototype 類實現了 Prototype 接口
public class ConcretePrototype implements Prototype {private String name;public ConcretePrototype(String name) {this.name = name;}// 實現 clone 方法@Overridepublic Prototype clone() throws CloneNotSupportedException {return (Prototype) super.clone();}@Overridepublic String toString() {return "ConcretePrototype{name='" + name + "'}";}
}// 客戶端代碼
public class Client {public static void main(String[] args) {try {// 創建一個原型實例ConcretePrototype prototype = new ConcretePrototype("Original");// 使用原型實例創建新對象ConcretePrototype copy = (ConcretePrototype) prototype.clone();System.out.println(prototype); // 輸出: ConcretePrototype{name='Original'}System.out.println(copy); // 輸出: ConcretePrototype{name='Original'}} catch (CloneNotSupportedException e) {e.printStackTrace();}}
}
在這個例子中:
Prototype
是一個標記接口,表明任何實現它的類都應提供一種克隆自己的方法。ConcretePrototype
類實現了Prototype
接口,并提供了具體的 clone() 方法實現。這里直接調用了 Object 類中的 clone() 方法,該方法執行的是淺拷貝。如果需要深拷貝,則需要手動實現相應的邏輯。Client
類展示了如何使用原型實例來創建新對象。通過調用 clone() 方法,可以從現有的對象快速生成一個新的對象副本。
注意事項
- 淺拷貝 vs 深拷貝:默認情況下,Java中的clone()方法執行的是淺拷貝,這意味著如果對象包含引用類型的成員變量,那么這些引用指向的對象并不會被復制,而是與原對象共享相同的引用。如果需要完全獨立的對象副本,則需要實現深拷貝,這通常涉及到遞歸地復制所有引用類型的成員變量。
Cloneable
接口:雖然Cloneable是一個標記接口,不包含任何方法,但它必須被實現,以便對象可以調用Object類中的clone()方法。如果不實現Cloneable接口,調用clone()會導致CloneNotSupportedException異常。
4.場景舉例
(1)場景描述
假設我們正在開發一款在線多人角色扮演游戲(MMORPG),在這個游戲中,玩家可以創建自己的游戲角色。每個角色都有復雜的屬性和狀態,包括但不限于裝備、技能、等級等。為了提高用戶體驗,我們需要允許玩家快速地創建多個具有相似或相同屬性的角色,比如在不同服務器上創建角色,或者在游戲中快速復制一個角色作為副本進行實驗。
(2)遇到的困難
- 對象創建成本高:由于每個角色都包含大量的屬性和狀態信息,直接通過構造函數創建新的角色實例會涉及到大量重復的初始化工作,這不僅耗時,還可能涉及復雜的計算和數據加載。
- 代碼復雜度增加:如果使用傳統的構造函數方式來創建新角色,隨著角色屬性的增加,構造函數將變得非常龐大且難以維護。此外,當需要修改角色的默認屬性時,必須更新所有相關的構造調用,增加了出錯的可能性。
- 性能問題:在一些情況下,可能需要頻繁地創建類似的對象(例如,玩家希望在同一時間創建多個具有相同初始配置的角色)。如果每次都從頭開始創建這些對象,可能會導致顯著的性能瓶頸。
- Prototype模式如何解決問題:
(3)解決方案
通過應用原型模式,我們可以有效地解決上述問題:
- 簡化對象創建過程:利用現有的對象作為原型,通過復制(克隆)的方式快速生成新的對象實例。這種方式避免了重復的初始化步驟,大大提高了對象創建的效率。
- 降低代碼復雜性:不需要為每個具體的類編寫復雜的構造邏輯,只需實現一個簡單的clone()方法即可。這樣不僅簡化了代碼結構,也使得維護變得更加容易。
- 提升性能表現:對于那些需要頻繁創建的對象,尤其是那些創建成本較高的對象,原型模式提供了一種高效的解決方案。通過復制已有的對象而不是每次都重新構建,可以顯著減少資源消耗和時間開銷。
(4)示例代碼
// 定義Prototype接口
public interface GameCharacter extends Cloneable {GameCharacter clone();
}// 實現具體的原型類
public class Warrior implements GameCharacter {private String name;private int level;private List<String> equipment;public Warrior(String name, int level) {this.name = name;this.level = level;this.equipment = new ArrayList<>();// 假設這里有一些初始化操作}@Overridepublic GameCharacter clone() {try {// 淺拷貝Warrior copy = (Warrior) super.clone();// 深拷貝列表copy.equipment = new ArrayList<>(this.equipment);return copy;} catch (CloneNotSupportedException e) {return null;}}// Getter and Setter methods...
}// 客戶端代碼
public class Main {public static void main(String[] args) {// 創建原型角色Warrior prototypeWarrior = new Warrior("Warrior", 1);prototypeWarrior.getEquipment().add("Sword");prototypeWarrior.getEquipment().add("Shield");// 使用原型角色創建新角色Warrior clonedWarrior = (Warrior) prototypeWarrior.clone();System.out.println(clonedWarrior); // 輸出: Warrior{name='Warrior', level=1, equipment=[Sword, Shield]}// 修改克隆的角色而不影響原角色clonedWarrior.setName("New Warrior");clonedWarrior.getEquipment().add("Helmet");System.out.println(prototypeWarrior); // 輸出: Warrior{name='Warrior', level=1, equipment=[Sword, Shield]}System.out.println(clonedWarrior); // 輸出: Warrior{name='New Warrior', level=1, equipment=[Sword, Shield, Helmet]}}
}
在這個例子中,我們定義了一個GameCharacter接口,并讓Warrior類實現了這個接口。通過實現clone()方法,我們可以輕松地復制Warrior對象,而無需重復其復雜的初始化過程。這種方法不僅提高了角色創建的效率,也降低了代碼的復雜性和維護難度。
5.效果
(1)prototype模式的適用性
Prototype模式適用于以下情況:
- 當一個系統應該獨立于它的產品創建、構成和表示時。
- 當要實例化的類是在運行時刻指定時,例如,通過動態裝載。
- 為了避免創建一個與產品類層次平行的工廠類層次時。
- 當一個類的實例只能有幾個不同狀態組合中的一種時。建立相應數目的原型并克隆它們可能比每次用合適的狀態手工實例化該類更方便一些。
(2)java中的prototype和javascript中的prototype
Java中的原型模式
定義與用途:
在Java中,原型模式是一種設計模式,它通過復制現有的實例來創建新的對象,而不是通過調用構造函數。這種模式通常用于避免復雜對象的重復初始化過程,提高性能。
原型模式的核心在于實現Cloneable接口,并重寫clone()方法,從而允許對象自我復制。
特點:
- 對象復制:主要目的是為了快速生成對象的一個副本,以減少創建新對象時的成本。
- 深拷貝與淺拷貝:在使用原型模式時,需要特別注意深拷貝和淺拷貝的區別,確保對象及其引用類型的成員變量都被正確復制。
- 實現方式:通過實現Cloneable接口并重寫Object類中的clone()方法來實現對象的克隆。
示例代碼:
public class PrototypeExample implements Cloneable {private String name;public PrototypeExample(String name) {this.name = name;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}// Getter and Setter methods...
}
JavaScript中的原型(Prototype)
定義與用途:
在JavaScript中,“原型”是指一個對象用來繼承屬性和方法的機制。每個JavaScript對象都有一個內部鏈接指向另一個對象,即它的原型;這個原型對象也有自己的原型,如此層層鏈接形成所謂的“原型鏈”。當嘗試訪問一個對象的屬性時,如果該對象本身沒有此屬性,則會沿著原型鏈向上查找。
JavaScript中的原型主要用于實現繼承,使得對象可以共享屬性和方法,減少內存消耗。
特點:
- 原型鏈:JavaScript的對象之間通過原型鏈相連,形成一種繼承關系。
- 動態性:可以通過修改原型對象來動態地為已有的對象添加屬性或方法。
- 效率優化:由于多個對象可以共享同一個原型,因此可以節省內存資源,尤其是在創建大量相似對象的情況下。
示例代碼
function Animal(name) {this.name = name;
}Animal.prototype.speak = function() {console.log(this.name + " makes a noise.");
};let dog = new Animal("Dog");
dog.speak(); // 輸出: Dog makes a noise.// 動態添加方法到原型
Animal.prototype.walk = function() {console.log(this.name + " is walking.");
};dog.walk(); // 輸出: Dog is walking.
主要區別
目的不同:
- Java中的原型模式主要用于簡化對象的創建過程,特別是對于那些創建成本較高的對象。
- JavaScript中的原型則主要是為了實現對象間的繼承,以及屬性和方法的共享。
實現機制不同:
- Java中通過實現Cloneable接口并重寫clone()方法來實現對象的克隆。
- JavaScript則是通過每個對象的__proto__屬性或者直接通過構造函數的prototype屬性來建立原型鏈,實現屬性和方法的繼承。
應用場景不同:
- Java的原型模式適用于需要高效地創建大量相似對象的場景。
- JavaScript的原型機制幾乎應用于所有的對象創建和繼承過程中,因為它構成了JavaScript面向對象編程的基礎。
總結來說,盡管Java中的原型模式和JavaScript中的原型都涉及到“復制”或“繼承”的概念,但它們的應用場景和技術實現有著本質的不同。Java中的原型模式更側重于對象的高效創建,而JavaScript中的原型則是其語言特性之一,用于實現基于原型的繼承機制。
(五)Singleton 模式
1.模式名稱
Singleton,也常稱之為單件模式或單根模式。
2.意圖解決的問題
在軟件開發中,開發人員希望一些服務類有且僅有一個實例供其他程序使用。例如,短消息服務程序或打印機服務程序,甚至對于系統配置環境的控制,為了避免并發訪問造成的不一致,也希望僅為其他程序提供一個實例。
對于供整個系統使用的對象可以使用一個全局變量,不過全局變量僅能保證正確編碼時使用了唯一的實例。但隨著系統不斷的擴張,開發隊伍的擴大,仍然無法保證這個類在系統中有且僅有一個實例。
3.模式描述
從結構角度而言,Singleton 是最簡單的一個模式,不過其用途很廣。
在 Singleton 中,通過將 Singleton 類的構造函數設為 protected 型(或 private)來防止外部對其直接初始化。
需要訪問 Singleton 的程序必須通過 getInstance()方法來獲得一個 Singleton。
在getInstance() 中僅創建一次 uniqueInstance 就可以保證系統中的唯一實例。
對于 Singleton 中的 uniqueInstance 有兩種不同的初始化策略(Lazy Initialization 和Early Initialization),在實現中將分別給出這兩種初始化策略的代碼。
public class Singleton {// 靜態變量保存唯一實例,在類加載時就創建了唯一的實例,保證了線程安全。private static final Singleton uniqueInstance = new Singleton();// 私有構造函數,防止外部實例化private Singleton() {// 初始化操作}// 公共靜態方法,返回唯一實例,提供了一個全局訪問點,用于獲取唯一的實例。public static Singleton getInstance() {return uniqueInstance;}// 單例對象的操作方法public void singletonOperation() {System.out.println("執行單例操作");}// 獲取單例數據的方法public String getSingletonData() {return "這是單例數據";}
}// 使用單例的示例代碼
public class SingletonDemo {public static void main(String[] args) {// 通過getInstance方法獲取單例對象Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();// 檢查兩個實例是否相同System.out.println(instance1 == instance2); // 輸出:true// 調用單例對象的方法instance1.singletonOperation(); // 輸出:執行單例操作System.out.println(instance1.getSingletonData()); // 輸出:這是單例數據}
}
(1) 餓漢式 vs 懶漢式
- 餓漢式(如上例):在類加載時就完成了初始化,所以類加載比較慢,但獲取對象的速度快,且是線程安全的。
- 懶漢式:在第一次調用 getInstance() 方法時才初始化實例,這樣可以延遲加載,節省內存。但是,懶漢式需要額外的同步機制來保證線程安全,否則可能會導致多個實例被創建。
(2)雙重檢查鎖定(DCL)
如果你希望在保持線程安全的同時,也能夠延遲加載,可以使用雙重檢查鎖定的方式實現懶漢式單例:
public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {// 初始化操作}public static Singleton getInstance() {if (uniqueInstance == null) {synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}public void singletonOperation() {System.out.println("執行單例操作");}public String getSingletonData() {return "這是單例數據";}
}
在這個版本中,volatile 關鍵字確保了多線程環境下的可見性,而雙重檢查則避免了每次調用 getInstance() 方法時都進行同步,提高了性能。
4.場景舉例
(1)場景描述
假設我們正在開發一個Web應用程序,該應用需要頻繁訪問某些計算密集型或I/O密集型的數據(例如,復雜的算法計算結果、外部API調用的結果等)。為了提高性能和減少響應時間,我們決定引入一個本地緩存機制來存儲這些數據。
這個緩存池并不直接由Spring管理,也不想用Redis作為緩存。
如果應用對延遲極其敏感,那么訪問本地內存的速度通常會比通過網絡訪問Redis更快。
同時,對于一些小型應用或者僅限于單機環境的應用,引入Redis這樣的外部依賴可能顯得過于復雜且沒有必要。
在這種情況下,可以考慮使用簡單的本地緩存來加速最頻繁訪問的數據。
你可能希望該對象池在整個應用中只有一個實例存在,以確保資源共享和狀態的一致性。
在這個例子中,我們將創建一個簡單的內存緩存池,用于緩存一些計算結果或者頻繁訪問的數據。為了保證緩存池的唯一性和共享性,我們需要手動實現單例模式。
(2)遇到的困難
- 重復創建緩存池實例:如果沒有適當的控制措施,不同的模塊可能會各自創建自己的緩存池實例。這不僅浪費內存資源,還可能導致數據不一致的問題,因為每個緩存池可能持有不同的數據副本。
- 數據一致性問題:如果允許創建多個緩存池實例,則在更新緩存時可能會遇到數據同步的問題。例如,當一個模塊更新了緩存中的某個條目時,其他模塊可能仍然使用舊版本的數據,導致數據不一致。
- 復雜性增加:若沒有統一的緩存池管理機制,維護這些分散的緩存池將會變得非常復雜。每當需要調整緩存策略(如緩存過期策略、最大容量限制等)時,都需要逐一修改各個緩存池的配置,增加了維護成本。
- 潛在的性能瓶頸:如果緩存池被多次實例化,每次都需要重新加載和初始化數據,這對性能是一個極大的損耗,尤其是在高并發環境下,這種影響尤為明顯。
(3)單例模式如何解決問題
通過使用單例模式實現緩存池,可以有效地解決上述問題:
- 唯一實例:采用單例模式確保整個應用程序中只有一個緩存池實例存在。這意味著所有需要訪問緩存的地方都將共享同一個緩存池,避免了重復創建實例帶來的資源浪費,并且保證了數據的一致性。
- 數據一致性保障:由于所有的緩存操作都是通過同一個緩存池進行的,因此可以確保任何時候從緩存中獲取的數據都是最新和最準確的,解決了不同模塊間數據不一致的問題。
- 簡化管理和維護:只需要在一個地方配置和調整緩存池的行為(如設置緩存的最大容量、過期策略等),簡化了維護工作,減少了出錯的可能性。
- 性能優化:緩存池只會在首次使用時初始化一次,后續的操作可以直接利用已有的緩存數據,極大地提高了性能,特別是在高并發場景下效果顯著。
(4)不使用單例模式
import java.util.concurrent.ConcurrentHashMap;public class CachePool {private final java.util.Map<String, Object> cache;public CachePool() {cache = new java.util.concurrent.ConcurrentHashMap<>();}// 添加緩存項public void put(String key, Object value) {cache.put(key, value);}// 獲取緩存項public Object get(String key) {return cache.get(key);}
}
在這個例子中,CachePool 類可以被隨意實例化,這意味著每個需要使用緩存的地方都可以創建自己的 CachePool 實例。
public class SomeService {public void processData(String dataKey) {CachePool cachePool1 = new CachePool();CachePool cachePool2 = new CachePool();// 嘗試從第一個緩存池獲取數據Object cachedData1 = cachePool1.get(dataKey);if (cachedData1 == null) {Object computedData = computeExpensiveOperation(dataKey);cachePool1.put(dataKey, computedData);System.out.println("Computed and cached data for key: " + dataKey);} else {System.out.println("Retrieved from cache 1: " + cachedData1);}// 嘗試從第二個緩存池獲取數據Object cachedData2 = cachePool2.get(dataKey);if (cachedData2 == null) {System.out.println("Data not found in cache 2");} else {System.out.println("Retrieved from cache 2: " + cachedData2);}}private Object computeExpensiveOperation(String dataKey) {// 模擬耗時操作return "Result of " + dataKey;}
}
(5)使用單例模式
首先,我們創建一個CachePool類,并采用雙重檢查鎖定的方式實現單例模式:
import java.util.concurrent.ConcurrentHashMap;public class CachePool {private static volatile CachePool instance;private final java.util.Map<String, Object> cache;// 私有構造函數,防止外部實例化private CachePool() {cache = new java.util.concurrent.ConcurrentHashMap<>();}// 提供全局訪問點,采用雙重檢查鎖定機制public static CachePool getInstance() {if (instance == null) {synchronized (CachePool.class) {if (instance == null) {instance = new CachePool();}}}return instance;}// 添加緩存項public void put(String key, Object value) {cache.put(key, value);}// 獲取緩存項public Object get(String key) {return cache.get(key);}
}
接下來,我們在應用程序的某個部分使用這個緩存池。因為CachePool不是由Spring管理的Bean,所以我們不能直接通過依賴注入的方式來獲取它的實例,而是需要手動調用其靜態方法getInstance()。
public class SomeService {public void processData(String dataKey) {CachePool cachePool1 = CachePool.getInstance();CachePool cachePool2 = CachePool.getInstance();// 嘗試從緩存池獲取數據Object cachedData = cachePool1.get(dataKey);if (cachedData == null) {Object computedData = computeExpensiveOperation(dataKey);cachePool1.put(dataKey, computedData);System.out.println("Computed and cached data for key: " + dataKey);} else {System.out.println("Retrieved from cache: " + cachedData);}// 再次嘗試從同一個緩存池獲取數據Object cachedDataAgain = cachePool2.get(dataKey);if (cachedDataAgain == null) {System.out.println("Unexpected: Data not found in cache");} else {System.out.println("Retrieved from cache again: " + cachedDataAgain);}}private Object computeExpensiveOperation(String dataKey) {// 模擬耗時操作return "Result of " + dataKey;}
}
5.效果
使用 Singleton 模式可以保證系統中有且僅有一個實例,這對于很多服務類或者環境配置類來說非常重要。
優點:
- 資源共享:確保整個應用程序中只有一個緩存池實例存在,所有模塊共享相同的緩存數據,提高了資源利用率。
- 數據一致性:所有緩存操作都是通過同一個實例進行的,保證了數據的一致性和準確性。
- 簡化維護:只需要在一個地方配置和調整緩存池的行為,簡化了維護工作,減少了出錯的可能性。
缺點:
- 設計復雜度增加:實現單例模式通常需要更多的代碼來處理線程安全等問題,如上述示例中的雙重檢查鎖定。
- 擴展性限制:在分布式系統或需要跨多個JVM共享數據的情況下,單例模式的本地緩存將不再適用,需要考慮其他解決方案,如Redis等外部緩存服務。
- Singleton 模式僅適用于系統中至多有一個實例的情況,應避免濫用。很多過度設計的 Singleton 同使用了靜態方法的工具類一樣,沒有任何必要,反而可能降低效率。
單例模式的一個特點是它確保一個類只有一個實例存在。通常,這種控制是通過將構造函數設為私有來實現的,這意味著你不能從外部創建該類的新實例,也不能通過繼承來覆蓋或擴展其行為。因此,單例類通常不適合用于需要多態性的場景。
在C++中,你可以定義一個通用的單例模板,這個模板可以應用于任何類型,從而減少重復代碼。例如,你可以定義一個Singleton<T>
模板,然后對于每個想要作為單例使用的類,只需指定相應的類型參數即可,無需為每個類單獨編寫單例邏輯。
相比之下,Java和C#不支持像C++那樣的模板機制(雖然C#有泛型,但它們主要用于類型安全的數據結構和算法,而不是設計模式的具體實現)。因此,在這些語言中,如果要實現單例模式,則必須針對每個具體的類分別編寫單例邏輯。然而,這并不是一個問題,因為在一個良好的系統設計中,應該盡量減少對單例模式的依賴,避免出現過多的單例類。
值得注意的是,單例模式并不等同于靜態類。雖然兩者都可以提供全局訪問點,但單例模式允許延遲初始化,并且可以在運行時改變其實現(例如通過子類化),而靜態類則不具備這樣的靈活性。
理想情況下,一個系統中不應該存在大量的單例類。單例模式應謹慎使用,主要用于那些確實需要在整個應用程序生命周期內保持唯一實例的對象,如配置管理器、日志記錄器等。過度使用單例模式可能導致代碼難以測試、維護復雜度增加以及潛在的線程安全問題。