文章目錄
- 參考:[設計模式——設計模式理念](https://mp.weixin.qq.com/s/IEduZFF6SaeAthWFFV6zKQ)
- 參考:[設計模式——工廠方法模式](https://mp.weixin.qq.com/s/7tKIPtjvDxDJm4uFnqGsgQ)
- 參考:[設計模式——抽象工廠模式](https://mp.weixin.qq.com/s/QRpn41l4RIJnLPr6EysHpw)
- 參考:[設計模式——模板方法模式](https://mp.weixin.qq.com/s/wbjRs9pFZ_wXa89-y60nrA)
- 參考:[設計模式——適配器模式](https://mp.weixin.qq.com/s/mznNdNSaJ4K85IA_pMDvYA)
- 參考:[設計模式——裝飾器模式](https://mp.weixin.qq.com/s/Xb5cc8wJdyW8-byMWvSu5A)
- 設計模式概念
- 設計模式的七大原則
- 1. 單一職責原則(SRP)
- 思想
- 示例
- 2. 接口隔離原則(ISP)
- 思想
- 示例
- 3. 依賴倒轉(置)原則(DIP)
- 思想
- 示例
- 4. 里氏替換原則(LSP)
- 思想
- 示例
- 5. 開閉原則(OCP)
- 思想
- 示例
- 6. 迪米特法則(LoD)
- 思想
- 示例
- 7. 合成復用原則(CRP)
- 思想
- 示例
- 23 種設計模式
參考:設計模式——設計模式理念
參考:設計模式——工廠方法模式
參考:設計模式——抽象工廠模式
參考:設計模式——模板方法模式
參考:設計模式——適配器模式
參考:設計模式——裝飾器模式
設計模式概念
設計模式(Design Pattern)是前輩們對代碼開發經驗的總結,是解決特定問題的一系列套路。它不是語法規定,而是一套用來提高代碼高內聚、低耦合以及可重用(復用)性、可擴展(維護)性、可讀性、可靠性以及安全性的解決方案。
- 高內聚:模塊內部功能緊密相關,職責單一(如
策略模式
中每個策略類只負責一種算法); - 低耦合:模塊間依賴最小化(如
觀察者模式
解耦發布者和訂閱者); - 可重用性:相同功能的代碼可重復使用,避免重復造輪子(如
工廠模式
封裝對象創建邏輯,多處復用); - 可讀性:編程規范性,便于其他程序員的閱讀和理解;代碼結構符合通用范式(如
單例模式
明確表示全局唯一實例); - 可擴展性:當需要增加新的功能時,非常方便;新增功能時無需修改原有代碼(如
裝飾器模式
動態添加功能); - 可靠性:當增加新功能后,對原來的功能沒有影響;減少意外錯誤(如
不可變對象模式
避免狀態被篡改);
設計模式的本質是面向對象設計原則的實際運用,是對類的封裝性、繼承性和多態性以及類的關聯關系和組合關系的充分理解。
設計模式的七大原則
設計模式常用的七大原則(OOP七大原則)有:
- 單一職責原則(SRP)
- 接口隔離原則(ISP)
- 依賴倒轉原則(DIP)
- 里氏替換原則(LSP)
- 開閉原則(OCP)
- 迪米特法則(LoD)
- 合成復用原則(CRP)
1. 單一職責原則(SRP)
思想
對類來說的,即一個類應該只負責一項職責;或對方法來說的,保證一個方法盡量做好一件事。如類 A 負責兩個不同職責:職責1,職責2。當職責1需求變更而改變A時,可能造成職責2執行錯誤,所以需要將類A的粒度分解為A1,A2。
核心思想:高內聚、職責分離
-
職責的單一性:這里的職責是指類所承擔的功能或任務。例如,在一個電商系統中,
OrderService
類負責處理訂單相關的業務邏輯,如創建訂單、查詢訂單等,而不應該同時負責用戶登錄、支付等其他與訂單無關的功能。每個職責都應該是明確的、獨立的,并且能夠被清晰地描述和理解。 -
高內聚性:單一職責原則有助于實現類的高內聚性。內聚性是指類中各個元素(方法、屬性等)之間的緊密程度。當一個類只負責一項職責時,其內部的方法和屬性都與該職責緊密相關,它們之間的內聚性就高。這樣的類更容易理解、維護和擴展,因為所有相關的功能都被集中在一個地方。
-
降低耦合度:如果一個類承擔了多個職責,那么這些職責之間可能會存在相互依賴關系,這會導致類與其他類之間的耦合度增加。當其中一個職責發生變化時,可能會影響到其他依賴它的類,從而引發連鎖反應,增加了系統的復雜性和維護成本。而遵循單一職責原則,將不同的職責分離到不同的類中,可以降低類之間的耦合度,使得各個類可以獨立地變化和擴展,互不影響。
典型應用模式:策略模式、命令模式、外觀模式;
好處:控制類的粒度大小、將對象解耦、提高其內聚性。
示例
假設有一個 Employee
類,用于處理員工的相關信息和操作。
-
不遵循單一職責原則,代碼可能如下:
public class Employee {private String name;private int age;private String department;// 保存員工信息到數據庫public void saveToDatabase() {// 數據庫操作代碼}// 生成員工報表public void generateReport() {// 報表生成代碼}// 發送員工郵件public void sendEmail() {// 郵件發送代碼} }
在上述代碼中,
Employee
類承擔了多個職責,包括保存員工信息到數據庫、生成員工報表和發送員工郵件。這違反了單一職責原則,因為這些職責之間并沒有直接的關聯,而且它們的變化原因也不同。 -
遵循單一職責原則,可以將這些職責分離到不同的類中:
// 員工信息類,只負責存儲員工的基本信息 public class EmployeeInfo {private String name;private int age;private String department;// 省略getter和setter方法 }// 員工數據存儲類,負責將員工信息保存到數據庫 public class EmployeeDatabaseHandler {public void saveToDatabase(EmployeeInfo employeeInfo) {// 數據庫操作代碼} }// 員工報表生成類,負責生成員工報表 public class EmployeeReportGenerator {public void generateReport(EmployeeInfo employeeInfo) {// 報表生成代碼} }// 員工郵件發送類,負責發送員工郵件 public class EmployeeEmailSender {public void sendEmail(EmployeeInfo employeeInfo) {// 郵件發送代碼} }
通過將不同的職責分離到不同的類中,每個類都只負責一項職責,遵循了單一職責原則。這樣的設計使得代碼更加清晰、易于維護和擴展。當需要修改某個職責的實現時,只需要在對應的類中進行修改,而不會影響到其他類。
2. 接口隔離原則(ISP)
思想
用多個專門的接口,而不使用單一的總接口,客戶端不應該依賴它不需要的接口,即一個類對另一個類的依賴應該建立在最小的接口上。即為各個類建立它們需要的專用接口,提高其內聚性。
-
按隔離原則應當這樣處理:將一個大而全的接口拆分成多個小的、特定的接口。比如類 A 通過接口 Interface1 依賴類B,類 C 通過接口 Interface1 依賴類D,如果接口 Interface1 對于類 A 和類 C 來說不是最小接口,那么類 B 和類 D 也必須去實現他們不需要的方法;所以將接口 Interface1 拆分為獨立的幾個接口,類 A 和類 C 分別與他們需要的接口建立依賴關系。也就是采用接口隔離原則;接口 Interface1 中出現的方法,根據實際情況拆分為多個接口代碼實現。
典型應用模式:適配器模式;
與單一職責原則類似,將接口隔離,系統地指定一系列規則。
示例
假設有一個 Animal
接口,它包含了動物的各種行為方法
-
不遵循接口隔離原則代碼示例,代碼可能如下:
Animal
接口包含了動物的各種行為方法interface Animal {void eat();void fly();void swim(); }
現在有一個
Dog
類實現這個接口:public class Dog implements Animal {@Overridepublic void eat() {System.out.println("Dog is eating.");}@Overridepublic void fly() {// 狗不會飛,這個方法沒有實際意義throw new UnsupportedOperationException("Dogs can't fly.");}@Overridejavapublic void swim() {System.out.println("Dog is swimming.");} }
在這個例子中,
Dog
類實現了Animal
接口,但fly
方法對于狗來說是不需要的,這就導致Dog
類不得不實現一個沒有實際意義的方法,違反了接口隔離原則。 -
遵循接口隔離原則代碼示例:
將
Animal
接口拆分成多個小接口:public interface Eatable {void eat(); }public interface Flyable {void fly(); }public interface Swimmable {void swim(); }
現在有一個
Dog
類實現它需要的接口,Bird
類實現它需要的接口:public class Dog implements Eatable, Swimmable {@Overridepublic void eat() {System.out.println("Dog is eating.");}@Overridepublic void swim() {System.out.println("Dog is swimming.");} }public class Bird implements Eatable, Flyable {@Overridepublic void eat() {System.out.println("Bird is eating.");}@Overridepublic void fly() {System.out.println("Bird is flying.");} }
通過將大接口拆分成多個小接口,
Dog
類只需要實現它實際需要的Eatable
和Swimmable
接口,避免了實現不必要的方法。同樣,Bird
類只需要實現Eatable
和Flyable
接口。這樣的設計更加靈活,符合接口隔離原則。
3. 依賴倒轉(置)原則(DIP)
思想
依賴倒轉(倒置)的中心思想是面向接口編程;
依賴倒轉原則包含兩個核心要點:
-
高層模塊不應該依賴低層模塊,兩者都應該依賴抽象:高層模塊通常是指負責業務邏輯和整體流程控制的模塊,而低層模塊則是實現具體功能的細節模塊。依賴倒轉原則強調,高層模塊不應該直接依賴于低層模塊的具體實現,而是應該依賴于抽象接口或抽象類。同樣,低層模塊也應該依賴于抽象,而不是相互依賴具體的實現。
-
抽象不應該依賴細節,細節應該依賴抽象:抽象代表著穩定的、通用的概念和規范,而細節則是具體的實現。該原則要求抽象不應該受到具體實現細節的影響,相反,具體的實現細節應該遵循抽象所定義的規范。
典型應用模式:依賴注入、工廠模式;
抽象指的是接口或抽象類,細節就是具體的實現類。使用接口或抽象類的目的是制定好規范,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。要面向接口編程,不要面向實現編程。
依賴倒轉原則的注意事項和細節:
- 低層模塊盡量都要有抽象類或接口,或者兩者都有,程序穩定性更好;
- 變量的聲明類型盡量是抽象類或接口,這樣我們的變量引用和實際對象間,就存在一個緩沖層,利于程序擴展和優化;
- 繼承時遵循里氏替換原則。
示例
假設有一個簡單的電商系統,其中有一個 OrderService
類(高層模塊)負責處理訂單業務,PaymentService
類(低層模塊)負責處理支付業務。
-
不遵循依賴倒轉原則,代碼可能如下:
// 具體的支付服務類 public class PaymentService {public void pay() {System.out.println("使用默認支付方式支付");} }// 訂單服務類,直接依賴具體的支付服務,OrderService直接依賴于 PaymentService的具體實現, public class OrderService {private PaymentService paymentService;public OrderService() {this.paymentService = new PaymentService();}public void createOrder() {// 處理訂單業務邏輯System.out.println("創建訂單");// 調用支付服務paymentService.pay();} }
在這個例子中,
OrderService
直接依賴于PaymentService
的具體實現,當需要添加新的支付方式(如支付寶支付、微信支付)時,就需要修改PaymentService
類和OrderService
類,這違反了依賴倒轉原則。 -
遵循依賴倒轉原則,可以引入一個抽象的支付接口:
// 抽象的支付接口 public interface Payment {void pay(); }// 具體的支付服務類,實現支付接口 public class DefaultPaymentService implements Payment {@Overridepublic void pay() {System.out.println("使用默認支付方式支付");} }// 訂單服務類,依賴抽象的支付接口 public class OrderService {private Payment payment;public OrderService(Payment payment) {this.payment = payment;}public void createOrder() {// 處理訂單業務邏輯System.out.println("創建訂單");// 調用支付服務payment.pay();} }
通過引入
Payment
接口,OrderService
類依賴于抽象的Payment
接口,而不是具體的PaymentService
類。這樣,當需要添加新的支付方式時,只需要實現Payment
接口,然后在創建OrderService
對象時傳入相應的實現類即可,不需要修改OrderService
類的代碼,提高了系統的可擴展性和可維護性。
4. 里氏替換原則(LSP)
思想
里氏替換原則指出:如果S是T的子類型,那么程序中T類型的對象可以被替換為S類型的對象,而不會對程序的正確性產生任何影響。也就是說,所有引用父類的地方必須能透明地使用其子類的對象,一個可以接受父類對象的地方,也應該能夠接受其子類對象,并且程序的行為不會因為將基類對象替換為子類對象而發生改變。里氏替換原則強調了繼承關系中子類與父類的行為兼容性,確保子類可以無縫替換父類而不引起問題。
更通俗地說:子類必須能夠完全替代其父類,而不影響程序的正確性。
- 典型應用模式:模板方法模式;
核心要點:
- 子類必須完全實現父類的方法:子類是對父類的擴展和細化,因此子類應該實現父類中定義的所有抽象方法和非抽象方法。如果子類沒有實現父類的某些方法,那么在使用子類對象替換父類對象時,就可能會導致程序出現錯誤或異常。
- 實現抽象類或接口的基本要求;
- 子類可以覆蓋父類的非抽象方法,但覆蓋時需保證不改變父類方法的預期行為,確保使用子類對象替換父類對象時程序的正確性;
- 子類中可以增加自己特有的方法:在滿足里氏替換原則的前提下,子類可以添加自己特有的方法和屬性,以實現更具體的功能。但這些新增的特性不能影響到子類與父類之間的替換關系,即不能因為子類的特殊行為而破壞了程序中依賴父類的部分的正常運行。
- 這些新增的特性不會影響子類與父類之間的替換關系,因為在使用父類引用指向子類對象時,不會調用到子類特有的方法,只有當進行類型轉換后才能使用這些特有的方法;
- 覆蓋或實現父類的方法時輸入參數可以被放大:里氏替換原則允許子類在覆蓋或實現父類方法時,將方法的輸入參數類型放寬。這意味著子類方法可以接受更廣泛的輸入參數,而不會影響到使用父類對象的代碼。
- 子類方法的參數類型可以是父類方法參數類型的父類型(即更寬泛的類型);(如父類用
Integer
,子類可以用Number
) - 子類方法可以接受比父類更寬松的參數值范圍;(如父類約束入參>0,子類可以放開入參約束>=0)
- 子類方法的參數類型可以是父類方法參數類型的父類型(即更寬泛的類型);(如父類用
- 覆蓋或實現父類的方法時輸出參數可以被縮小:與輸入參數相反,子類在覆蓋或實現父類方法時,輸出參數的類型應該是父類方法輸出參數類型的子類型。這是因為調用者在使用父類對象時,期望得到的是父類方法所聲明的返回類型或其子類型的對象。如果子類方法返回的是父類返回類型的超類型對象,那么可能會導致調用者在處理返回結果時出現錯誤。
- 子類方法返回類型可以是父類方法返回類型的子類型(父類返回
Number
,子類可以返回Integer
); - 子類方法可以承諾比父類更精確的返回值特性(父類返回任意集合,子類返回排序集合);
- 子類方法可以拋出比父類更少的異常或更具體的異常類型;
- 子類方法返回類型可以是父類方法返回類型的子類型(父類返回
示例
1、子類必須完全實現父類的方法
子類要實現父類中定義的所有抽象方法和非抽象方法。若子類未實現父類的某些方法,使用子類對象替換父類對象時,程序可能出錯。
-
符合里氏替換原則的示例代碼:
// 抽象父類:交通工具 abstract class Vehicle {// 抽象方法:啟動public abstract void start(); }// 子類:汽車 class Car extends Vehicle {@Overridepublic void start() {System.out.println("汽車啟動");} }// 子類:自行車 class Bicycle extends Vehicle {@Overridepublic void start() {System.out.println("自行車蹬起來啟動");} }// 測試類 public class LSPExample1 {public static void main(String[] args) {Vehicle car = new Car();Vehicle bicycle = new Bicycle();startVehicle(car);startVehicle(bicycle);}public static void startVehicle(Vehicle vehicle) {vehicle.start();} }
Vehicle
是抽象父類,定義了抽象方法start
。Car
和Bicycle
子類都實現了該方法。在startVehicle
方法中,可傳入Car
或Bicycle
對象,程序正常運行。 -
不符合里氏替換原則的示例代碼:
// 抽象父類:交通工具 abstract class Vehicle {// 抽象方法:啟動public abstract void start(); }// 子類:汽車 class Car extends Vehicle {// 未實現 start 方法 }// 測試類 public class LSPViolationExample1 {public static void main(String[] args) {Vehicle car = new Car();startVehicle(car);}public static void startVehicle(Vehicle vehicle) {vehicle.start(); // 編譯錯誤,Car 類未實現 start 方法} }
Car
子類沒有實現父類Vehicle
的start
方法,當調用startVehicle
方法時,會出現編譯錯誤,無法正常使用子類對象替換父類對象。
2、子類中可以增加自己特有的方法
在滿足里氏替換原則的基礎上,子類可添加自身特有的方法和屬性,但不能影響子類與父類的替換關系。
-
符合里氏替換原則的示例代碼:
// 父類:動物 class Animal {public void eat() {System.out.println("動物進食");} }// 子類:貓 class Cat extends Animal {public void meow() {System.out.println("喵喵叫");} }// 測試類 public class LSPExample2 {public static void main(String[] args) {Animal cat = new Cat();cat.eat();if (cat instanceof Cat) {Cat realCat = (Cat) cat;realCat.meow();}} }
Cat
類繼承自Animal
類,添加了meow
方法。可將Cat
對象賦值給Animal
類型變量并調用eat
方法,若要調用meow
方法,需進行類型轉換。 -
不符合里氏替換原則的示例代碼:
// 父類:動物 class Animal {public void eat() {System.out.println("動物進食");} }// 子類:貓 class Cat extends Animal {public void meow() {System.out.println("喵喵叫");}@Overridepublic void eat() {throw new UnsupportedOperationException("貓拒絕進食");} }// 測試類 public class LSPViolationExample2 {public static void main(String[] args) {Animal cat = new Cat();try {cat.eat(); // 調用時拋出異常,破壞了原有行為} catch (UnsupportedOperationException e) {System.out.println("出現異常:" + e.getMessage());}} }
Cat
類重寫eat
方法時拋出異常,改變了父類方法的正常行為。當使用Cat
對象替換Animal
對象調用eat
方法時,程序出現異常,破壞了程序的正確性。
3、覆蓋或實現父類的方法時輸入參數可以被放大
子類在覆蓋或實現父類方法時,可放寬方法的輸入參數類型,使子類方法能接受更廣泛的輸入參數,且不影響使用父類對象的代碼。
-
符合里氏替換原則的示例代碼:
import java.util.ArrayList; import java.util.List;// 父類 class Parent {public void printList(List<Integer> list) {for (Integer num : list) {System.out.println(num);}} }// 子類 class Child extends Parent {public void printList(List<Number> list) {for (Number num : list) {System.out.println(num);}} }// 測試類 public class LSPExample3 {public static void main(String[] args) {Parent parent = new Parent();Parent child = new Child();List<Integer> intList = new ArrayList<>();intList.add(1);intList.add(2);parent.printList(intList);child.printList(intList);} }
父類
Parent
的printList
方法接受List<Integer>
類型參數,子類Child
的printList
方法接受List<Number>
類型參數。由于Integer
是Number
的子類,Child
對象可正常處理List<Integer>
類型參數。 -
不符合里氏替換原則的示例代碼:
import java.util.ArrayList; import java.util.List;// 父類 class Parent {public void printList(List<Number> list) {for (Number num : list) {System.out.println(num);}} }// 子類 class Child extends Parent {public void printList(List<Integer> list) {for (Integer num : list) {System.out.println(num);}} }// 測試類 public class LSPViolationExample3 {public static void main(String[] args) {Parent parent = new Parent();Parent child = new Child();List<Number> numberList = new ArrayList<>();numberList.add(1.0);numberList.add(2.0);parent.printList(numberList);// child.printList(numberList); 編譯錯誤,Child 類的 printList 方法不能接受 List<Number> 類型參數} }
子類
Child
的printList
方法輸入參數類型范圍比父類小,當使用Child
對象替換Parent
對象處理List<Number>
類型參數時,會出現編譯錯誤。
4、覆蓋或實現父類的方法時輸出參數可以被縮小
子類在覆蓋或實現父類方法時,輸出參數的類型應是父類方法輸出參數類型的子類型。調用者使用父類對象時,期望得到父類方法聲明的返回類型或其子類型的對象。
-
符合里氏替換原則的示例代碼:
// 父類 class SuperClass {public Number getNumber() {return 1;} }// 子類 class SubClass extends SuperClass {@Overridepublic Integer getNumber() {return 2;} }// 測試類 public class LSPExample4 {public static void main(String[] args) {SuperClass superClass = new SuperClass();SuperClass subClass = new SubClass();Number num1 = superClass.getNumber();Number num2 = subClass.getNumber();System.out.println(num1);System.out.println(num2);} }
父類
SuperClass
的getNumber
方法返回Number
類型,子類SubClass
的getNumber
方法返回Integer
類型,Integer
是Number
的子類。SubClass
對象可正常賦值給SuperClass
類型變量并調用getNumber
方法。 -
不符合里氏替換原則的示例代碼:
// 父類 class SuperClass {public Integer getNumber() {return 1;} }// 子類 class SubClass extends SuperClass {@Overridepublic Number getNumber() {return 2.0;} }// 測試類 public class LSPViolationExample4 {public static void main(String[] args) {SuperClass superClass = new SuperClass();SuperClass subClass = new SubClass();Integer num1 = superClass.getNumber();// Integer num2 = subClass.getNumber(); 編譯錯誤,無法將 Number 類型賦值給 Integer 類型} }
子類
SubClass
的getNumber
方法返回類型是Number
,比父類的返回類型范圍大。當使用SubClass
對象替換SuperClass
對象時,將返回值賦值給Integer
類型變量會出現編譯錯誤。
5. 開閉原則(OCP)
思想
對擴展開放,對修改關閉;
解釋:擴展原來程序,但盡量不修改原來的程序,即通過擴展(而非修改)增加新功能;
-
核心思想:通過抽象和繼承實現擴展性。開閉原則的核心在于通過抽象和封裝,將軟件系統中相對穩定的部分和容易變化的部分分離。穩定的部分作為抽象層,定義了系統的基本結構和行為規范;容易變化的部分則通過具體的實現類來體現,當需求發生變化時,只需要添加新的實現類,而不需要修改抽象層和其他已有的實現類。
典型應用模式:裝飾器模式、適配器模式、策略模式、模板方法模式;
示例
以一個簡單的圖形繪制為例,說明開閉原則的應用。
-
不遵循開閉原則的設計,代碼可能如下:
// 圖形類 class Shape {String type;public Shape(String type) {this.type = type;} }// 圖形繪制類 class Drawing {public void drawShape(Shape shape) {if ("circle".equals(shape.type)) {drawCircle();} else if ("rectangle".equals(shape.type)) {drawRectangle();}}private void drawCircle() {System.out.println("繪制圓形");}private void drawRectangle() {System.out.println("繪制矩形");} }
在這個設計中,如果需要添加新的圖形(如三角形),就需要修改
Drawing
類的drawShape
方法,添加新的if-else
分支,這違反了開閉原則。 -
遵循開閉原則的設計
// 抽象圖形類 abstract class Shape {public abstract void draw(); }// 圓形類 class Circle extends Shape {@Overridepublic void draw() {System.out.println("繪制圓形");} }// 矩形類 class Rectangle extends Shape {@Overridepublic void draw() {System.out.println("繪制矩形");} }// 圖形繪制類 class Drawing {public void drawShape(Shape shape) {shape.draw();} }
在這個設計中,
Shape
是抽象類,定義了抽象方法draw
。Circle
和Rectangle
是具體的圖形類,實現了draw
方法。Drawing
類的drawShape
方法通過調用Shape
對象的draw
方法來繪制圖形。當需要添加新的圖形(如三角形)時,只需要創建一個新的類繼承自Shape
,并實現draw
方法,而不需要修改Drawing
類的代碼,符合開閉原則。
6. 迪米特法則(LoD)
思想
一個對象應盡可能少地了解其他對象,具體來說,一個類對于其他類知道得越少越好,盡量降低類與類之間的耦合;一個類應該只和它的直接朋友通信,而避免和陌生的類直接通信(不要和"陌生人"說話、不要直接操作"朋友的朋友"、不要暴露內部結構給外部)
"直接朋友"包括:
- 當前對象本身(
this
):對象自身的屬性和方法可以直接訪問。 - 以參數形式傳入到當前對象方法中的對象:在方法內部可以直接使用這些參數對象。
- 當前對象的成員變量(屬性):如果當前對象包含其他對象作為成員變量,那么這些成員對象也是朋友;
- 如果當前對象的成員對象是一個集合,那么集合中的元素也都是朋友。
- 當前對象的方法所創建或實例化的對象:通過
new
關鍵字創建的對象,可在當前對象中直接使用。
典型應用模式:外觀模式、中介者模式;
示例
假設有一個學校管理系統,包含 School
類、Teacher
類和 Student
類。School
類需要統計所有學生的數量。
-
不遵循迪米特法則的設計,代碼可能如下:
// 學生類 class Student {// 學生相關屬性和方法 }// 教師類 class Teacher {private Student[] students;public Teacher(Student[] students) {this.students = students;}public Student[] getStudents() {return students;} }// 學校類 class School {private Teacher[] teachers;public School(Teacher[] teachers) {this.teachers = teachers;}public int getTotalStudents() {int total = 0;for (Teacher teacher : teachers) {Student[] students = teacher.getStudents();total += students.length;}return total;} }
在這個設計中,
School
類通過Teacher
類獲取了Student
類的信息,這使得School
類與Student
類之間產生了不必要的交互,違反了迪米特法則。School
類知道了太多關于Student
類的信息,增加了類之間的耦合度。 -
遵循迪米特法則的設計:
// 學生類 class Student {// 學生相關屬性和方法 }// 教師類 class Teacher {private Student[] students;public Teacher(Student[] students) {this.students = students;}public int getStudentCount() {return students.length;} }// 學校類 class School {private Teacher[] teachers;public School(Teacher[] teachers) {this.teachers = teachers;}public int getTotalStudents() {int total = 0;for (Teacher teacher : teachers) {total += teacher.getStudentCount();}return total;} }
在這個設計中,
School
類只與Teacher
類進行交互,通過調用Teacher
類的getStudentCount
方法來獲取學生數量,而不需要了解Student
類的具體信息。這樣,School
類對其他類的了解最少,遵循了迪米特法則,降低了類之間的耦合度。
7. 合成復用原則(CRP)
思想
優先使用對象組合或者聚合等關聯關系,其次才考慮使用繼承關系來達到復用目的。簡單來說,就是在一個新的對象里通過關聯關系(組合、聚合)來使用一些已有的對象,使之成為新對象的一部分;新對象通過委派調用已有對象的方法達到復用功能的目的,而不是通過繼承父類來獲得已有的功能。
典型應用模式:裝飾器模式、橋接模式;
組合與聚合
- 組合:是一種強 “擁有” 關系,體現了嚴格的部分和整體的關系,部分和整體的生命周期是一致的。例如,汽車和發動機的關系,發動機是汽車的一部分,沒有汽車,發動機通常沒有獨立的意義,并且發動機的生命周期和汽車的生命周期緊密相關。
- 聚合:是一種弱 “擁有” 關系,體現的是 A 對象可以包含 B 對象,但 B 對象不是 A 對象的一部分。比如,公司和員工的關系,員工是公司的一部分,但員工可以獨立于公司存在,有自己獨立的生命周期。
示例
假設要設計一個學生課程管理系統。
-
不遵循合成復用原則(使用繼承來實現復用),代碼可能如下:
// 課程類 class Course {private String courseName;private String teacher;public Course(String courseName, String teacher) {this.courseName = courseName;this.teacher = teacher;}public void showCourseInfo() {System.out.println("課程名: " + courseName + ", 授課教師: " + teacher);} }// 學生選課類,繼承自課程類 class StudentCourse extends Course {private String studentName;public StudentCourse(String courseName, String teacher, String studentName) {super(courseName, teacher);this.studentName = studentName;}public void showStudentInfo() {System.out.println("選課學生: " + studentName);} }
在這個設計中,
StudentCourse
類繼承了Course
類。然而,繼承是一種強耦合關系。要是Course
類發生改變,例如添加或修改方法,可能會對StudentCourse
類產生影響。而且,從邏輯上來說,學生選課并非是課程的一種特殊形式,這種繼承關系在語義上不太合適。 -
遵循合成復用原則(使用組合):
// 課程類 class Course {private String courseName;private String teacher;public Course(String courseName, String teacher) {this.courseName = courseName;this.teacher = teacher;}public void showCourseInfo() {System.out.println("課程名: " + courseName + ", 授課教師: " + teacher);} }// 學生類 class Student {private String studentName;private Course[] selectedCourses;public Student(String studentName, Course[] selectedCourses) {this.studentName = studentName;this.selectedCourses = selectedCourses;}public void showStudentAndCourses() {System.out.println("學生姓名: " + studentName);for (Course course : selectedCourses) {course.showCourseInfo();}} }
在這個設計里,
Student
類通過組合的方式持有Course
對象的引用。Student
類和Course
類是松耦合關系,當Course
類的實現發生變化時,只要其接口(如showCourseInfo
方法)保持不變,就不會對Student
類產生影響。同時,這種設計更符合實際邏輯,學生可以選擇多門課程,并且能靈活地對課程進行管理。
23 種設計模式
23中設計模式:(GoF23)
-
創建型模式:(5種)跟創建對象有關
單例模式、工廠模式、抽象工廠模式、建造者模式、原型模式;
-
結構型模式:(7種)
適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式;
-
行為型模式:(11種)
模板方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、解釋器
模式、狀態模式、策略模式、責任鏈模式、訪問者模式;