23種設計模式
結構型模式
適配器模式
基本介紹
- 適配器模式(Adapter Pattern)將某個類的接口轉換成客戶端期望的另一個接口表示,主要目的是兼容性,讓原本因接口不匹配不能一起工作的兩個類可以協同工作。其別名爲包裝器(Wrapper)。
- 適配器模式屬於結構型模式。
- 主要分爲三類:類適配器模式、對象適配器模式、接口適配器模式。
適配器模式工作原理
- 適配器模式將一個類的接口轉換成另一種接口讓原本接口不兼容的類可以兼容,從用戶的角度看不到被適配者,是解耦的。
- 用戶調用適配器轉化出來的目標接口方法,適配器再調用被適配者的相關接口方法。
- 用戶收到反饋結果,感覺只是和目標接口交互,如圖:
類適配器模式
- 基本介紹:Adapter類,通過繼承 src類,實現 dst 類接口,完成 src 到 dst 的適配。
- 應用實例:以生活中充電器的例子來講解適配器,充電器本身相當於Adapter,220V交流電相當於src (即被適配者),我們的目dst(即 目標)是5V直流電。
- UML類圖:
- 代碼實現:
/**
* 適配器模式
*/
public class AdapterCase01 {
public static void main(String[] args) {
new Phone().charging(new ClassVoltageAdapter());
}
}
/**
* 被適配的類
*/
class Voltage220V {
public int output220V() {
int src = 220;
System.out.println("電壓" + src + "V");
return src;
}
}
/**
* 適配接口
*/
interface IVoltage5V {
int output5V();
}
/**
* 手機
*/
class Phone {
void charging(IVoltage5V voltage5V) {
int output = voltage5V.output5V();
if (output == 5) {
System.out.println("電壓爲5V,可以充電");
} else if (output > 5) {
System.out.println("電壓大於5V,不可以充電");
}
}
}
/**
* 類適配器
*/
class ClassVoltageAdapter extends Voltage220V implements IVoltage5V {
@Override
public int output5V() {
System.out.println("----類適配器----");
int srcV = output220V();
int dstV = srcV / 44;
return dstV;
}
}
- Java是單繼承機制,所以類適配器需要繼承src類這一點算是一個缺點,因爲這要求dst必須是接口,有一定侷限性。
- src類的方法在Adapter中都會暴露出來,也增加了使用的成本。
- 由於其繼承了src類,所以它可以根據需求重寫src類的方法,使得Adapter的靈活性增強了。
對象適配器模式
- 基本思路和類的適配器模式相同,只是將Adapter類作修改,不是繼承src類,而是持有src類的實例,以解決兼容性的問題。 即持有 src類,實現 dst 類接口,完成src到dst的適配。
- 根據“合成複用原則”,在系統中儘量使用關聯關係來替代繼承關係。
- 對象適配器模式是適配器模式常用的一種。
- UML類圖:
- 代碼實現:
/**
* 對象適配器
*/
class ObjectVoltageAdapter implements IVoltage5V {
private Voltage220V voltage220V;
ObjectVoltageAdapter(Voltage220V voltage220V) {
this.voltage220V = voltage220V;
}
@Override
public int output5V() {
System.out.println("----對象適配器----");
int srcV = voltage220V.output220V();
int dstV = srcV / 44;
return dstV;
}
}
- 對象適配器和類適配器其實算是同一種思想,只不過實現方式不同。根據合成複用原則,使用組合替代繼承, 所以它解決了類適配器必須繼承src的侷限性問題,也不再要求dst必須是接口。
- 使用成本更低,更靈活。
接口適配器模式
- 一些書籍稱適配器模式爲(Default Adapter Pattern)或缺省適配器模式。
- 當不需要全部實現接口提供的方法時,可先設計一個抽象類實現接口,併爲該接口中每個方法提供一個默認實現(空方法),那麼該抽象類的子類可有選擇地覆蓋父類的某些方法來實現需求。
- 適用於一個接口不想使用其所有的方法的情況。
- UML類圖:
- 代碼實現:
public class AdapterCase02 {
public static void main(String[] args) {
AbstractAadpter adpter = new AbstractAadpter() {
@Override
public void m1() {
System.out.println("m1");
}
};
adpter.m1();
}
}
interface InterfaceA {
void m1();
void m2();
void m3();
void m4();
}
abstract class AbstractAadpter implements InterfaceA {
@Override public void m1() {}
@Override public void m2() {}
@Override public void m3() {}
@Override public void m4() {}
}
適配器模式在SpringMVC框架應用的源碼分析
- SpringMvc中的HandlerAdapter,就使用了適配器模式。
- 可以看到處理器的類型不同,有多重實現方式,那麼調用方式就不是確定的,如果需要直接調用 Controller 方法,需要調用的時候就得不斷是使用 if else 來進行判斷是哪一種子類然後執行。那麼如果後面要擴展 Controller,得修改原來的代碼,這樣違背了 OCP 原則。
- Spring定義了一個適配接口,使得每一種Controller有一種對應的適配器實現類。
- 適配器代替controller執行相應的方法。
- 擴展Controller 時,只需要增加一個適配器類就完成了SpringMVC的擴展了,這就是設計模式的力量。
- 動手寫SpringMVC通過適配器設計模式獲取到對應的Controller的源碼:
- UML類圖:
- 代碼實現:
// 處理器
public interface Handler {
void doHandle();
}
class SimpleHandler implements Handler {
@Override
public void doHandle() {
System.out.println("SimpleHandler.doHandle() ...");
}
}
class HttpHandler implements Handler {
@Override
public void doHandle() {
System.out.println("HttpHandler.doHandle() ...");
}
}
class AnnotationHandler implements Handler {
@Override
public void doHandle() {
System.out.println("AnnotationHandler.doHandle() ...");
}
}
// 處理器適配器
public interface HandlerAdapter {
boolean support(Handler handler);
void handle(Handler handler);
}
class SimpleHandlerAdapter implements HandlerAdapter {
@Override
public boolean support(Handler handler) {
return handler instanceof SimpleHandler;
}
@Override
public void handle(Handler handler) {
handler.doHandle();
}
}
class HttpHandlerAdapter implements HandlerAdapter {
@Override
public boolean support(Handler handler) {
return handler instanceof HttpHandler;
}
@Override
public void handle(Handler handler) {
handler.doHandle();
}
}
class AnnotationHandlerAdapter implements HandlerAdapter {
@Override
public boolean support(Handler handler) {
return handler instanceof AnnotationHandler;
}
@Override
public void handle(Handler handler) {
handler.doHandle();
}
}
// 分派
public class DispatchServlet {
private static List<HandlerAdapter> handlerAdapters = new ArrayList<>();
public DispatchServlet() {
handlerAdapters.add(new SimpleHandlerAdapter());
handlerAdapters.add(new HttpHandlerAdapter());
handlerAdapters.add(new AnnotationHandlerAdapter());
}
public void doDispatch() {
HttpHandler handler = new HttpHandler();
HandlerAdapter adapter = getHandler(handler);
if (adapter != null) {
adapter.handle(handler);
}
}
private HandlerAdapter getHandler(Handler handler) {
for (HandlerAdapter adapter : handlerAdapters) {
if (adapter.support(handler)) {
return adapter;
}
}
return null;
}
public static void main(String[] args) {
new DispatchServlet().doDispatch();
}
}
適配器模式的注意事項和細節
- 三種命名方式,是根據 src是以怎樣的形式給到Adapter(在Adapter裏的形式)來命名的。
- 類適配器:以類給到,在Adapter裏,就是將src當做類,繼承。
- 對象適配器:以對象給到,在Adapter裏,將src作爲一個對象,持有。
- 接口適配器:以接口給到,在Adapter裏,將src作爲一個接口,實現。
- Adapter模式最大的作用還是將原本不兼容的接口融合在一起工作。
橋接模式
手機操作問題
現在對不同手機類型的不同品牌實現操作編程(比如:開機、關機、上網,打電話等),如圖:
傳統方案解決手機操作問題
- 擴展性問題(類爆炸):如果我們再增加手機的樣式(旋轉式),就需要增加各個品牌手機的類,同樣如果我們增加一個手機品牌,也要在各個手機樣式類下增加。
- 違反了單一職責原則,當我們增加手機樣式時,要同時增加所有品牌的手機,這樣增加了代碼維護成本。
- 解決方案-使用橋接模式。
橋接模式(Bridge)-基本介紹
- 橋接模式(Bridge模式)是指:將實現與抽象放在兩個不同的類層次中,使兩個層次可以獨立改變,是一種結構型設計模式。
- Bridge模式基於類的最小設計原則,通過使用封裝、聚合及繼承等行爲讓不同的類承擔不同的職責。它的主要特點是把抽象(Abstraction)與行爲實現(Implementation)分離開來,從而可以保持各部分的獨立性以及應對他們的功能擴展。
- UML原理類圖:
* 抽象類(Abstraction):維護了Implementor,及其實現子類,兩者是聚合關係,Abstraction充當橋接類
* RefinedAbstraction:是Abstraction抽象類的實現子類
* Implementor:是行爲實現類的接口
* ConcreteImplementorA、ConcreteImplementorB:是行爲的具體實現類
- 代碼實現:
public class BridgeCase01 {
public static void main(String[] args) {
Phone foldedXiaoMi = new FoldedPhone(new XiaoMi());
foldedXiaoMi.open();
foldedXiaoMi.close();
foldedXiaoMi.call();
System.out.println("===============");
Phone foldedViVo = new FoldedPhone(new ViVo());
foldedViVo.open();
foldedViVo.close();
foldedViVo.call();
}
}
/**
* 手機品牌:Implementor行爲實現類的接口
*/
interface Brand {
void open();
void close();
void call();
}
/**
* 小米手機:行爲的具體實現類
*/
class XiaoMi implements Brand {
@Override
public void open() {
System.out.println("小米手機開機");
}
@Override
public void close() {
System.out.println("小米手機關機");
}
@Override
public void call() {
System.out.println("小米手機打電話");
}
}
/**
* ViVo手機:行爲的具體實現類
*/
class ViVo implements Brand {
@Override
public void open() {
System.out.println("ViVo手機開機");
}
@Override
public void close() {
System.out.println("ViVo手機關機");
}
@Override
public void call() {
System.out.println("ViVo手機打電話");
}
}
/**
* 手機樣式:抽象類(Abstraction)
*/
abstract class Phone {
/**
* 組合進 品牌
*/
private Brand brand;
Phone(Brand brand) {
this.brand = brand;
}
protected void open() {
this.brand.open();
}
protected void close() {
this.brand.close();
}
protected void call() {
this.brand.call();
}
}
/**
* 手機樣式具體的實現子類
*/
class FoldedPhone extends Phone {
private static final String STYLE = "摺疊樣式";
FoldedPhone(Brand brand) {
super(brand);
}
@Override
public void open() {
System.out.print(STYLE);
super.open();
}
@Override
public void close() {
System.out.print(STYLE);
super.close();
}
@Override
public void call() {
System.out.print(STYLE);
super.call();
}
}
//摺疊樣式小米手機開機
//摺疊樣式小米手機關機
//摺疊樣式小米手機打電話
//===============
//摺疊樣式ViVo手機開機
//摺疊樣式ViVo手機關機
//摺疊樣式ViVo手機打電話
- 如果此時需要增加一種樣式,比如直立式:
class UpRightPhone extends Phone {
private static final String STYLE = "直立樣式";
UpRightPhone(Brand brand) {
super(brand);
}
@Override
public void open() {
System.out.print(STYLE);
super.open();
}
@Override
public void close() {
System.out.print(STYLE);
super.close();
}
@Override
public void call() {
System.out.print(STYLE);
super.call();
}
}
public class BridgeCase01 {
public static void main(String[] args) {
Phone upRightXiaomi = new UpRightPhone(new XiaoMi());
upRightXiaomi.open();
upRightXiaomi.close();
upRightXiaomi.call();
}
}
//直立樣式小米手機開機
//直立樣式小米手機關機
//直立樣式小米手機打電話
橋接模式在JDBC的源碼剖析
- Jdbc的Driver接口,如果從橋接模式來看,Driver就是一個接口,下面可以有MySQL的Driver,Oracle的Driver,這些就可以當做實現接口類。
橋接模式的注意事項和細節
- 實現了抽象和實現部分的分離,從而極大的提供了系統的靈活性,讓抽象部分和實現部分獨立開來,這有助於系統進行分層設計,從而產生更好的結構化系統。
- 對於系統的高層部分,只需要知道抽象部分和實現部分的接口就可以了,其它的部分由具體業務來完成。
- 橋接模式替代多層繼承方案,可以減少子類的個數,降低系統的管理和維護成本。
- 橋接模式的引入增加了系統的理解和設計難度,由於聚合關聯關係建立在抽象層,要求開發者針對抽象進行設計和編程。
- 橋接模式要求正確識別出系統中兩個獨立變化的維度【抽象與實現】,因此其使用範圍有一定的侷限性,即需要有這樣的應用場景。
橋接模式其它應用場景
- 對於那些不希望使用繼承或因爲多層次繼承導致系統類的個數急劇增加的系統,橋接模式尤爲適用。
- 常見的應用場景:JDBC驅動程序、銀行轉賬系統【轉賬分類:網上轉賬、櫃檯轉賬、AMT轉賬…;轉賬用戶類型:普通用戶、銀卡用戶、金卡用戶…】、消息管理【消息類型:即時消息、延時消息…;消息分類:手機短信、郵件消息、QQ消息…】。
裝飾器模式
星巴克咖啡訂單項目
- 咖啡種類/單品咖啡:Espresso(意大利濃咖啡)、ShortBlack、LongBlack(美式咖啡)、Decaf(無因咖啡)。
- 調料:Milk、Soy(豆漿)、Chocolate。
- 要求在擴展新的咖啡種類時,具有良好的擴展性、改動方便、維護方便。
- 使用OO的來計算不同種類咖啡的費用:客戶可以點單品咖啡,也可以單品咖啡+調料組合。
方案一
- Drink 是一個抽象類,表示飲料;description 就是對咖啡的描述,比如咖啡的名字;cost()方法就是計算費用,Drink類中做成一個抽象方法。
- Decaf 就是單品咖啡, 繼承Drink,並實現cost;Espress && Milk 就是單品咖啡+調料, 這個組合很多。
- 問題:這樣設計,會有很多類,當我們增加一個單品咖啡,或者一個新的調料,類的數量就會倍增,就會出現類爆炸。
方案二
- 前面分析到方案1因爲咖啡單品+調料組合會造成類的倍增,因此可以做改進,將調料內置到Drink類,這樣就不會造成類數量過多。從而提高項目的維護性(如圖):
- 方案2可以控制類的數量,不至於造成很多的類,但是在增加或者刪除調料種類時,代碼的維護量很大。
- 考慮到用戶可以添加多份調料時,可以將 hasMilk 返回一個對應int。
- 方案三:考慮使用裝飾者模式。
裝飾者模式定義
- 動態的將新功能附加到對象上。在對象功能擴展方面,它比繼承更有彈性,裝飾者模式也體現了開閉原則(ocp)。
- 這裏提到的動態的將新功能附加到對象和ocp原則,在後面的應用實例上會以代碼的形式體現。
裝飾者模式(Decorator)原理
- UML類圖:
- 裝飾者模式就像打包一個快遞,主體比如:陶瓷、衣服 (Component) 【被裝飾者】;包裝比如:報紙填充、塑料泡沫、紙板、木板(Decorator)。
- Component:主體,比如類似前面的Drink。
- ConcreteComponent:具體的主體,比如前面的各個單品咖啡。
- Decorator:裝飾者,比如各調料。
- 在如圖的Component與ConcreteComponent之間,如果ConcreteComponent類很多,還可以設計一個緩衝層,將共有的部分提取出來,抽象層一個類。
裝飾者模式解決星巴克咖啡訂單
- 設計方案:
- 裝飾者模式下的訂單:2份巧克力+一份牛奶的LongBlack
- 代碼實現:
public class DecoratorCase {
public static void main(String[] args) {
// 2份巧克力+一份牛奶的 LongBlack
// 1、點一份 LongBlack
Drink order = new LongBlack();
System.out.println("費用1 = " + order.getCost() + ",描述 = " + order.getDesc());
// 2、order加入一份牛奶
order = new Milk(order);
System.out.println("費用2 = " + order.getCost() + ",描述 = " + order.getDesc());
// 3、order加入一份巧克力
order = new Chocolate(order);
System.out.println("費用3 = " + order.getCost() + ",描述 = " + order.getDesc());
// 4、order再加入一份巧克力
order = new Chocolate(order);
System.out.println("費用4 = " + order.getCost() + ",描述 = " + order.getDesc());
}
}
/**
* 被裝飾者:Component
*/
@Data
abstract class Drink {
private String desc;
private float price;
abstract float getCost();
}
class Coffee extends Drink {
@Override
float getCost() {
return super.getPrice();
}
}
/**
* 具體的被裝飾者
*/
class Espresso extends Coffee {
Espresso() {
setDesc("意大利咖啡");
setPrice(6.0f);
}
}
class LongBlack extends Coffee {
LongBlack() {
setDesc("LongBlack");
setPrice(5.0f);
}
}
class ShortBlack extends Coffee {
ShortBlack() {
setDesc("ShortBlack");
setPrice(4.0f);
}
}
/**
* 裝飾者
*/
class Decorator extends Drink {
private Drink drink;
Decorator(Drink drink) {
this.drink = drink;
}
@Override
float getCost() {
return super.getPrice() + drink.getCost();
}
@Override
public String getDesc() {
// drink.getDesc() 是被裝飾者的信息
return super.getDesc() + " " + super.getPrice() + " && " + drink.getDesc();
}
}
/**
* 具體的裝飾者:調味品
*/
class Chocolate extends Decorator {
Chocolate(Drink drink) {
super(drink);
setDesc(" 巧克力 ");
// 調味品的價格
setPrice(3.0f);
}
}
class Milk extends Decorator {
Milk(Drink drink) {
super(drink);
setDesc(" 牛奶 ");
setPrice(2.0f);
}
}
class Soy extends Decorator {
Soy(Drink drink) {
super(drink);
setDesc(" 豆漿 ");
setPrice(1.5f);
}
}
裝飾者模式在JDK應用的源碼分析
- Java的IO結構,FilterInputStream就是一個裝飾者
public abstract class InputStream implements Closeable{} // 是一個抽象類,即Component
public class FilterInputStream extends InputStream {// 是一個裝飾者類Decorator
protected volatile InputStream in; // 被裝飾的對象
}
public class DataInputStream extends FilterInputStream implements DataInput {} // 是 FilterInputStream 子類,也繼承了被裝飾的對象 InputStream
組合模式
學校院系展示需求
- 編寫程序展示一個學校院系結構:需求是這樣,要在一個頁面中展示出學校的院系組成,一個學校有多個學院,一個學院有多個系。
- 將學院看做是學校的子類,系是學院的子類,這樣實際上是站在組織大小來進行分層次的。
- 實際上我們的要求是:在一個頁面中展示出學校的院系組成,一個學校有多個學院,一個學院有多個系, 因此這種方案,不能很好實現的管理的操作,比如對學院、系的添加,刪除,遍歷等。
- 解決方案:把學校、院、系都看做是組織結構,他們之間沒有繼承的關係,而是一個樹形結構,可以更好的實現管理操作。 => 組合模式
組合模式基本介紹
- 組合模式(Composite Pattern),又叫部分整體模式,它創建了對象組的樹形結構,將對象組合成樹狀結構以表示“整體-部分”的層次關係。
- 組合模式依據樹形結構來組合對象,用來表示部分以及整體層次。
- 這種類型的設計模式屬於結構型模式。
- 組合模式使得用戶對單個對象和組合對象的訪問具有一致性,即:組合能讓客戶以一致的方式處理個別對象以及組合對象。
- UML類圖:
* Component : 這是組合中對象聲明接口,在適當情況下,實現所有類共有的接口默認行爲,用於訪問和管理Component 子部件, Component 可以是抽象類或者接口
* Leaf : 在組合中表示葉子節點,葉子節點沒有子節點
* Composite : 非葉子節點, 用於存儲子部件, 在 Component 接口中實現 子部件的相關操作,比如增加(add),刪除
- 組合模式解決這樣的問題:當我們的要處理的對象可以生成一顆樹形結構,而我們要對樹上的節點和葉子進行操作時,它能夠提供一致的方式,而不用考慮它是節點還是葉子。
組合模式解決學校院系展示
- 思路分析:
- 代碼實現:
public class CompositeCase {
public static void main(String[] args) {
// 從大到小創建對象
University university = new University("清華大學");
College college1 = new College("計算機學院");
College college2 = new College("信息工程學院");
college1.add(new Department("軟件工程"));
college1.add(new Department("網絡安全"));
college1.add(new Department("計算機科學與技術"));
college2.add(new Department("通信工程"));
college2.add(new Department("信息工程"));
// college1.print();
university.add(college1);
university.add(college2);
university.print();
}
}
@Data
abstract class OrgComponent {
private String name;
OrgComponent(String name){
this.name = name;
}
protected void add(OrgComponent component) {
// 默認實現
throw new UnsupportedOperationException();
}
protected void remove(OrgComponent component) {
// 默認實現
throw new UnsupportedOperationException();
}
protected void get(int index) {
// 默認實現
throw new UnsupportedOperationException();
}
abstract void print();
}
class University extends OrgComponent {
List<OrgComponent> components = new ArrayList<>();
University(String name) {
super(name);
}
@Override
public void add(OrgComponent component) {
components.add(component);
}
@Override
public void remove(OrgComponent component) {
components.remove(component);
}
@Override
public void get(int index) {
components.get(index);
}
@Override
public void print() {
System.out.println("--------" + getName() +"--------");
if (components.size() > 0) {
for (OrgComponent component : components) {
component.print();
}
}
}
}
class College extends OrgComponent {
List<OrgComponent> components = new ArrayList<>();
College(String name) {
super(name);
}
@Override
public void add(OrgComponent component) {
// 實際業務中,add方法不一定完全一樣
components.add(component);
}
@Override
public void remove(OrgComponent component) {
components.remove(component);
}
@Override
public void get(int index) {
components.get(index);
}
@Override
public void print() {
System.out.println("--------" + getName() +"--------");
if (components.size() > 0) {
for (OrgComponent component : components) {
component.print();
}
}
}
}
/**
* 葉子結點
*/
class Department extends OrgComponent {
Department(String name) {
super(name);
}
@Override
public void print() {
System.out.println(getName());
}
}
組合模式在JDK集合的源碼分析
- Java的集合類-HashMap就使用了組合模式,類圖:
public class JDKComposite {
public static void main(String[] args) {
/*
Map 就是一個抽象的構建(類似組合模式裏面的Component)
HashMap是一箇中間的構建(Composite),實現/繼承了相關方法:put、putAll
Node 是 HasMap 的靜態內部類,類似於 Leaf 葉子結點,沒有 put、putAll 方法
*/
Map<Integer, String> hashMap = new HashMap<>();
hashMap.put(0, "西遊記");
Map<Integer, String> map = new HashMap<>();
map.put(1, "水滸傳");
map.put(2, "紅樓夢");
hashMap.putAll(map);
System.out.println(hashMap);
}
}
組合模式的注意事項和細節
- 組合模式簡化了客戶端操作,客戶端只需要面對一致的對象而不用考慮整體部分或者節點葉子的問題。
- 具有較強的擴展性,當我們要更改組合對象時,我們只需要調整內部的層次關係客戶端不用做出任何改動。
- 方便創建出複雜的層次結構,客戶端不用理會組合裏面的組成細節,容易添加節點或者葉子從而創建出複雜的樹形結構。
- 需要遍歷組織機構,或者處理的對象具有樹形結構時,非常適合使用組合模式。
- 要求較高的抽象性,如果節點和葉子有很多差異性的話,比如很多方法和屬性都不一樣,不適合使用組合模式。
外觀模式
影院管理項目
- 組建一個家庭影院:DVD播放器、投影儀、自動屏幕、環繞立體聲、爆米花機,要求完成使用家庭影院的功能,其過程爲:
• 直接用遙控器:統籌各設備開關 • 開爆米花機
• 放下屏幕
• 開投影儀
• 開音響
• 開DVD,選dvd
• 去拿爆米花
• 調暗燈光
• 播放
• 觀影結束後,關閉各種設備
傳統方式解決影院管理
- 在ClientTest 的main方法中,創建各個子系統的對象,並直接去調用子系統(對象)相關方法,會造成調用過程混亂,沒有清晰的過程。
- 不利於在ClientTest 中,去維護對子系統的操作。
- 解決思路:定義一個高層接口,給子系統中的一組接口提供一個一致的界面(比如在高層接口提供四個方法 ready, play, pause, end ),用來訪問子系統中的一羣接口。
- 也就是說 就是通過定義一個一致的接口(界面類),用以屏蔽內部子系統的細節,使得調用端只需跟這個接口發生調用,而無需關心這個子系統的內部細節 => 外觀模式。
外觀模式基本介紹
- 外觀模式(Facade),也叫“過程模式:外觀模式爲子系統中的一組接口提供一個一致的界面,此模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。
- 外觀模式通過定義一個一致的接口,用以屏蔽內部子系統的細節,使得調用端只需跟這個接口發生調用,而無需關心這個子系統的內部細節。
外觀模式原理類圖
- 外觀類(Facade):爲調用端提供統一的調用接口, 外觀類知道哪些子系統負責處理請求,從而將調用端的請求代理給適當子系統對象。
- 調用者(Client):外觀接口的調用者。
- 子系統的集合:指模塊或者子系統,處理Facade 對象指派的任務,他是功能的實際提供者。
外觀模式解決影院管理
- 代碼實現:
public class FacadeCase {
public static void main(String[] args) {
// 如果直接調用,很不方便
HomeTheaterFacade facade = new HomeTheaterFacade();
facade.ready();
facade.play();
facade.end();
}
}
class DVDPlayer {
private DVDPlayer() {}
private static DVDPlayer INSTANCE = new DVDPlayer();
public static DVDPlayer getInstance() {
return INSTANCE;
}
public void on() {
System.out.println("DVD on");
}
public void off() {
System.out.println("DVD off");
}
public void play() {
System.out.println("DVD playing");
}
public void pause() {
System.out.println("DVD paused");
}
}
class Popcorn {
private Popcorn() {}
private static Popcorn INSTANCE = new Popcorn();
public static Popcorn getInstance() {
return INSTANCE;
}
public void on() {
System.out.println("Popcorn on");
}
public void off() {
System.out.println("Popcorn off");
}
public void pop() {
System.out.println("Popcorn popping");
}
}
class Projector {
private Projector() {}
private static Projector INSTANCE = new Projector();
public static Projector getInstance() {
return INSTANCE;
}
public void on() {
System.out.println("Projector on");
}
public void off() {
System.out.println("Projector off");
}
public void focus() {
System.out.println("Projector focus");
}
}
class Screen {
private Screen() {}
private static Screen INSTANCE = new Screen();
public static Screen getInstance() {
return INSTANCE;
}
public void up() {
System.out.println("Screen up");
}
public void down() {
System.out.println("Screen down");
}
}
class Stereo {
private Stereo() {}
private static Stereo INSTANCE = new Stereo();
public static Stereo getInstance() {
return INSTANCE;
}
public void on() {
System.out.println("Stereo on");
}
public void off() {
System.out.println("Stereo off");
}
public void up() {
System.out.println("Stereo up");
}
public void down() {
System.out.println("Stereo down");
}
}
class TheaterLight {
private TheaterLight() {}
private static TheaterLight INSTANCE = new TheaterLight();
public static TheaterLight getInstance() {
return INSTANCE;
}
public void on() {
System.out.println("TheaterLight on");
}
public void off() {
System.out.println("TheaterLight off");
}
public void dim() {
System.out.println("TheaterLight dim");
}
public void bright() {
System.out.println("TheaterLight bright");
}
}
class HomeTheaterFacade {
/**
* 定義各個子系統的對象
*/
private DVDPlayer dvdPlayer;
private Popcorn popcorn;
private Projector projector;
private Screen screen;
private Stereo stereo;
private TheaterLight theaterLight;
public HomeTheaterFacade() {
this.dvdPlayer = DVDPlayer.getInstance();
this.popcorn = Popcorn.getInstance();
this.projector = Projector.getInstance();
this.screen = Screen.getInstance();
this.stereo = Stereo.getInstance();
this.theaterLight = TheaterLight.getInstance();
}
public void ready(){
popcorn.on();
popcorn.pop();
screen.down();
projector.on();
stereo.on();
dvdPlayer.on();
theaterLight.dim();
}
public void play() {
dvdPlayer.play();
}
public void pause() {
dvdPlayer.pause();
}
public void end() {
popcorn.off();
screen.up();
projector.off();
stereo.off();
dvdPlayer.off();
theaterLight.off();
}
}
外觀模式在MyBatis框架應用的源碼分析
- MyBatis 中的Configuration 去創建MetaObject 對象使用到外觀模式
外觀模式的注意事項和細節
- 外觀模式對外屏蔽了子系統的細節,因此外觀模式降低了客戶端對子系統使用的複雜性。
- 外觀模式對客戶端與子系統的耦合關係 - 解耦,讓子系統內部的模塊更易維護和擴展。
- 通過合理的使用外觀模式,可以幫我們更好的劃分訪問的層次。
- 當系統需要進行分層設計時,可以考慮使用Facade模式。
- 在維護一個遺留的大型系統時,可能這個系統已經變得非常難以維護和擴展,此時可以考慮爲新系統開發一個Facade類,來提供遺留系統的比較清晰簡單的接口,讓新系統與Facade類交互,提高複用性。
- 不能過多的或者不合理的使用外觀模式,使用外觀模式好,還是直接調用模塊好。要以讓系統有層次,利於維護爲目的。
享元模式
展示網站項目需求
- 小型的外包項目,給客戶A做一個產品展示網站,客戶A的朋友感覺效果不錯,也希望做這樣的產品展示網站,但是要求都有些不同:有客戶要求以新聞的形式發佈、有客戶人要求以博客的形式發佈、有客戶希望以微信公衆號的形式發佈。
- 傳統方案解決網站展現項目:直接複製粘貼一份,然後根據客戶不同要求,進行定製修改,給每個網站租用一個空間。
- 需要的網站結構相似度很高,而且都不是高訪問量網站,如果分成多個虛擬空間來處理,相當於一個相同網站的實例對象很多,造成服務器的資源浪費。
- 解決思路:整合到一個網站中,共享其相關的代碼和數據,對於硬盤、內存、CPU、數據庫空間等服務器資源都可以達成共享,減少服務器資源。
- 對於代碼來說,由於是一份實例,維護和擴展都更加容易。==>> 使用 享元模式 來解決
享元模式基本介紹
- 享元模式(Flyweight Pattern) 也叫蠅量模式:運用共享技術有效地支持大量細粒度的對象。
- 常用於系統底層開發,解決系統的性能問題。像數據庫連接池,裏面都是創建好的連接對象,在這些連接對象中有我們需要的則直接拿來用,避免重新創建,如果沒有我們需要的,則創建一個。
- 享元模式能夠解決重複對象的內存浪費的問題,當系統中有大量相似對象,需要緩衝池時。不需總是創建新對象,可以從緩衝池裏拿。這樣可以降低系統內存,同時提高效率。
- 享元模式經典的應用場景就是池技術了,String常量池、數據庫連接池、緩衝池等等都是享元模式的應用,享元模式是池技術的重要實現方式。
享元模式的原理類圖
- FlyWeight 是抽象的享元角色,他是產品的抽象類,同時定義出對象的外部狀態和內部狀態(後面介紹) 的接口或實現。
- ConcreteFlyWeight 是具體的享元角色,是具體的產品類,實現抽象角色定義相關業務。
- UnSharedConcreteFlyWeight 是不可共享的角色,一般不會出現在享元工廠。
- FlyWeightFactory 享元工廠類,用於構建一個池容器(集合), 同時提供從池中獲取對象方法。
內部狀態和外部狀態
比如圍棋、五子棋、跳棋,它們都有大量的棋子對象,圍棋和五子棋只有黑白兩色,跳棋顏色多一點,所以棋子顏色就是棋子的內部狀態;而各個棋子之間的差別就是位置的不同,當我們落子後,落子顏色是定的,但位置是變化的,所以棋子座標就是棋子的外部狀態。
- 享元模式提出了兩個要求:細粒度和共享對象。這裏就涉及到內部狀態和外部狀態了,即將對象的信息分爲兩個部分:內部狀態和外部狀態.
- 內部狀態指對象共享出來的信息,存儲在享元對象內部且不會隨環境的改變而改變。
- 外部狀態指對象得以依賴的一個標記,是隨環境改變而改變的、不可共享的狀態。
- 舉個例子:圍棋理論上有361個空位可以放棋子,每盤棋都有可能有兩三百個棋子對象產生,因爲內存空間有限,一臺服務器很難支持更多的玩家玩圍棋遊戲,如果用享元模式來處理棋子,那麼棋子對象就可以減少到只有兩個實例,這樣就很好的解決了對象的開銷問題。
享元模式解決網站展現項目
public class FlyweightCase {
public static void main(String[] args) {
WebSiteFactory factory = new WebSiteFactory();
WebSite webSite1 = factory.getWebSite("新聞");
webSite1.use(new User("張三"));
WebSite webSite2 = factory.getWebSite("博客");
webSite2.use(new User("李四"));
WebSite webSite3 = factory.getWebSite("博客");
webSite3.use(new User("王五"));
System.out.println("網站的分類共 " + factory.getWebSiteCount() + " 個");
}
}
abstract class WebSite {
abstract void use(User user);
}
class ConcreteWebSite extends WebSite {
/**
* 網站類型:內部狀態
*/
private String type;
ConcreteWebSite(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println("網站的發佈形式:" + type + "," + user.getName() + "在使用中...");
}
}
class WebSiteFactory {
private Map<String, ConcreteWebSite> pool = new HashMap<>();
public WebSite getWebSite(String type) {
if (!pool.containsKey(type)) {
pool.put(type, new ConcreteWebSite(type));
}
return pool.get(type);
}
public int getWebSiteCount() {
return pool.size();
}
}
/**
* 外部狀態
*/
@Data
@AllArgsConstructor
class User {
private String name;
}
//網站的發佈形式:新聞,張三在使用中...
//網站的發佈形式:博客,李四在使用中...
//網站的發佈形式:博客,王五在使用中...
//網站的分類共 2 個
享元模式在JDK-Interger的應用源碼分析
- 在JDK Integer中使用到了享元模式
public class JDKFlyweight {
public static void main(String[] args) {
/*
如果 Integer.valueOf(x) x 在 -127~128 之間,就是使用的享元模式返回
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
*/
Integer x = Integer.valueOf(127);
Integer y = new Integer(127);
Integer z = Integer.valueOf(127);
Integer w = new Integer(127);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
System.out.println(x == z); // true
System.out.println(w == x); // false
System.out.println(w == y); // fasle
Integer x1 = Integer.valueOf(200);
Integer x2 = Integer.valueOf(200);
System.out.println(x1 == x2); // false
}
}
- 在 valueOf 方法中,先判斷值是否在 IntegerCache 中,如果不在,就創建新的 Integer(new), 否則,就直接從 緩存池返回。
- valueOf 方法,就使用到享元模式。
- 如果使用 valueOf 方法得到一個 Integer 實例,範圍在 -128 - 127 ,執行速度比 new 快。
享元模式的注意事項和細節
- 在享元模式這樣理解,“享”就表示共享,“元”表示對象。
- 系統中有大量對象,這些對象消耗大量內存,並且對象的狀態大部分可以外部化時,我們就可以考慮選用享元模式。
- 用唯一標識碼判斷,如果在內存中有,則返回這個唯一標識碼所標識的對象,用HashMap/HashTable存儲。
- 享元模式大大減少了對象的創建,降低了程序內存的佔用,提高效率。
- 享元模式提高了系統的複雜度。需要分離出內部狀態和外部狀態,而外部狀態具有固化特性,不應該隨着內部狀態的改變而改變,這是我們使用享元模式需要注意的地方。
- 使用享元模式時,注意劃分內部狀態和外部狀態,並且需要有一個工廠類加以控制。
- 享元模式經典的應用場景是需要緩衝池的場景,比如 String常量池、數據庫連接池。
代理模式
代理模式的基本介紹
- 代理模式:爲一個對象提供一個替身,以控制對這個對象的訪問。即通過代理對象訪問目標對象。
- 好處:可以在目標對象實現的基礎上,增強額外的功能操作,即擴展目標對象的功能。
- 被代理的對象可以是遠程對象、創建開銷大的對象或需要安全控制的對象。
- 代理模式有不同的形式,主要有三種 靜態代理、動態代理 (JDK代理、接口代理)和 Cglib代理 (可以在內存動態的創建對象,而不需要實現接口, 他是屬於動態代理的範疇) 。
- 代理模式UML圖:
靜態代理
- 靜態代理在使用時,需要定義接口或者父類,被代理對象(即目標對象)與代理對象一起實現相同的接口或者是繼承相同父類。
- 應用實例:
* 定義一個接口:ITeacherDao
* 目標對象TeacherDAO實現接口ITeacherDAO
* 使用靜態代理方式,就需要在代理對象TeacherDAOProxy中也實現ITeacherDAO
* 調用的時候通過調用代理對象的方法來調用目標對象
* 特別提醒:代理對象與目標對象要實現相同的接口,然後通過調用相同的方法來調用目標對象的方法。
- 思路分析:
- 代碼實現:
public class StaticProxyCase {
public static void main(String[] args) {
// 創建被代理對象
TeacherDao teacherDao = new TeacherDao();
// 創建代理對象,同時將被代理對象傳遞給代理對象
TeacherDaoProxy proxy = new TeacherDaoProxy(teacherDao);
// 通過代理對象,調用到被代理對象的方法
proxy.teach();
}
}
interface ITeacherDao {
void teach();
}
class TeacherDao implements ITeacherDao {
@Override
public void teach() {
System.out.println("老師在上課...");
}
}
/**
* 代理對象
*/
class TeacherDaoProxy implements ITeacherDao {
private ITeacherDao target;
TeacherDaoProxy(ITeacherDao target) {
this.target = target;
}
@Override
public void teach() {
System.out.println("開始代理...");
target.teach();
System.out.println("結束代理...");
}
}
- 靜態代理優點:在不修改目標對象的功能前提下,能通過代理對象對目標功能擴展。
- 靜態代理缺點:因爲代理對象需要與目標對象實現一樣的接口,所以會有很多代理類;一旦接口增加方法,目標對象與代理對象都要維護。
動態代理
- 代理對象不需要實現接口了,但是目標對象要實現接口,否則不能用動態代理。
- 代理對象的生成,是利用JDK的API,動態的在內存中構建代理對象。
- 動態代理也叫做:JDK代理、接口代理。
JDK中生成代理對象的API
- 代理類所在包:java.lang.reflect.Proxy
- JDK實現代理只需要使用newProxyInstance方法,但是該方法需要接收三個參數,完整的寫法是
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- 將前面的靜態代理改進成JDK動態代理模式:
- 代碼實現:
public class JDKDynamicProxyCase {
public static void main(String[] args) {
JDKProxyFactory proxy = new JDKProxyFactory(new TeacherDao());
ITeacherDao teacherDao = (ITeacherDao) proxy.getProxyInstance();
System.out.println("teacherDao = " + teacherDao + ", teacherDao.getClass = " + teacherDao.getClass());
// class com.atguigu.dp.L11Proxy.$Proxy0 內存中動態生成了代理對象
teacherDao.teach();
}
}
class JDKProxyFactory {
private Object target;
JDKProxyFactory(Object target) {
this.target = target;
}
/**
* public static Object newProxyInstance(ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h)
* 1、loader:指定當前目標對象使用的類加載器,獲取加載器的方法固定
* 2、interfaces:目標對象實現的接口類型,使用泛型確定類型
* 3、h:事件處理,執行目標對象方法時,會觸發事情處理器方法,會把當前執行的目標對象方法作爲參數傳入
*/
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK動態代理 Before...");
Object result = method.invoke(target, args);
System.out.println("JDK動態代理 After...");
return result;
}
});
}
}
Cglib代理模式
- 靜態代理和JDK代理模式都要求目標對象是實現一個接口,但是有時候目標對象只是一個單獨的對象,並沒有實現任何的接口,這個時候可使用目標對象子類來實現代理-這就是Cglib代理。
- Cglib代理也叫作子類代理,它是在內存中構建一個子類對象從而實現對目標對象功能擴展,有些書也將Cglib代理歸屬到動態代理。
- Cglib是一個強大的高性能的代碼生成包,它可以在運行期擴展java類與實現java接口。它廣泛的被許多AOP的框架使用,例如Spring AOP,實現方法攔截。
- 在AOP編程中如何選擇代理模式:標對象需要實現接口,用JDK代理;目標對象不需要實現接口,用Cglib代理。
- Cglib包的底層是通過使用字節碼處理框架ASM來轉換字節碼並生成新的類。
- UML類圖:
- 代碼實現:
public class CglibDynamicProxyCase {
public static void main(String[] args) {
// 創建目標對象
StudentDao target = new StudentDao();
// 獲取代理對象,並將目標對象傳遞給代理對象
StudentDao studentDao = (StudentDao) new CglibProxyFactory(target).getProxyInstance();
// 執行代理對象的方法
studentDao.study();
}
}
class StudentDao {
public void study() {
System.out.println("學生學習中...");
}
}
class CglibProxyFactory implements MethodInterceptor {
private Object target;
CglibProxyFactory(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLib動態代理 Before...");
Object result = method.invoke(target, args);
System.out.println("CGLib動態代理 After...");
return result;
}
public Object getProxyInstance() {
// 1、創建一個工具類
Enhancer enhancer = new Enhancer();
// 2、設置父類
enhancer.setSuperclass(target.getClass());
// 3、設置回調函數
enhancer.setCallback(this);
// 4、創建子類對象,即代理對象
return enhancer.create();
}
}
- 在內存中動態構建子類,注意代理的類不能爲final,否則報錯 java.lang.IllegalArgumentException。
- 目標對象的方法如果爲final/static,那麼就不會被攔截,即不會執行目標對象額外的業務方法。
代理模式(Proxy)的變體
- 防火牆代理:內網通過代理穿透防火牆,實現對公網的訪問。
- 緩存代理:當請求圖片文件等資源時,先到緩存代理取,如果取到資源則ok,如果取不到資源,再到公網或者數據庫取,然後緩存。
- 遠程代理:遠程對象的本地代表,通過它可以把遠程對象當本地對象來調用。遠程代理通過網絡和真正的遠程對象溝通信息。
- 同步代理:主要使用在多線程編程中,完成多線程間同步工作。