结构型模式是為解決怎樣組裝現有的類,設計它們的交互方式,從而達到實現一定的功能目的。結構型模式包容了對很多問題的解決。例如:擴展性(外觀、組成、代理、裝飾)、封裝(適配器、橋接)。
在解決了對象的創建問題之後,對象的組成以及對象之間的依賴關係就成了開發人員關注的焦點,因為如何設計對象的結構、繼承和依賴關係會影響到後續程序的維護性、代碼的健壯性、耦合性等。
適配器模式#
適配器模式(Adapter Pattern)是一種結構型設計模式,其核心作用是將一個類的接口轉換為客戶端期望的另一種接口,從而解決因接口不兼容而無法協同工作的類之間的協作問題。它類似於現實中的 “電源轉換器”,通過中間層接口轉換,讓原本不匹配的組件能夠協同工作。
主要組成部分#
- 目標接口(Target):客戶端期望的接口,定義了需要調用的方法。
- 被適配者(Adaptee):需要適配的接口,已存在的類或接口,但其接口與目標不兼容。
- 適配器(Adapter):連接目標與被適配者的中間層,通過繼承或組合實現接口轉換。
實現#
-
類適配器(繼承方式):適配器繼承被適配者類,並實現目標接口。靈活性較低
// 目標接口 interface Target { void request(); } // 被適配者 class Adaptee { void specificRequest() { /*...*/ } } // 適配器 class Adapter extends Adaptee implements Target { @Override void request() { specificRequest(); } }
-
對象適配器(組合方式):適配器持有被適配者的對象實例,通過委託調用方法。
class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override void request() { adaptee.specificRequest(); } }
-
接口適配器(缺省適配器):解決目標接口方法過多而客戶端僅需部分實現的問題。它通過抽象類作為中間層,為接口方法提供默認實現(通常為空或簡單邏輯),允許子類按需重寫特定方法,從而避免強制實現所有接口方法的冗餘。
// 目標接口 public interface MultiFunction { void print(); void scan(); void copy(); } // 抽象適配器類 public abstract class DefaultAdapter implements MultiFunction { @Override public void print() {} // 默認空實現 @Override public void scan() {} @Override public void copy() {} } // 具體適配器類 public class PrinterAdapter extends DefaultAdapter { @Override public void print() { System.out.println("打印功能已啟用"); } // scan()和copy()無需重寫,直接使用默認空邏輯 } // 調用 public class Client { public static void main(String[] args) { MultiFunction device = new PrinterAdapter(); device.print(); // 輸出:打印功能已啟用 } }
外觀模式#
外觀模式(Facade Pattern)是一種結構型設計模式,其核心目標是為複雜的子系統提供一個統一的、簡化的接口,從而隱藏系統內部的複雜性,降低客戶端與子系統之間的耦合度,提升系統的易用性和可維護性。
主要組成部分#
通過一個門面類(Facade) 封裝多個子系統的交互邏輯,對外提供高層次的接口。客戶端只需與門面類交互,無需直接調用複雜的子系統組件。
- 門面角色(Facade):統一協調子系統的調用,對外提供簡化接口。
- 子系統角色(Subsystem Classes):實現具體功能的模塊。
- 客戶端(Client)、
橋接模式#
2. 橋接模式 — Graphic Design Patterns
設想如果要繪製矩形、圓形、橢圓、正方形,我們至少需要 4 個形狀類,但是如果繪製的圖形需要具有不同的顏色,如紅色、綠色、藍色等,此時至少有如下兩種設計方案:
- 第一種設計方案是為每一種形狀都提供一套各種顏色的版本。
- 第二種設計方案是根據實際需要對形狀和顏色進行組合
橋接模式將繼承關係轉換為關聯關係,從而降低了類與類之間的耦合,減少了代碼編寫量。將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
橋接模式的構造#
實現案例#
橋接模式(Bridge Pattern)-(最通俗易懂的案例)_橋接器模式實例 - CSDN 博客
組合模式#
將這棵樹理解成一個大的容器,容器裡面包含很多的成員對象,這些對象即可以是容器對象也可以是葉子對象。但是由於容器對象和葉子對象在功能上的區別,使得我們在使用的過程中必須要區分容器對象和葉子對象,但是這樣就會給客戶帶來不必要的麻煩,作為客戶,它希望能夠一致的對待容器對象和葉子對象。這就是組合模式的設計動機:組合模式定義了如何將容器對象和葉子對象進行遞歸組合,使得客戶在使用的過程中無需區分,可以對它們進行一致的處理。
組合模式(Composite Pattern) 也稱為 整體 - 部分(Part-Whole)模式,它的宗旨是通過將單個對象(葉子節點)和組合對象(樹枝節點)用相同的接口進行表示,使得客戶對單個對象和組合對象的使用具有一致性。
組合模式結構#
組合模式分為透明式的組合模式和安全式的組合模式。在該方式中,由於抽象構件聲明了所有子類中的全部方法,所以客戶端無須區別樹葉對象和樹枝對象,對客戶端來說是透明的。但其缺點是:樹葉構件本來沒有 Add ()、Remove () 及 GetChild () 方法,卻要實現它們(空實現或拋異常),這樣會帶來一些安全性問題。
透明模式的實現#
public class CompositePattern {
public static void main(String[] args) {
Component c0 = new Composite();
Component c1 = new Composite();
Component leaf1 = new Leaf("1");
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c0.add(leaf1);
c0.add(c1);
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
//抽象構件
interface Component {
public void add(Component c);
public void remove(Component c);
public Component getChild(int i);
public void operation();
}
//樹葉構件
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
public void add(Component c) {
}
public void remove(Component c) {
}
public Component getChild(int i) {
return null;
}
public void operation() {
System.out.println("樹葉" + name + ":被訪問!");
}
}
//樹枝構件
class Composite implements Component {
private ArrayList<Component> children = new ArrayList<Component>();
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
public void operation() {
for (Object obj : children) {
((Component) obj).operation();
}
}
}
安全模式實現#
由於葉子和分支有不同的接口,客戶端在調用時要知道樹葉對象和樹枝對象的存在,所以失去了透明性。
Leaf 類不需要實現那些對於它來說無用的方法,如 add、remove 和 getChild,這使得設計更加合理且易於理解。同時,這樣的設計也避免了潛在的錯誤
代理模式#
控制對一個對象的訪問,通過引入一個代理對象來管理這個訪問。代理模式允許在不改變原對象接口的情況下,增加額外的功能或控制訪問方式。
主要組成部分#
- Subject(目標接口):
- 定義了代理類和真實主題類的共同接口。
- 這使得客戶端可以在不改變代碼的情況下,使用代理或者真實主題對象。
- Real Subject(真實主題):
- 實現了
Subject
接口的具體業務邏輯。 - 這是客戶端真正需要訪問的對象。
- 實現了
- Proxy(代理):
- 也實現了
Subject
接口,並且維護一個對Real Subject
的引用。 - 代理類負責在客戶端和真實主題之間進行協調,可能在請求前後執行一些額外的操作。
- 也實現了
靜態代理#
一個代理只能服務於一個特定的業務實現類
1. 定義目標接口
首先,定義一個接口,該接口聲明了需要被代理的方法。
JAVApublic interface Subject {
void request();
}
2. 實現真實主題類
創建一個實現了上述接口的具體類,該類包含實際的業務邏輯。
JAVApublic class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: 處理請求");
}
}
3. 創建代理類
代理類也實現相同的接口,並持有一個對真實主題對象的引用。代理類可以在調用真實主題的方法之前或之後添加額外的功能。
JAVApublic class Proxy implements Subject {
private RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
// 執行預處理操作
System.out.println("Proxy: 執行預處理操作");
realSubject.request();
// 執行後處理操作
System.out.println("Proxy: 執行後處理操作");
}
}
4. 編寫客戶端代碼
客戶端通過接口與代理類或真實主題對象交互,無需知道是直接調用還是通過代理。
JAVApublic class Client {
public static void main(String[] args) {
// 創建真實主題對象
Subject realSubject = new RealSubject();
// 創建代理對象,並將真實主題傳遞給代理
Subject proxy = new Proxy(realSubject);
// 調用代理的方法
proxy.request();
}
}
動態代理#
動態代理採用反射的機制,在運行時創建一個接口類的實例。
//業務接口
interface DateService {
void add();
void del();
}
// 具體業務類
class DateServiceImplA implements DateService {
@Override
public void add() {
System.out.println("成功添加!");
}
@Override
public void del() {
System.out.println("成功刪除!");
}
}
// 代理
class ProxyInvocationHandler implements InvocationHandler {
private DateService service;
public ProxyInvocationHandler(DateService service) {
this.service = service;
}
// this.getClass().getClassLoader() 第一个是类加载器,用来加载代理类。
// service.getClass().getInterfaces() 被代理类实现的所有接口,使得代理对象能够提供与目标对象相同的接口视图。
// this 当前实例(即实现了 InvocationHandler 接口的对象),用于处理对代理对象的方法调用。
public Object getDateServiceProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
}
/* invoke 方法:
重寫的 invoke 方法會在代理對象上調用任何方法時觸發。它接收三個參數:
Object proxy:代表代理對象本身。
Method method:表示正在被調用的目標方法。
Object[] args:傳遞給目標方法的參數列表。
在方法體內,首先通過 method.invoke(service, args); 調用了目標對象的實際方法,並將結果保存到 result 變量中。
然後打印出一條日志信息,表明代理對象執行了某個方法,並記錄了方法名和返回值。
最後返回目標方法的執行結果。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
var result = method.invoke(service, args); // 讓service調用方法,方法返回值
System.out.println(proxy.getClass().getName() + "代理類執行" + method.getName() + "方法,返回" + result + ",記錄日志!");
return result;
}
}
//客戶端
public class Test {
public static void main(String[] args) {
DateService serviceA = new DateServiceImplA();
DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
serviceProxy.add();
serviceProxy.del();
}
}
裝飾模式#
裝飾模式(Decorator Pattern)-(最通俗易懂的案例)_裝飾模式使用例子 - CSDN 博客
裝飾模式(Decorator),動態地給一個對象添加一些額外的職責,就增加功能來說,裝飾模式比生成子類更為靈活。裝飾模式屬於結構型模式,它是作為現有的類的一個包裝。
裝飾模式的主要角色包括:
-
Component(抽象組件):定義了被裝飾對象和裝飾器共同的接口。定義一個對象接口,可以給這些對象動態地添加職責。
-
ConcreteComponent(具體組件):實現了 Component 接口,是被裝飾的對象。定義了一個具體的對象,也可以給這個對象添加一些職責。
-
Decorator(抽象裝飾器):持有一個 Component 對象的引用,並定義了一個與 Component 具有相同接口的方法,以保持一致性。
-
ConcreteDecorator(具體裝飾器):實現了 Decorator 定義的方法,在調用被裝飾對象的方法前後可以執行一些額外的操作。
具體實現#
// 抽象組件
public interface INoodles {
public void cook();
}
// 具體組件
public class Noodles implements INoodles {
@Override
public void cook() {
System.out.println("的面條");
}
}
// 抽象裝飾類
public abstract class NoodlesDecorator implements INoodles {
private INoodles noodles; //添加一個INoodles 的引用
public NoodlesDecorator(INoodles noodles){ //通過構造器來設置INoodles
this.noodles = noodles;
}
@Override
public void cook() {
if (noodles!=null){
noodles.cook();
}
}
}
// 具體裝飾類
public class EggDecorator extends NoodlesDecorator {
public EggDecorator(INoodles noodles) {
super(noodles);
}
/**
* 重寫父類的cook方法,並添加自己的實現,調用父類的cook方法,此cook方法是通過本類的構造器
* EggDecorator(INoodles noodles)傳入的noodles的cook操作
*/
@Override
public void cook() {
System.out.println("加了一个荷包蛋");
super.cook();
}
}
// 具體裝飾類
public class BeefDecorator extends NoodlesDecorator {
public BeefDecorator(INoodles noodles) {
super(noodles);
}
@Override
public void cook() {
System.out.println("加了一斤牛肉");
super.cook();
}
}
public class Client {
public static void main(String[] args) {
INoodles noodles1 = new Noodles();
INoodles noodles1WithEgg = new EggDecorator(noodles1);
noodles1WithEgg.cook();
}
}
裝飾模式和代理模式的區別#
- 意圖不同:裝飾模式關注的是增強對象的功能,而代理模式更注重於控制對對象的訪問。
- 實現細節:在裝飾模式中,裝飾器和被裝飾的對象必須實現相同的接口,這樣才能保證裝飾鏈中的對象能夠相互替換。而在代理模式中,代理類和目標對象也常常實現相同的接口,但這主要是為了保持一致性,並不是必須的條件。
- 設計考量:使用裝飾模式時,設計者更多考慮的是如何在不修改現有代碼的情況下擴展功能;而使用代理模式時,則更多地考慮如何保護、管理對資源的訪問。