之前有小夥伴私信我說看源碼的時候感覺源碼很難,不知道該怎麼看,其實這有部分原因是因爲沒有弄懂一些源碼實現的套路,也就是設計模式,所以本文我就總結了9種在源碼中非常常見的設計模式,並列舉了很多源碼的實現例子,希望對你看源碼和日常工作中有所幫助。
單例模式
單例模式是指一個類在一個進程中只有一個實例對象(但也不一定,比如Spring中的Bean的單例是指在一個容器中是單例的)
單例模式創建分爲餓漢式和懶漢式,總共大概有8種寫法。但是在開源項目中使用最多的主要有兩種寫法:
1、靜態常量
靜態常量方式屬於餓漢式,以靜態變量的方式聲明對象。這種單例模式在Spring中使用的比較多,舉個例子,在Spring中對於Bean的名稱生成有個類AnnotationBeanNameGenerator就是單例的。
2、雙重檢查機制
除了上面一種,還有一種雙重檢查機制在開源項目中也使用的比較多,而且在面試中也比較喜歡問。雙重檢查機制方式屬於懶漢式,代碼如下:
public class Singleton {
private volatile static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
之所以這種方式叫雙重檢查機制,主要是在創建對象的時候進行了兩次INSTANCE == null的判斷。
疑問講解
這裏解釋一下雙重檢查機制的三個疑問:
- 外層判斷null的作用
- 內層判斷null的作用
- 變量使用volatile關鍵字修飾的作用
外層判斷null的作用:其實就是爲了減少進入同步代碼塊的次數,提高效率。你想一下,其實去了外層的判斷其實是可以的,但是每次獲取對象都需要進入同步代碼塊,實在是沒有必要。
內層判斷null的作用:防止多次創建對象。假設AB同時走到同步代碼塊,A先搶到鎖,進入代碼,創建了對象,釋放鎖,此時B進入代碼塊,如果沒有判斷null,那麼就會直接再次創建對象,那麼就不是單例的了,所以需要進行判斷null,防止重複創建單例對象。
volatile關鍵字的作用:防止重排序。因爲創建對象的過程不是原子,大概會分爲三個步驟
- 第一步:分配內存空間給Singleton這個對象
- 第二步:初始化對象
- 第三步:將INSTANCE變量指向Singleton這個對象內存地址
假設沒有使用volatile關鍵字發生了重排序,第二步和第三步執行過程被調換了,也就是先將INSTANCE變量指向Singleton這個對象內存地址,再初始化對象。這樣在發生併發的情況下,另一個線程經過第一個if非空判斷時,發現已經爲不爲空,就直接返回了這個對象,但是此時這個對象還未初始化,內部的屬性可能都是空值,一旦被使用的話,就很有可能出現空指針這些問題。
雙重檢查機制在dubbo中的應用
在dubbo的spi機制中獲取對象的時候有這樣一段代碼:
雖然這段代碼跟上面的單例的寫法有點不同,但是不難看出其實是使用了雙重檢查機制來創建對象,保證對象單例。
建造者模式
將一個複雜對象的構造與它的表示分離,使同樣的構建過程可以創建不同的表示,這樣的設計模式被稱爲建造者模式。它是將一個複雜的對象分解爲多個簡單的對象,然後一步一步構建而成。
上面的意思看起來很繞,其實在實際開發中,其實建造者模式使用的還是比較多的,比如有時在創建一個pojo對象時,就可以使用建造者模式來創建:
PersonDTO personDTO = PersonDTO.builder()
.name("三友的java日記")
.age(18)
.sex(1)
.phone("188****9527")
.build();`
上面這段代碼就是通過建造者模式構建了一個PersonDTO對象,所以建造者模式又被稱爲Budiler模式。
這種模式在創建對象的時候看起來比較優雅,當構造參數比較多的時候,適合使用建造者模式。
接下來就來看看建造者模式在開源項目中是如何運用的
1、在Spring中的運用
我們都知道,Spring在創建Bean之前,會將每個Bean的聲明封裝成對應的一個BeanDefinition,而BeanDefinition會封裝很多屬性,所以Spring爲了更加優雅地創建BeanDefinition,就提供了BeanDefinitionBuilder這個建造者類。
2、在Guava中的運用
在項目中,如果我們需要使用本地緩存,會使用本地緩存的實現的框架來創建一個,比如在使用Guava來創建本地緩存時,就會這麼寫
Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)
.maximumSize(200)
.build();`
這其實也就是建造者模式。
建造者模式不僅在開源項目中有所使用,在JDK源碼中也有使用到,比如StringBuilder類。
工廠模式
工廠模式在開源項目中也使用的非常多,具體的實現大概可以細分爲三種:
- 簡單工廠模式
- 工廠方法模式
- 抽象工廠模式
簡單工廠模式
簡單工廠模式,就跟名字一樣,的確很簡單。比如說,現在有個動物接口Animal,具體的實現有貓Cat、狗Dog等等,而每個具體的動物對象創建過程很複雜,有各種各樣地步驟,此時就可以使用簡單工廠來封裝對象的創建過程,調用者不需要關心對象是如何具體創建的。
public class SimpleAnimalFactory {
public Animal createAnimal(String animalType) {
if ("cat".equals(animalType)) {
Cat cat = new Cat();
//一系列複雜操作
return cat;
} else if ("dog".equals(animalType)) {
Dog dog = new Dog();
//一系列複雜操作
return dog;
} else {
throw new RuntimeException("animalType=" + animalType + "無法創建對應對象");
}
}
}`
當需要使用這些對象,調用者就可以直接通過簡單工廠創建就行。
SimpleAnimalFactory animalFactory = new SimpleAnimalFactory();
Animal cat = animalFactory.createAnimal("cat");`
需要注意的是,一般來說如果每個動物對象的創建只需要簡單地new一下就行了,那麼其實就無需使用工廠模式,工廠模式適合對象創建過程複雜的場景。
工廠方法模式
上面說的簡單工廠模式看起來沒啥問題,但是還是違反了七大設計原則的OCP原則,也就是開閉原則。所謂的開閉原則就是對修改關閉,對擴展開放。
什麼叫對修改關閉?就是儘可能不修改的意思。就拿上面的例子來說,如果現在新增了一種動物兔子,那麼createAnimal方法就得修改,增加一種類型的判斷,那麼就此時就出現了修改代碼的行爲,也就違反了對修改關閉的原則。
所以解決簡單工廠模式違反開閉原則的問題,就可以使用工廠方法模式來解決。
/**
* 工廠接口
*/
public interface AnimalFactory {
Animal createAnimal();
}
/**
* 小貓實現
*/
public class CatFactory implements AnimalFactory {
@Override
public Animal createAnimal() {
Cat cat = new Cat();
//一系列複雜操作
return cat;
}
}
/**
* 小狗實現
*/
public class DogFactory implements AnimalFactory {
@Override
public Animal createAnimal() {
Dog dog = new Dog();
//一系列複雜操作
return dog;
}
}`
這種方式就是工廠方法模式。他將動物工廠提取成一個接口AnimalFactory,具體每個動物都各自實現這個接口,每種動物都有各自的創建工廠,如果調用者需要創建動物,就可以通過各自的工廠來實現。
AnimalFactory animalFactory = new CatFactory();
Animal cat = animalFactory.createAnimal();`
此時假設需要新增一個動物兔子,那麼只需要實現AnimalFactory接口就行,對於原來的貓和狗的實現,其實代碼是不需要修改的,遵守了對修改關閉的原則,同時由於是對擴展開放,實現接口就是擴展的意思,那麼也就符合擴展開放的原則。
抽象工廠模式
工廠方法模式其實是創建一個產品的工廠,比如上面的例子中,AnimalFactory其實只創建動物這一個產品。而抽象工廠模式特點就是創建一系列產品,比如說,不同的動物喫的東西是不一樣的,那麼就可以加入食物這個產品,通過抽象工廠模式來實現。
public interface AnimalFactory {
Animal createAnimal();
Food createFood();
}`
在動物工廠中,新增了創建食物的接口,小狗小貓的工廠去實現這個接口,創建狗糧和貓糧,這裏就不去寫了。
1、工廠模式在Mybatis的運用
在Mybatis中,當需要調用Mapper接口執行sql的時候,需要先獲取到SqlSession,通過SqlSession再獲取到Mapper接口的動態代理對象,而SqlSession的構造過程比較複雜,所以就提供了SqlSessionFactory工廠類來封裝SqlSession的創建過程。
對於使用者來說,只需要通過SqlSessionFactory來獲取到SqlSession,而無需關心SqlSession是如何創建的。
2、工廠模式在Spring中的運用
我們知道Spring中的Bean是通過BeanFactory創建的。
BeanFactory就是Bean生成的工廠。一個Spring Bean在生成過程中會經歷複雜的一個生命週期,而這些生命週期對於使用者來說是無需關心的,所以就可以將Bean創建過程的邏輯給封裝起來,提取出一個Bean的工廠。
策略模式
策略模式也比較常見,就比如說在Spring源碼中就有很多地方都使用到了策略模式。
在講策略模式是什麼之前先來舉個例子。
假設現在有一個需求,需要將消息推送到不同的平臺。
最簡單的做法其實就是使用if else來做判斷就行了。
public void notifyMessage(User user, String content, int notifyType) {
if (notifyType == 0) {
//調用短信通知的api發送短信
} else if (notifyType == 1) {
//調用app通知的api發送消息
}
}`
根據不同的平臺類型進行判斷,調用對應的api發送消息。
雖然這樣能實現功能,但是跟上面的提到的簡單工廠的問題是一樣的,同樣違反了開閉原則。當需要增加一種平臺類型,比如郵件通知,那麼就得修改notifyMessage的方法,再次進行else if的判斷,然後調用發送郵件的郵件發送消息。
此時就可以使用策略模式來優化了。
首先設計一個策略接口:
public interface MessageNotifier {
/**
* 是否支持改類型的通知的方式
*
* @param notifyType 0:短信 1:app
* @return
*/
boolean support(int notifyType);
/**
* 通知
*
* @param user
* @param content
*/
void notify(User user, String content);
}`
短信通知實現:
@Component
public class SMSMessageNotifier implements MessageNotifier {
@Override
public boolean support(int notifyType) {
return notifyType == 0;
}
@Override
public void notify(User user, String content) {
//調用短信通知的api發送短信
}
}`
app通知實現:
public class AppMessageNotifier implements MessageNotifier {
@Override
public boolean support(int notifyType) {
return notifyType == 1;
}
@Override
public void notify(User user, String content) {
//調用通知app通知的api
}
}`
最後notifyMessage的實現只需要要循環調用所有的MessageNotifier的support方法,一旦support方法返回true,說明當前MessageNotifier支持該類的消息發送,最後再調用notify發送消息就可以了。
@Resource
private List<MessageNotifier> messageNotifiers;
public void notifyMessage(User user, String content, int notifyType) {
for (MessageNotifier messageNotifier : messageNotifiers) {
if (messageNotifier.support(notifyType)) {
messageNotifier.notify(user, content);
}
}
}
那麼如果現在需要支持通過郵件通知,只需要實現MessageNotifier接口,注入到Spring容器就行,其餘的代碼根本不需要有任何變動。
到這其實可以更好的理解策略模式了。就拿上面舉的例子來說,短信通知,app通知等其實都是發送消息一種策略,而策略模式就是需要將這些策略進行封裝,抽取共性,使這些策略之間相互替換。
策略模式在SpringMVC中的運用
1、對接口方法參數的處理
比如說,我們經常在寫接口的時候,會使用到了@PathVariable、@RequestParam、@RequestBody等註解,一旦我們使用了註解,SpringMVC會處理註解,從請求中獲取到參數,然後再調用接口傳遞過來,而這個過程,就使用到了策略模式。
對於這類參數的解析,SpringMVC提供了一個策略接口HandlerMethodArgumentResolver
這個接口的定義就跟我們上面定義的差不多,不同的參數處理只需要實現這個解決就行,比如上面提到的幾個註解,都有對應的實現。
比如處理@RequestParam註解的RequestParamMethodArgumentResolver的實現。
當然還有其它很多的實現,如果想知道各種註解處理的過程,只需要找到對應的實現類就行了。
2、對接口返回值的處理
同樣,SpringMVC對於返回值的處理也是基於策略模式來實現的。
HandlerMethodReturnValueHandler接口定義跟上面都是同一種套路。
比如說,常見的對於@ResponseBody註解處理的實現RequestResponseBodyMethodProcessor。
同樣,HandlerMethodReturnValueHandler的實現也有很多,這裏就不再舉例了。
策略模式在Spring的運用遠不止這兩處,對於配置文件的加載PropertySourceLoader也是策略模式的運用。
模板方法模式
模板方法模式是指,在父類中定義一個操作中的框架,而操作步驟的具體實現交由子類做。其核心思想就是,對於功能實現的順序步驟是一定的,但是具體每一步如何實現交由子類決定。
比如說,對於旅遊來說,一般有以下幾個步驟:
- 做攻略,選擇目的地
- 收拾行李
- 乘坐交通工具去目的地
- 玩耍、拍照
- 乘坐交通工具去返回
但是對於去哪,收拾什麼東西都,乘坐什麼交通工具,都是由具體某個旅行來決定。
那麼對於旅遊這個過程使用模板方法模式翻譯成代碼如下:
public abstract class Travel {
public void travel() {
//做攻略
makePlan();
//收拾行李
packUp();
//去目的地
toDestination();
//玩耍、拍照
play();
//乘坐交通工具去返回
backHome();
}
protected abstract void makePlan();
protected abstract void packUp();
protected abstract void toDestination();
protected abstract void play();
protected abstract void backHome();
}
對於某次旅行來說,只需要重寫每個步驟該做的事就行,比如說這次可以選擇去杭州西湖,下次可以去長城,但是對於旅行過程來說是不變了,對於調用者來說,只需要調用暴露的travel方法就行。
可能這說的還是比較抽象,我再舉兩個模板方法模式在源碼中實現的例子。
模板方法模式在源碼中的使用
1、模板方法模式在HashMap中的使用
HashMap我們都很熟悉,可以通過put方法存元素,並且在元素添加成功之後,會調用一下afterNodeInsertion方法。
而afterNodeInsertion其實是在HashMap中是空實現,什麼事都沒幹。
這其實就是模板方法模式。HashMap定義了一個流程,那就是當元素成功添加之後會調用afterNodeInsertion,子類如果需要在元素添加之後做什麼事,那麼重寫afterNodeInsertion就行。
正巧,JDK中的LinkedHashMap重寫了這個方法。
而這段代碼主要乾的一件事就是可能會移除最老的元素,至於到底會不會移除,得看if是否成立。
添加元素移除最老的元素,基於這種特性其實可以實現LRU算法,比如Mybatis的LruCache就是基於LinkedHashMap實現的,有興趣的可以扒扒源碼,這裏就不再展開講了。
2、模板方法模式在Spring中的運用
我們都知道,在Spring中,ApplicationContext在使用之前需要調用一下refresh方法,而refresh方法就定義了整個容器刷新的執行流程代碼。
在整個刷新過程有一個onRefresh方法
而onRefresh方法默認是沒有做任何事,並且在註釋上有清楚兩個單詞Template method,翻譯過來就是模板方法的意思,所以onRefresh就是一個模板方法,並且方法內部的註釋也表明了,這個方法是爲了子類提供的。
在Web環境下,子類會重寫這個方法,然後創建一個Web服務器。
3、模板方法模式在Mybatis中的使用
在Mybatis中,是使用Executor執行Sql的。
而Mybatis一級緩存就在Executor的抽象實現中BaseExecutor實現的。如圖所示,紅圈就是一級緩存
比如在查詢的時候,如果一級緩存有,那麼就處理緩存的數據,沒有的話就調用queryFromDatabase從數據庫查
queryFromDatabase會調用doQuery方法從數據庫查數據,然後放入一級緩存中。
而doQuery是個抽象方法
所以doQuery其實就是一個模板方法,需要子類真正實現從數據庫中查詢數據,所以這裏就使用了模板方法模式。
責任鏈模式
在責任鏈模式裏,很多對象由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,由該鏈上的某一個對象或者某幾個對象決定處理此請求,每個對象在整個處理過程中值扮演一個小小的角色。
舉個例子,現在有個請假的審批流程,根據請假的人的級別審批到的領導不同,比如有有組長、主管、HR、分管經理等等。
先需要定義一個處理抽象類,抽象類有個下一個處理對象的引用,提供了抽象處理方法,還有一個對下一個處理對象的調用方法。
public abstract class ApprovalHandler {
/**
* 責任鏈中的下一個處理對象
*/
protected ApprovalHandler next;
/**
* 設置下一個處理對象
*
* @param approvalHandler
*/
public void nextHandler(ApprovalHandler approvalHandler) {
this.next = approvalHandler;
}
/**
* 處理
*
* @param approvalContext
*/
public abstract void approval(ApprovalContext approvalContext);
/**
* 調用下一個處理對象
*
* @param approvalContext
*/
protected void invokeNext(ApprovalContext approvalContext) {
if (next != null) {
next.approval(approvalContext);
}
}
}
幾種審批人的實現
//組長審批實現
public class GroupLeaderApprovalHandler extends ApprovalHandler {
@Override
public void approval(ApprovalContext approvalContext) {
System.out.println("組長審批");
//調用下一個處理對象進行處理
invokeNext(approvalContext);
}
}
//主管審批實現
public class DirectorApprovalHandler extends ApprovalHandler {
@Override
public void approval(ApprovalContext approvalContext) {
System.out.println("主管審批");
//調用下一個處理對象進行處理
invokeNext(approvalContext);
}
}
//hr審批實現
public class HrApprovalHandler extends ApprovalHandler {
@Override
public void approval(ApprovalContext approvalContext) {
System.out.println("hr審批");
//調用下一個處理對象進行處理
invokeNext(approvalContext);
}
}
有了這幾個實現之後,接下來就需要對對象進行組裝,組成一個鏈條,比如在Spring中就可以這麼玩。
@Component
public class ApprovalHandlerChain {
@Autowired
private GroupLeaderApprovalHandler groupLeaderApprovalHandler;
@Autowired
private DirectorApprovalHandler directorApprovalHandler;
@Autowired
private HrApprovalHandler hrApprovalHandler;
public ApprovalHandler getChain() {
//組長處理完下一個處理對象是主管
groupLeaderApprovalHandler.nextHandler(directorApprovalHandler);
//主管處理完下一個處理對象是hr
directorApprovalHandler.nextHandler(hrApprovalHandler);
//返回組長,這樣就從組長開始審批,一條鏈就完成了
return groupLeaderApprovalHandler;
}
}
之後對於調用方而言,只需要獲取到鏈條,開始處理就行。
一旦後面出現需要增加或者減少審批人,只需要調整鏈條中的節點就行,對於調用者來說是無感知的。
責任鏈模式在開源項目中的使用
1、在SpringMVC中的使用
在SpringMVC中,可以通過使用HandlerInterceptor對每個請求進行攔截。
而HandlerInterceptor其實就使用到了責任鏈模式,但是這種責任鏈模式的寫法跟上面舉的例子寫法不太一樣。
對於HandlerInterceptor的調用是在HandlerExecutionChain中完成的。
比如說,對於請求處理前的攔截,就在是這樣調用的。
其實就是循環遍歷每個HandlerInterceptor,調用preHandle方法。
2、在Sentinel中的使用
Sentinel是阿里開源的一個流量治理組件,而Sentinel核心邏輯的執行其實就是一條責任鏈。
在Sentinel中,有個核心抽象類AbstractLinkedProcessorSlot
這個組件內部也維護了下一個節點對象,這個類扮演的角色跟例子中的ApprovalHandler類是一樣的,寫法也比較相似。這個組件有很多實現
比如有比較核心的幾個實現
- DegradeSlot:熔斷降級的實現
- FlowSlot:流量控制的實現
- StatisticSlot:統計的實現,比如統計請求成功的次數、異常次數,爲限流提供數據來源
- SystemSlot:根據系統規則來進行流量控制
整個鏈條的組裝的實現是由DefaultSlotChainBuilder實現的
並且內部是使用了SPI機制來加載每個處理節點
所以,如果你想自定一些處理邏輯,就可以基於SPI機制來擴展。
除了上面的例子,比如Gateway網關、Dubbo、MyBatis等等框架中都有責任鏈模式的身影,所以責任鏈模式使用的還是比較多的。
代理模式
代理模式也是開源項目中很常見的使用的一種設計模式,這種模式可以在不改變原有代碼的情況下增加功能。
舉個例子,比如現在有個PersonService接口和它的實現類PersonServiceImpl
//接口
public interface PersonService {
void savePerson(PersonDTO person);
}
//實現
public class PersonServiceImpl implements PersonService{
@Override
public void savePerson(PersonDTO person) {
//保存人員信息
}
}
這個類剛開始運行的好好的,但是突然之前不知道咋回事了,有報錯,需要追尋入參,所以此時就可以這麼寫。
public class PersonServiceImpl implements PersonService {
@Override
public void savePerson(PersonDTO person) {
log.info("savePerson接口入參:{}", JSON.toJSONString(person));
//保存人員信息
}
}
這麼寫,就修改了代碼,萬一以後不需要打印日誌了呢,豈不是又要修改代碼,不符和之前說的開閉原則,那麼怎麼寫呢?可以這麼玩。
public class PersonServiceProxy implements PersonService {
private final PersonService personService = new PersonServiceImpl();
@Override
public void savePerson(PersonDTO person) {
log.info("savePerson接口入參:{}", JSON.toJSONString(person));
personService.savePerson(person);
}
}
可以實現一個代理類PersonServiceProxy,對PersonServiceImpl進行代理,這個代理類乾的事就是打印日誌,最後調用PersonServiceImpl進行人員信息的保存,這就是代理模式。
當需要打印日誌就使用PersonServiceProxy,不需要打印日誌就使用PersonServiceImpl,這樣就行了,不需要改原有代碼的實現。
講到了代理模式,就不得不提一下Spring AOP,Spring AOP其實跟靜態代理很像,最終其實也是調用目標對象的方法,只不過是動態生成的,這裏就不展開講解了。
代理模式在Mybtais中的使用
前面在說模板方法模式的時候,舉了一個BaseExecutor使用到了模板方法模式的例子,並且在BaseExecutor這裏面還完成了一級緩存的操作。
其實不光是一級緩存是通過Executor實現的,二級緩存其實也是,只不過不在BaseExecutor裏面實現,而是在CachingExecutor中實現的。
CachingExecutor中內部有一個Executor類型的屬性delegate,delegate單詞的意思就是代理的意思,所以CachingExecutor顯然就是一個代理類,這裏就使用到了代理模式。
CachingExecutor的實現原理其實很簡單,先從二級緩存查,查不到就通過被代理的對象查找數據,而被代理的Executor在Mybatis中默認使用的是SimpleExecutor實現,SimpleExecutor繼承自BaseExecutor。
這裏思考一下二級緩存爲什麼不像一級緩存一樣直接寫到BaseExecutor中?
這裏我猜測一下是爲了減少耦合。
我們知道Mybatis的一級緩存默認是開啓的,一級緩存寫在BaseExecutor中的話,那麼只要是繼承了BaseExecutor,就擁有了一級緩存的能力。
但二級緩存默認是不開啓的,如果寫在BaseExecutor中,講道理也是可以的,但不符和單一職責的原則,類的功能過多,同時會耦合很多判斷代碼,比如開啓二級緩存走什麼邏輯,不開啓二級緩存走什麼邏輯。而使用代理模式很好的解決了這一問題,只需要在創建的Executor的時候判斷是否開啓二級緩存,開啓的話就用CachingExecutor代理一下,不開啓的話老老實實返回未被代理的對象就行,默認是SimpleExecutor。
如圖所示,是構建Executor對象的源碼,一旦開啓了二級緩存,就會將前面創建的Executor進行代理,構建一個CachingExecutor返回。
適配器模式
適配器模式使得原本由於接口不兼容而不能一起工作的哪些類可以一起工作,將一個類的接口轉換成客戶希望的另一個接口。
舉個生活中的例子,比如手機充電器接口類型有USB TypeC接口和Micro USB接口等。現在需要給一個Micro USB接口的手機充電,但是現在只有USB TypeC接口的充電器,這怎麼辦呢?
其實一般可以弄個一個USB TypeC轉Micro USB接口的轉接頭,這樣就可以給Micro USB接口手機充電了,代碼如下
USBTypeC接口充電
public class USBTypeC {
public void chargeTypeC() {
System.out.println("開啓充電了");
}
}
MicroUSB接口
public interface MicroUSB {
void charge();
}
適配實現,最後是調用USBTypeC接口來充電
public class MicroUSBAdapter implements MicroUSB {
private final USBTypeC usbTypeC = new USBTypeC();
@Override
public void charge() {
//使用usb來充電
usbTypeC.chargeTypeC();
}
}
方然除了上面這種寫法,還有一種繼承的寫法。
public class MicroUSBAdapter extends USBTypeC implements MicroUSB {
@Override
public void charge() {
//使用usb來充電
this.chargeTypeC();
}
}
這兩種寫法主要是繼承和組合(聚合)的區別。
這樣就可以通過適配器(轉接頭)就可以實現USBTypeC給MicroUSB接口充電。
適配器模式在日誌中的使用
在日常開發中,日誌是必不可少的,可以幫助我們快速快速定位問題,但是日誌框架比較多,比如Slf4j、Log4j等等,一般同一系統都使用一種日誌框架。
但是像Mybatis這種框架來說,它本身在運行的過程中也需要產生日誌,但是Mybatis框架在設計的時候,無法知道項目中具體使用的是什麼日誌框架,所以只能適配各種日誌框架,項目中使用什麼框架,Mybatis就使用什麼框架。
爲此Mybatis提供一個Log接口
而不同的日誌框架,只需要適配這個接口就可以了
就拿Slf4j的實現來看,內部依賴了一個Slf4j框架中的Logger對象,最後所有日誌的打印都是通過Slf4j框架中的Logger對象來實現的。
此外,Mybatis還提供瞭如下的一些實現
這樣,Mybatis在需要打印日誌的時候,只需要從Mybatis自己的LogFactory中獲取到Log對象就行,至於最終獲取到的是什麼Log實現,由最終項目中使用日誌框架來決定。
觀察者模式
當對象間存在一對多關係時,則使用觀察者模式(Observer Pattern)。比如,當一個對象被修改時,則會自動通知依賴它的對象。
這是什麼意思呢,舉個例子來說,假設發生了火災,可能需要打119、救人,那麼就可以基於觀察者模式來實現,打119、救人的操作只需要觀察火災的發生,一旦發生,就觸發相應的邏輯。
觀察者的核心優點就是觀察者和被觀察者是解耦合的。就拿上面的例子來說,火災事件(被觀察者)根本不關係有幾個監聽器(觀察者),當以後需要有變動,只需要擴展監聽器就行,對於事件的發佈者和其它監聽器是無需做任何改變的。
觀察者模式實現起來比較複雜,這裏我舉一下Spring事件的例子來說明一下。
觀察者模式在Spring事件中的運用
Spring事件,就是Spring基於觀察者模式實現的一套API。
Spring事件的實現比較簡單,其實就是當Bean在生成完成之後,會將所有的ApplicationListener接口實現(監聽器)添加到ApplicationEventMulticaster中。
ApplicationEventMulticaster可以理解爲一個調度中心的作用,可以將事件通知給監聽器,觸發監聽器的執行。
retrieverCache中存儲了事件類型和對應監聽器的緩存。當發佈事件的時候,會通過事件的類型找到對應的監聽器,然後循環調用監聽器。
所以,Spring的觀察者模式實現的其實也不復雜。
總結
本文通過對設計模式的講解加源碼舉例的方式介紹了9種在代碼設計中常用的設計模式:
- 單例模式
- 建造者模式
- 工廠模式
- 策略模式
- 模板方法模式
- 責任鏈模式
- 代理模式
- 適配器模式
- 觀察者模式
其實這些設計模式不僅在源碼中常見在平時工作中也是可以經常使用到的。
設計模式其實還是一種思想,或者是套路性的東西,至於設計模式具體怎麼用、如何用、代碼如何寫還得依靠具體的場景來進行靈活的判斷。