如何使用 Java8 實(shí)現(xiàn)觀察者模式篓跛?(下)

【編者按】本文作者是 BAE 系統(tǒng)公司的軟件工程師 Justin Albano畸陡。在本篇文章中鸣哀,作者通過(guò)在 Java8 環(huán)境下實(shí)現(xiàn)觀察者模式的實(shí)例架忌,進(jìn)一步介紹了什么是觀察者模式、專業(yè)化及其命名規(guī)則我衬,供大家參考學(xué)習(xí)叹放。本文系國(guó)內(nèi) ITOM 管理平臺(tái) OneAPM 工程師編譯整理。

線程安全的實(shí)現(xiàn)

前面章節(jié)介紹了在現(xiàn)代Java環(huán)境下的實(shí)現(xiàn)觀察者模式挠羔,雖然簡(jiǎn)單但很完整井仰,但這一實(shí)現(xiàn)忽略了一個(gè)關(guān)鍵性問(wèn)題:線程安全。大多數(shù)開(kāi)放的Java應(yīng)用都是多線程的破加,而且觀察者模式也多用于多線程或異步系統(tǒng)糕档。例如,如果外部服務(wù)更新其數(shù)據(jù)庫(kù)拌喉,那么應(yīng)用也會(huì)異步地收到消息速那,然后用觀察者模式通知內(nèi)部組件更新,而不是內(nèi)部組件直接注冊(cè)監(jiān)聽(tīng)外部服務(wù)尿背。

觀察者模式的線程安全主要集中在模式的主體上端仰,因?yàn)樾薷淖?cè)監(jiān)聽(tīng)器集合時(shí)很可能發(fā)生線程沖突,比如田藐,一個(gè)線程試圖添加一個(gè)新的監(jiān)聽(tīng)器荔烧,而另一線程又試圖添加一個(gè)新的animal對(duì)象吱七,這將觸發(fā)對(duì)所有注冊(cè)監(jiān)聽(tīng)器的通知。鑒于先后順序鹤竭,在已注冊(cè)的監(jiān)聽(tīng)器收到新增動(dòng)物的通知前踊餐,第一個(gè)線程可能已經(jīng)完成也可能尚未完成新監(jiān)聽(tīng)器的注冊(cè)。這是一個(gè)經(jīng)典的線程資源競(jìng)爭(zhēng)案例臀稚,正是這一現(xiàn)象告訴開(kāi)發(fā)者們需要一個(gè)機(jī)制來(lái)保證線程安全吝岭。

這一問(wèn)題的最簡(jiǎn)單的解決方案是:所有訪問(wèn)或修改注冊(cè)監(jiān)聽(tīng)器list的操作都須遵循Java的同步機(jī)制,比如:

public synchronized AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }
public synchronized void unregisterAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }
public synchronized void notifyAnimalAddedListeners (Animal animal) { /*...*/ }

這樣一來(lái)吧寺,同一時(shí)刻只有一個(gè)線程可以修改或訪問(wèn)已注冊(cè)的監(jiān)聽(tīng)器列表窜管,可以成功地避免資源競(jìng)爭(zhēng)問(wèn)題,但是新問(wèn)題又出現(xiàn)了稚机,這樣的約束太過(guò)嚴(yán)格(synchronized關(guān)鍵字和Java并發(fā)模型的更多信息幕帆,請(qǐng)參閱官方網(wǎng)頁(yè))。通過(guò)方法同步赖条,可以時(shí)刻觀測(cè)對(duì)監(jiān)聽(tīng)器list的并發(fā)訪問(wèn)失乾,注冊(cè)和撤銷監(jiān)聽(tīng)器對(duì)監(jiān)聽(tīng)器list而言是寫(xiě)操作,而通知監(jiān)聽(tīng)器訪問(wèn)監(jiān)聽(tīng)器list是只讀操作纬乍。由于通過(guò)通知訪問(wèn)是讀操作碱茁,因此是可以多個(gè)通知操作同時(shí)進(jìn)行的。

因此蕾额,只要沒(méi)有監(jiān)聽(tīng)器注冊(cè)或撤銷注冊(cè)早芭,任意多的并發(fā)通知都可以同時(shí)執(zhí)行,而不會(huì)引發(fā)對(duì)注冊(cè)的監(jiān)聽(tīng)器列表的資源爭(zhēng)奪诅蝶。當(dāng)然退个,其他情況下的資源爭(zhēng)奪現(xiàn)象存在已久,為了解決這一問(wèn)題调炬,設(shè)計(jì)了ReadWriteLock用以分開(kāi)管理讀寫(xiě)操作的資源鎖定语盈。Zoo類的線程安全ThreadSafeZoo實(shí)現(xiàn)代碼如下:

public class ThreadSafeZoo {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    protected final Lock readLock = readWriteLock.readLock();
    protected final Lock writeLock = readWriteLock.writeLock();
    private List<Animal> animals = new ArrayList<>();
    private List<AnimalAddedListener> listeners = new ArrayList<>();
    public void addAnimal (Animal animal) {
        // Add the animal to the list of animals
        this.animals.add(animal);
        // Notify the list of registered listeners
        this.notifyAnimalAddedListeners(animal);
    }
    public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {
        // Lock the list of listeners for writing
        this.writeLock.lock();
        try {
            // Add the listener to the list of registered listeners
            this.listeners.add(listener);
        }
        finally {
            // Unlock the writer lock
            this.writeLock.unlock();
        }
        return listener;
    }
    public void unregisterAnimalAddedListener (AnimalAddedListener listener) {
        // Lock the list of listeners for writing
        this.writeLock.lock();
        try {
            // Remove the listener from the list of the registered listeners
            this.listeners.remove(listener);
        }
        finally {
            // Unlock the writer lock
            this.writeLock.unlock();
        }
    }
    public void notifyAnimalAddedListeners (Animal animal) {
        // Lock the list of listeners for reading
        this.readLock.lock();
        try {
            // Notify each of the listeners in the list of registered listeners
            this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
        }
        finally {
            // Unlock the reader lock
            this.readLock.unlock();
        }
    }
}

通過(guò)這樣部署,Subject的實(shí)現(xiàn)能確保線程安全并且多個(gè)線程可以同時(shí)發(fā)布通知缰泡。但盡管如此刀荒,依舊存在兩個(gè)不容忽略的資源競(jìng)爭(zhēng)問(wèn)題:

  1. 對(duì)每個(gè)監(jiān)聽(tīng)器的并發(fā)訪問(wèn)。多個(gè)線程可以同時(shí)通知監(jiān)聽(tīng)器要新增動(dòng)物了棘钞,這意味著一個(gè)監(jiān)聽(tīng)器可能會(huì)同時(shí)被多個(gè)線程同時(shí)調(diào)用缠借。

  2. 對(duì)animal list的并發(fā)訪問(wèn)。多個(gè)線程可能會(huì)同時(shí)向animal list添加對(duì)象宜猜,如果通知的先后順序存在影響泼返,那就可能導(dǎo)致資源競(jìng)爭(zhēng),這就需要一個(gè)并發(fā)操作處理機(jī)制來(lái)避免這一問(wèn)題姨拥。如果注冊(cè)的監(jiān)聽(tīng)器列表在收到通知添加animal2后绅喉,又收到通知添加animal1渠鸽,此時(shí)就會(huì)產(chǎn)生資源競(jìng)爭(zhēng)。但是如果animal1和animal2的添加由不同的線程執(zhí)行柴罐,也是有可能在animal2前完成對(duì)animal1添加操作徽缚,具體來(lái)說(shuō)就是線程1在通知監(jiān)聽(tīng)器前添加animal1并鎖定模塊,線程2添加animal2并通知監(jiān)聽(tīng)器革屠,然后線程1通知監(jiān)聽(tīng)器animal1已經(jīng)添加凿试。雖然在不考慮先后順序時(shí),可以忽略資源競(jìng)爭(zhēng)屠阻,但問(wèn)題是真實(shí)存在的红省。

對(duì)監(jiān)聽(tīng)器的并發(fā)訪問(wèn)

并發(fā)訪問(wèn)監(jiān)聽(tīng)器可以通過(guò)保證監(jiān)聽(tīng)器的線程安全來(lái)實(shí)現(xiàn)额各。秉承著類的“責(zé)任自負(fù)”精神国觉,監(jiān)聽(tīng)器有“義務(wù)”確保自身的線程安全。例如虾啦,對(duì)于前面計(jì)數(shù)的監(jiān)聽(tīng)器麻诀,多線程的遞增或遞減動(dòng)物數(shù)量可能導(dǎo)致線程安全問(wèn)題,要避免這一問(wèn)題傲醉,動(dòng)物數(shù)的計(jì)算必須是原子操作(原子變量或方法同步)蝇闭,具體解決代碼如下:

public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener {
    private static AtomicLong animalsAddedCount = new AtomicLong(0);
    @Override
    public void updateAnimalAdded (Animal animal) {
        // Increment the number of animals
        animalsAddedCount.incrementAndGet();
        // Print the number of animals
        System.out.println("Total animals added: " + animalsAddedCount);
    }
}

方法同步解決方案代碼如下:

public class CountingAnimalAddedListener implements AnimalAddedListener {
    private static int animalsAddedCount = 0;
    @Override
    public synchronized void updateAnimalAdded (Animal animal) {
        // Increment the number of animals
        animalsAddedCount++;
        // Print the number of animals
        System.out.println("Total animals added: " + animalsAddedCount);
    }
}

要強(qiáng)調(diào)的是監(jiān)聽(tīng)器應(yīng)該保證自身的線程安全,subject需要理解監(jiān)聽(tīng)器的內(nèi)部邏輯硬毕,而不是簡(jiǎn)單確保對(duì)監(jiān)聽(tīng)器的訪問(wèn)和修改的線程安全呻引。否則,如果多個(gè)subject共用同一個(gè)監(jiān)聽(tīng)器吐咳,那每個(gè)subject類都要重寫(xiě)一遍線程安全的代碼逻悠,顯然這樣的代碼不夠簡(jiǎn)潔,因此需要在監(jiān)聽(tīng)器類內(nèi)實(shí)現(xiàn)線程安全韭脊。

監(jiān)聽(tīng)器的有序通知

當(dāng)要求監(jiān)聽(tīng)器有序執(zhí)行時(shí)童谒,讀寫(xiě)鎖就不能滿足需求了,而需要引入一個(gè)新的機(jī)制沪羔,可以保證notify函數(shù)的調(diào)用順序和animal添加到zoo的順序一致饥伊。有人嘗試過(guò)用方法同步來(lái)實(shí)現(xiàn),然而根據(jù)Oracle文檔中的方法同步介紹蔫饰,可知方法同步并不提供操作執(zhí)行的順序管理琅豆。它只是保證原子操作,也就是說(shuō)操作不會(huì)被打斷篓吁,并不能保證先來(lái)先執(zhí)行(FIFO)的線程順序茫因。ReentrantReadWriteLock可以實(shí)現(xiàn)這樣的執(zhí)行順序,代碼如下:

public class OrderedThreadSafeZoo {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    protected final Lock readLock = readWriteLock.readLock();
    protected final Lock writeLock = readWriteLock.writeLock();
    private List<Animal> animals = new ArrayList<>();
    private List<AnimalAddedListener> listeners = new ArrayList<>();
    public void addAnimal (Animal animal) {
        // Add the animal to the list of animals
        this.animals.add(animal);
        // Notify the list of registered listeners
        this.notifyAnimalAddedListeners(animal);
    }
    public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {
        // Lock the list of listeners for writing
        this.writeLock.lock();
        try {
            // Add the listener to the list of registered listeners
            this.listeners.add(listener);
        }
        finally {
            // Unlock the writer lock
            this.writeLock.unlock();
        }
        return listener;
    }
    public void unregisterAnimalAddedListener (AnimalAddedListener listener) {
        // Lock the list of listeners for writing
        this.writeLock.lock();
        try {
            // Remove the listener from the list of the registered listeners
            this.listeners.remove(listener);
        }
        finally {
            // Unlock the writer lock
            this.writeLock.unlock();
        }
    }
    public void notifyAnimalAddedListeners (Animal animal) {
        // Lock the list of listeners for reading
        this.readLock.lock();
        try {
            // Notify each of the listeners in the list of registered listeners
            this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
        }
        finally {
            // Unlock the reader lock
            this.readLock.unlock();
        }
    }
}

這樣的實(shí)現(xiàn)方式越除,register, unregister和notify函數(shù)將按照先進(jìn)先出(FIFO)的順序獲得讀寫(xiě)鎖權(quán)限节腐。例如外盯,線程1注冊(cè)一個(gè)監(jiān)聽(tīng)器,線程2在開(kāi)始執(zhí)行注冊(cè)操作后試圖通知已注冊(cè)的監(jiān)聽(tīng)器翼雀,線程3在線程2等待只讀鎖的時(shí)候也試圖通知已注冊(cè)的監(jiān)聽(tīng)器饱苟,采用fair-ordering方式,線程1先完成注冊(cè)操作狼渊,然后線程2可以通知監(jiān)聽(tīng)器箱熬,最后線程3通知監(jiān)聽(tīng)器。這樣保證了action的執(zhí)行順序和開(kāi)始順序一致狈邑。

如果采用方法同步城须,雖然線程2先排隊(duì)等待占用資源,線程3仍可能比線程2先獲得資源鎖米苹,而且不能保證線程2比線程3先通知監(jiān)聽(tīng)器糕伐。問(wèn)題的關(guān)鍵所在:fair-ordering方式可以保證線程按照申請(qǐng)資源的順序執(zhí)行。讀寫(xiě)鎖的順序機(jī)制很復(fù)雜蘸嘶,應(yīng)參照ReentrantReadWriteLock的官方文檔以確保鎖的邏輯足夠解決問(wèn)題良瞧。

截止目前實(shí)現(xiàn)了線程安全似炎,在接下來(lái)的章節(jié)中將介紹提取主題的邏輯并將其mixin類封裝為可重復(fù)代碼單元的方式優(yōu)缺點(diǎn)徽诲。

主題邏輯封裝到Mixin類

把上述的觀察者模式設(shè)計(jì)實(shí)現(xiàn)封裝到目標(biāo)的mixin類中很具吸引力肄梨。通常來(lái)說(shuō)远搪,觀察者模式中的觀察者包含已注冊(cè)的監(jiān)聽(tīng)器的集合谆甜;負(fù)責(zé)注冊(cè)新的監(jiān)聽(tīng)器的register函數(shù)短绸;負(fù)責(zé)撤銷注冊(cè)的unregister函數(shù)和負(fù)責(zé)通知監(jiān)聽(tīng)器的notify函數(shù)胖缤。對(duì)于上述的動(dòng)物園的例子眷蚓,zoo類除動(dòng)物列表是問(wèn)題所需外澳骤,其他所有操作都是為了實(shí)現(xiàn)主題的邏輯歧强。

Mixin類的案例如下所示,需要說(shuō)明的是為使代碼更為簡(jiǎn)潔宴凉,此處去掉關(guān)于線程安全的代碼:

public abstract class ObservableSubjectMixin<ListenerType> {
    private List<ListenerType> listeners = new ArrayList<>();
    public ListenerType registerListener (ListenerType listener) {
        // Add the listener to the list of registered listeners
        this.listeners.add(listener);
        return listener;
    }
    public void unregisterAnimalAddedListener (ListenerType listener) {
        // Remove the listener from the list of the registered listeners
        this.listeners.remove(listener);
    }
    public void notifyListeners (Consumer<? super ListenerType> algorithm) {
        // Execute some function on each of the listeners
        this.listeners.forEach(algorithm);
    }
}

正因?yàn)闆](méi)有提供正在注冊(cè)的監(jiān)聽(tīng)器類型的接口信息誊锭,不能直接通知某個(gè)特定的監(jiān)聽(tīng)器,所以正需要保證通知功能的通用性弥锄,允許客戶端添加一些功能丧靡,如接受泛型參數(shù)類型的參數(shù)匹配,以適用于每個(gè)監(jiān)聽(tīng)器籽暇,具體實(shí)現(xiàn)代碼如下:

public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> {
    private List<Animal> animals = new ArrayList<>();
    public void addAnimal (Animal animal) {
        // Add the animal to the list of animals
        this.animals.add(animal);
        // Notify the list of registered listeners
        this.notifyListeners((listener) -> listener.updateAnimalAdded(animal));
    }
}

Mixin類技術(shù)的最大優(yōu)勢(shì)是把觀察者模式的Subject封裝到一個(gè)可重復(fù)調(diào)用的類中温治,而不是在每個(gè)subject類中都重復(fù)寫(xiě)這些邏輯。此外戒悠,這一方法使得zoo類的實(shí)現(xiàn)更為簡(jiǎn)潔熬荆,只需要存儲(chǔ)動(dòng)物信息,而不用再考慮如何存儲(chǔ)和通知監(jiān)聽(tīng)器绸狐。

然而卤恳,使用mixin類并非只有優(yōu)點(diǎn)累盗。比如,如果要存儲(chǔ)多個(gè)類型的監(jiān)聽(tīng)器怎么辦突琳?例如若债,還需要存儲(chǔ)監(jiān)聽(tīng)器類型AnimalRemovedListener。mixin類是抽象類拆融,Java中不能同時(shí)繼承多個(gè)抽象類蠢琳,而且mixin類不能改用接口實(shí)現(xiàn),這是因?yàn)榻涌诓话瑂tate镜豹,而觀察者模式中state需要用來(lái)保存已經(jīng)注冊(cè)的監(jiān)聽(tīng)器列表傲须。

其中的一個(gè)解決方案是創(chuàng)建一個(gè)動(dòng)物增加和減少時(shí)都會(huì)通知的監(jiān)聽(tīng)器類型ZooListener,代碼如下所示:

public interface ZooListener {
    public void onAnimalAdded (Animal animal);
    public void onAnimalRemoved (Animal animal);
}

這樣就可以使用該接口實(shí)現(xiàn)利用一個(gè)監(jiān)聽(tīng)器類型對(duì)zoo狀態(tài)各種變化的監(jiān)聽(tīng)了:

public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> {
    private List<Animal> animals = new ArrayList<>();
    public void addAnimal (Animal animal) {
        // Add the animal to the list of animals
        this.animals.add(animal);
        // Notify the list of registered listeners
        this.notifyListeners((listener) -> listener.onAnimalAdded(animal));
    }
    public void removeAnimal (Animal animal) {
        // Remove the animal from the list of animals
        this.animals.remove(animal);
        // Notify the list of registered listeners
        this.notifyListeners((listener) -> listener.onAnimalRemoved(animal));
    }
}

將多個(gè)監(jiān)聽(tīng)器類型合并到一個(gè)監(jiān)聽(tīng)器接口中確實(shí)解決了上面提到的問(wèn)題趟脂,但仍舊存在不足之處泰讽,接下來(lái)的章節(jié)會(huì)詳細(xì)討論。

Multi-Method監(jiān)聽(tīng)器和適配器

在上述方法散怖,監(jiān)聽(tīng)器的接口中實(shí)現(xiàn)的包含太多函數(shù)菇绵,接口就過(guò)于冗長(zhǎng)肄渗,例如镇眷,Swing MouseListener就包含5個(gè)必要的函數(shù)。盡管可能只會(huì)用到其中一個(gè)翎嫡,但是只要用到鼠標(biāo)點(diǎn)擊事件就必須要添加這5個(gè)函數(shù)欠动,更多可能是用空函數(shù)體來(lái)實(shí)現(xiàn)剩下的函數(shù),這無(wú)疑會(huì)給代碼帶來(lái)不必要的混亂惑申。

其中一種解決方案是創(chuàng)建適配器(概念來(lái)自GoF提出的適配器模式)具伍,適配器中以抽象函數(shù)的形式實(shí)現(xiàn)監(jiān)聽(tīng)器接口的操作,供具體監(jiān)聽(tīng)器類繼承圈驼。這樣一來(lái)人芽,具體監(jiān)聽(tīng)器類就可以選擇其需要的函數(shù),對(duì)adapter不需要的函數(shù)采用默認(rèn)操作即可绩脆。例如上面例子中的ZooListener類萤厅,創(chuàng)建ZooAdapter(Adapter的命名規(guī)則與監(jiān)聽(tīng)器一致,只需要把類名中的Listener改為Adapter即可)靴迫,代碼如下:

public class ZooAdapter implements ZooListener {
    @Override
    public void onAnimalAdded (Animal animal) {}
    @Override
    public void onAnimalRemoved (Animal animal) {}
}

乍一看惕味,這個(gè)適配器類微不足道,然而它所帶來(lái)的便利卻是不可小覷的玉锌。比如對(duì)于下面的具體類名挥,只需選擇對(duì)其實(shí)現(xiàn)有用的函數(shù)即可:

public class NamePrinterZooAdapter extends ZooAdapter {
    @Override
    public void onAnimalAdded (Animal animal) {
        // Print the name of the animal that was added
        System.out.println("Added animal named " + animal.getName());
    }
}

有兩種替代方案同樣可以實(shí)現(xiàn)適配器類的功能:一是使用默認(rèn)函數(shù);二是把監(jiān)聽(tīng)器接口和適配器類合并到一個(gè)具體類中主守。默認(rèn)函數(shù)是Java8新提出的禀倔,在接口中允許開(kāi)發(fā)者提供默認(rèn)(防御)的實(shí)現(xiàn)方法榄融。

Java庫(kù)的這一更新主要是方便開(kāi)發(fā)者在不改變老版本代碼的情況下,實(shí)現(xiàn)程序擴(kuò)展救湖,因此應(yīng)該慎用這個(gè)方法剃袍。部分開(kāi)發(fā)者多次使用后民效,會(huì)感覺(jué)這樣寫(xiě)的代碼不夠?qū)I(yè)畏邢,而又有開(kāi)發(fā)者認(rèn)為這是Java8的特色舒萎,不管怎樣摊灭,需要明白這個(gè)技術(shù)提出的初衷是什么,再結(jié)合具體問(wèn)題決定是否要用。使用默認(rèn)函數(shù)實(shí)現(xiàn)的ZooListener接口代碼如下示:

public interface ZooListener {
    default public void onAnimalAdded (Animal animal) {}
    default public void onAnimalRemoved (Animal animal) {}
}

通過(guò)使用默認(rèn)函數(shù),實(shí)現(xiàn)該接口的具體類,無(wú)需在接口中實(shí)現(xiàn)全部函數(shù),而是選擇性實(shí)現(xiàn)所需函數(shù)。雖然這是接口膨脹問(wèn)題一個(gè)較為簡(jiǎn)潔的解決方案酝润,開(kāi)發(fā)者在使用時(shí)還應(yīng)多加注意疏咐。

第二種方案是簡(jiǎn)化觀察者模式政己,省略了監(jiān)聽(tīng)器接口沦泌,而是用具體類實(shí)現(xiàn)監(jiān)聽(tīng)器的功能他宛。比如ZooListener接口就變成了下面這樣:

public class ZooListener {
    public void onAnimalAdded (Animal animal) {}
    public void onAnimalRemoved (Animal animal) {}
}

這一方案簡(jiǎn)化了觀察者模式的層次結(jié)構(gòu)队塘,但它并非適用于所有情況,因?yàn)槿绻驯O(jiān)聽(tīng)器接口合并到具體類中陌凳,具體監(jiān)聽(tīng)器就不可以實(shí)現(xiàn)多個(gè)監(jiān)聽(tīng)接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口寫(xiě)在同一個(gè)具體類中赔硫,那么單獨(dú)一個(gè)具體監(jiān)聽(tīng)器就不可以同時(shí)實(shí)現(xiàn)這兩個(gè)接口了炒俱。此外权悟,監(jiān)聽(tīng)器接口的意圖比具體類更顯而易見(jiàn)榔昔,很顯然前者就是為其他類提供接口怔檩,但后者就并非那么明顯了蓄诽。

如果沒(méi)有合適的文檔說(shuō)明薛训,開(kāi)發(fā)者并不會(huì)知道已經(jīng)有一個(gè)類扮演著接口的角色闸英,實(shí)現(xiàn)了其對(duì)應(yīng)的所有函數(shù)。此外膊爪,類名不包含adapter自阱,因?yàn)轭惒⒉贿m配于某一個(gè)接口,因此類名并沒(méi)有特別暗示此意圖米酬。綜上所述沛豌,特定問(wèn)題需要選擇特定的方法,并沒(méi)有哪個(gè)方法是萬(wàn)能的赃额。

在開(kāi)始下一章前加派,需要特別提一下,適配器在觀察模式中很常見(jiàn)跳芳,尤其是在老版本的Java代碼中芍锦。Swing API正是以適配器為基礎(chǔ)實(shí)現(xiàn)的,正如很多老應(yīng)用在Java5和Java6中的觀察者模式中所使用的那樣飞盆。zoo案例中的監(jiān)聽(tīng)器或許并不需要適配器娄琉,但需要了解適配器提出的目的以及其應(yīng)用,因?yàn)槲覀兛梢栽诂F(xiàn)有的代碼中對(duì)其進(jìn)行使用吓歇。下面的章節(jié)孽水,將會(huì)介紹時(shí)間復(fù)雜的監(jiān)聽(tīng)器,該類監(jiān)聽(tīng)器可能會(huì)執(zhí)行耗時(shí)的運(yùn)算或進(jìn)行異步調(diào)用城看,不能立即給出返回值女气。

Complex & Blocking監(jiān)聽(tīng)器

關(guān)于觀察者模式的一個(gè)假設(shè)是:執(zhí)行一個(gè)函數(shù)時(shí),一系列監(jiān)聽(tīng)器會(huì)被調(diào)用测柠,但假定這一過(guò)程對(duì)調(diào)用者而言是完全透明的炼鞠。例如,客戶端代碼在Zoo中添加animal時(shí)轰胁,在返回添加成功之前谒主,并不知道會(huì)調(diào)用一系列監(jiān)聽(tīng)器。如果監(jiān)聽(tīng)器的執(zhí)行需要時(shí)間較長(zhǎng)(其時(shí)間受監(jiān)聽(tīng)器的數(shù)量软吐、每個(gè)監(jiān)聽(tīng)器執(zhí)行時(shí)間影響)瘩将,那么客戶端代碼將會(huì)感知這一簡(jiǎn)單增加動(dòng)物操作的時(shí)間副作用。

本文不能面面俱到的討論這個(gè)話題凹耙,下面幾條是開(kāi)發(fā)者調(diào)用復(fù)雜的監(jiān)聽(tīng)器時(shí)應(yīng)該注意的事項(xiàng):

  1. 監(jiān)聽(tīng)器啟動(dòng)新線程。新線程啟動(dòng)后肠仪,在新線程中執(zhí)行監(jiān)聽(tīng)器邏輯的同時(shí)肖抱,返回監(jiān)聽(tīng)器函數(shù)的處理結(jié)果,并運(yùn)行其他監(jiān)聽(tīng)器執(zhí)行异旧。

  2. Subject啟動(dòng)新線程意述。與傳統(tǒng)的線性迭代已注冊(cè)的監(jiān)聽(tīng)器列表不同,Subject的notify函數(shù)重啟一個(gè)新的線程,然后在新線程中迭代監(jiān)聽(tīng)器列表荤崇。這樣使得notify函數(shù)在執(zhí)行其他監(jiān)聽(tīng)器操作的同時(shí)可以輸出其返回值拌屏。需要注意的是需要一個(gè)線程安全機(jī)制來(lái)確保監(jiān)聽(tīng)器列表不會(huì)進(jìn)行并發(fā)修改。

  3. 隊(duì)列化監(jiān)聽(tīng)器調(diào)用并采用一組線程執(zhí)行監(jiān)聽(tīng)功能术荤。將監(jiān)聽(tīng)器操作封裝在一些函數(shù)中并隊(duì)列化這些函數(shù)倚喂,而非簡(jiǎn)單的迭代調(diào)用監(jiān)聽(tīng)器列表。這些監(jiān)聽(tīng)器存儲(chǔ)到隊(duì)列中后瓣戚,線程就可以從隊(duì)列中彈出單個(gè)元素并執(zhí)行其監(jiān)聽(tīng)邏輯端圈。這類似于生產(chǎn)者-消費(fèi)者問(wèn)題,notify過(guò)程產(chǎn)生可執(zhí)行函數(shù)隊(duì)列子库,然后線程依次從隊(duì)列中取出并執(zhí)行這些函數(shù)舱权,函數(shù)需要存儲(chǔ)被創(chuàng)建的時(shí)間而非執(zhí)行的時(shí)間供監(jiān)聽(tīng)器函數(shù)調(diào)用。例如仑嗅,監(jiān)聽(tīng)器被調(diào)用時(shí)創(chuàng)建的函數(shù)宴倍,那么該函數(shù)就需要存儲(chǔ)該時(shí)間點(diǎn),這一功能類似于Java中的如下操作:

public class AnimalAddedFunctor {
    private final AnimalAddedListener listener;
    private final Animal parameter;
    public AnimalAddedFunctor (AnimalAddedListener listener, Animal parameter) {
        this.listener = listener;
        this.parameter = parameter;
    }
    public void execute () {
        // Execute the listener with the parameter provided during creation
        this.listener.updateAnimalAdded(this.parameter);
    }
}

函數(shù)創(chuàng)建并保存在隊(duì)列中仓技,可以隨時(shí)調(diào)用鸵贬,這樣一來(lái)就無(wú)需在遍歷監(jiān)聽(tīng)器列表時(shí)立即執(zhí)行其對(duì)應(yīng)操作了。一旦每個(gè)激活監(jiān)聽(tīng)器的函數(shù)都?jí)喝腙?duì)列中浑彰,“消費(fèi)者線程”就會(huì)給客戶端代碼返回操作權(quán)恭理。之后某個(gè)時(shí)間點(diǎn)“消費(fèi)者線程”將會(huì)執(zhí)行這些函數(shù),就像在監(jiān)聽(tīng)器被notify函數(shù)激活時(shí)執(zhí)行一樣郭变。這項(xiàng)技術(shù)在其他語(yǔ)言中被叫作參數(shù)綁定颜价,剛好適合上面的例子,技術(shù)的實(shí)質(zhì)是保存監(jiān)聽(tīng)器的參數(shù)诉濒,execute()函數(shù)再直接調(diào)用周伦。如果監(jiān)聽(tīng)器接收多個(gè)參數(shù),處理方法也類似未荒。

需要注意的是如果要保存監(jiān)聽(tīng)器的執(zhí)行順序专挪,則需要引入綜合排序機(jī)制。方案一中片排,監(jiān)聽(tīng)器按照正常的順序激活新線程寨腔,這樣可以確保監(jiān)聽(tīng)器按照注冊(cè)的順序執(zhí)行。方案二中率寡,隊(duì)列支持排序迫卢,其中的函數(shù)會(huì)按照進(jìn)入隊(duì)列的順序執(zhí)行。簡(jiǎn)單來(lái)說(shuō)就是冶共,開(kāi)發(fā)者需要重視監(jiān)聽(tīng)器多線程執(zhí)行的復(fù)雜程度乾蛤,加以小心處理以確保實(shí)現(xiàn)所需的功能每界。

結(jié)束語(yǔ)

觀察者模式在1994年被寫(xiě)進(jìn)書(shū)中以前,就已經(jīng)是主流的軟件設(shè)計(jì)模式了家卖,為軟件設(shè)計(jì)中經(jīng)常出現(xiàn)的問(wèn)題提供了很多令人滿意的解決方案眨层。Java一直是使用該模式的引領(lǐng)者,在其標(biāo)準(zhǔn)庫(kù)中封裝了這一模式上荡,但是鑒于Java更新到了版本8趴樱,十分有必要重新考查經(jīng)典模式在其中的使用。隨著lambda表達(dá)式和其他新結(jié)構(gòu)的出現(xiàn)榛臼,這一“古老的”模式又有了新的生機(jī)伊佃。無(wú)論是處理舊程序還是使用這一歷史悠久的方法解決新問(wèn)題,尤其對(duì)經(jīng)驗(yàn)豐富的Java開(kāi)發(fā)者來(lái)說(shuō)沛善,觀察者模式都是開(kāi)發(fā)者的主要工具航揉。

(編譯自:https://dzone.com/articles/the-observer-pattern-using-modern-java)

OneAPM 為您提供端到端的 Java 應(yīng)用性能解決方案,我們支持所有常見(jiàn)的 Java 框架及應(yīng)用服務(wù)器金刁,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸帅涂,定位異常根本原因。分鐘級(jí)部署尤蛮,即刻體驗(yàn)媳友,Java 監(jiān)控從來(lái)沒(méi)有如此簡(jiǎn)單。想閱讀更多技術(shù)文章产捞,請(qǐng)?jiān)L問(wèn) OneAPM 官方技術(shù)博客醇锚。
本文轉(zhuǎn)自 OneAPM 官方博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市坯临,隨后出現(xiàn)的幾起案子焊唬,更是在濱河造成了極大的恐慌,老刑警劉巖看靠,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赶促,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡挟炬,警方通過(guò)查閱死者的電腦和手機(jī)鸥滨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谤祖,“玉大人婿滓,你說(shuō)我怎么就攤上這事≈嘞玻” “怎么了空幻?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)容客。 經(jīng)常有香客問(wèn)我秕铛,道長(zhǎng),這世上最難降的妖魔是什么缩挑? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任但两,我火速辦了婚禮,結(jié)果婚禮上供置,老公的妹妹穿的比我還像新娘谨湘。我一直安慰自己,他們只是感情好芥丧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布紧阔。 她就那樣靜靜地躺著,像睡著了一般续担。 火紅的嫁衣襯著肌膚如雪擅耽。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,231評(píng)論 1 299
  • 那天物遇,我揣著相機(jī)與錄音乖仇,去河邊找鬼。 笑死询兴,一個(gè)胖子當(dāng)著我的面吹牛乃沙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诗舰,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼警儒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了眶根?” 一聲冷哼從身側(cè)響起蜀铲,我...
    開(kāi)封第一講書(shū)人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎汛闸,沒(méi)想到半個(gè)月后蝙茶,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诸老,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年隆夯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片别伏。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蹄衷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厘肮,到底是詐尸還是另有隱情愧口,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布类茂,位于F島的核電站耍属,受9級(jí)特大地震影響托嚣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜厚骗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一示启、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧领舰,春花似錦夫嗓、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至锉桑,卻和暖如春排霉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刨仑。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工郑诺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杉武。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓辙诞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親轻抱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子飞涂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 【編者按】本文作者是 BAE 系統(tǒng)公司的軟件工程師 Justin Albano较店。在本篇文章中,作者通過(guò)在 Java...
    OneAPM閱讀 660評(píng)論 0 4
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理容燕,服務(wù)發(fā)現(xiàn)梁呈,斷路器,智...
    卡卡羅2017閱讀 134,652評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,085評(píng)論 25 707
  • 《思帝鄉(xiāng)》 春日游蘸秘,杏花吹滿頭官卡。 陌上誰(shuí)家年少,足風(fēng)流醋虏。 妾擬將身嫁與寻咒,一生休。 縱被無(wú)情棄颈嚼,不能羞毛秘。 -----...
    我煮魚(yú)閱讀 282評(píng)論 0 1
  • 文|金玲 討好叫挟,是人際關(guān)系中非常常見(jiàn)的模式艰匙。我們都知道不需要討好全世界,讓自己開(kāi)心就好霞揉,然而實(shí)際上卻總是難以做到旬薯。...
    金玲老師的生涯空間站閱讀 341評(píng)論 0 0