淺談 EDA | 小蟲茶肆

171 阅读7分钟
原文链接: jingkecn.github.io

故事要追溯到兩年前,那時我還在 WizTiVi 完成我的畢業實習,我的企業導師 Alexandre 給我的課題是:實現一個基於 JavaScript 快速開發可穿戴設備應用程序的 SDK 框架。

導師讓我從 iOS + watchOS 開始著手研究。當時我心裡就想,那簡單,iOS 有 JavaScriptCore,經驗告訴我,我可以利用 iOS 和 watchOS 之間的通信協議進行 RPC,也就是「遠程過程調用(Remote Procedure Call)」,然後通過 Worker 創建 watchOS 應用匹配 watchOS SDK 中極其有限的 API 來進行開發,不就大功告成?

要實現 RPC 免不了要實現「主從式架構(Client-Sever Model)」,而「事件驅動架構(EDA, Event Driven Architecture)」有利於解耦「客戶端(Client)」和「服務器(Server)」之間的消息處理。

當時天真的我以為,JavaScriptCore 自帶 EDA,於是我向導師匯報了個大致方案,轉身便自己跳進了個巨大的坑裡面……

提要

然而,當我開始讀 W3C 規範的時候,我就崩潰了:摔!原來 EDA 不是 JavaScript 的原生 API 而是 JavaScript DOM 的!無奈之下,我只好硬著頭皮自己在 JavaScriptCore 中手動實現 EDA 了😱。

有興趣了解如何在 JavaScriptCore 中實現 EDA 的同學可以移步至此:WatchWorker

然而,今天我並不是要詳述實習課題中 EDA 的具體實現過程,而是要藉此拿小本本記錄一下我從畢業實習結束到現在關於 EDA 的幾點感悟,以及實現 EDA 的基本模式。

望·觀察 EDA

到目前為止,我接觸過的顯式支持 EDA 開發技術有以下幾種:

JavaScript DOM

W3C 草案規範在 JavaScript DOM 中定義的 EventTarget 接口:

[Constructor,
 Exposed=(Window,Worker,AudioWorklet)]
interface EventTarget {
  void addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options);
  void removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options);
  boolean dispatchEvent(Event event);
};

callback interface EventListener {
  void handleEvent(Event event);
};

dictionary EventListenerOptions {
  boolean capture = false;
};

dictionary AddEventListenerOptions : EventListenerOptions {
  boolean passive = false;
  boolean once = false;
};

嗯,我當時照著這個 IDL 老老實實地在 JavaScriptCore 裡面手動實現了一個 EDA 😂。

.Net(C #,C++/CLI)

準確來說,是 C++/CLI 支持的 Delegates and Events

.Net 中的 INotifyPropertyChanged 就是一個很好的例子:

/* Internal definition. */
public interface INotifyPropertyChanged {
    event PropertyChangedEventHandler PropertyChanged;
}

/* Public context. */
class Clazz: INotifyPropertyChanged {
    private String _property = "";
    public String Property {
        get { return _property; }
        set {
            __property = value
            OnPropertyChanged(nameof(Property))
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] name) => 
        PropertyChanged?.invoke(this, new PropertyChangedEventArgs(name))
}

/* Program enter point. */
class Program {
    static int main() {
        // Instantiate a new Clazz.
        var clazz = new Clazz();
        // Add event handler.
        clazz.PropertyChanged += (sender, args) => WriteLine("on {args.PropertyName} changed.");
        // Change property.
        clazz.Property = "PropertyStringValue"; // It will print: on Property changed.
        return 0;
    }
}

其他

當然,還有其他編程語言也支持原生的 EDA,比如 Microsoft C++ Language 的 _event 等。

聞·認識 EDA

本節主要針對 JavaScript 和 C# 兩種編程語言的 EDA 編程模式進行研究。

JavaScript

綜上觀察,我們發現,在 JavaScript 中,EDA 存在著兩個角色:事件監聽者 EventListener 和事件觸發者 EventTarget

事件觸發者 EventTarget

事件觸發者 EventTarget 負責註冊 addEventListener 或者註銷 removeEventListener 事件監聽者 EventListener,並且適時觸發 dispatchEvent 相應的監聽事件 Event

理論上,如果我們希望觸發事件,則需要實現 EventTarget
在前端開發中,JavaScript DOM 已經為我們實現了大量的 EventTarget,因而大多數情況下我們不需要針對 DOM 重新實現 EventTarget

事件監聽者 EventListener

事件監聽者 EventListener 負責在監聽事件 Event 被觸發的時候處理 handleEvent 監聽事件 Event

理論上,如果我們希望監聽事件,則需要實現 EventListener
在前端開發中,EventListener 通常是有特定簽名的回調函數:(event) => { // handle event },貌似並不需要開發者進行額外的接口實現(此處存疑)。

.Net(C #,C++/CLI)

而在 C# 中,EDA 的表現形式更為簡單,直接通過 event 關鍵字聲明事件:

若要定義一個事件,您可以使用 C# 中的 event 或 Visual Basic 中的 Event 關鍵字在事件類中簽名并指定事件的委託類型。

此外,從上一節《望·觀察 EDA》中我們可以看到,event 可以通過 +=-= 操作符來實現註冊註銷事件處理函數。

問·研究 EDA

經過前一節《聞·初識 EDA》對 EDA 的初步認識,我們能否總結出 JavaScript 還是 C# 中的 EDA的共同點呢?

JavaScript DOM

在 JavaScript DOM 中:

  • 監聽方 EventListner 與被監聽方 EventTarget
  • EventListener 經由 EventTarget 來:
    • 註冊監聽器:addEventListener
    • 註銷監聽器:removeEventListener
  • 只有 EventTarget 能夠觸發事件 dispatchEvent
  • 只有 EventListener 能夠處理事件 handleEvent

.Net(C #,C++/CLI)

在 C#(C++/CLI)中:

  • 通過關鍵字 event 定義事件屬性默認為「被監聽方」,
  • 通過 +=-= 操作符來註冊或註銷的監聽方法 delegate 默認為「監聽方」,
  • 只有「被監聽方」能夠觸發事件 invoke
  • 只有「監聽方」能夠處理事件 delegate

歸納

綜上,JavaScript DOM 和 .Net 中的 EDA 實現都有如下特點:

  • 監聽方被監聽方兩種角色,
  • 監聽方可以經由被監聽方註冊註銷事件監聽器,
  • 只有被監聽方有權決定觸發相關事件的時間和方式,
  • 只有監聽方有權自行決定相關事件的處理方式。

後續

鑒於 JavaScript DOM 中的 EDA 實現過於複雜,為從簡,下面將從集中討論 C# 的 EDA 模式。

有興趣了解 JavaScript DOM 中「事件輪詢」的同學可以移步至此:Event Loop

C# 的 event 關鍵字有如下兩個重要特點:

  • event 通過執行 +=-= 操作符來註冊和註銷 delegate
  • event 通過執行 invoke 來觸發事件處理 delegate 并傳遞事件參數。

綜上,event 必然是一個包含一系列 delegate 的集合,並且可以通過 invoke 來觸發每一個 delegate 并傳遞事件參數。

切·實現 EDA

最近因工作需要,在安卓相關開發中我已經脫離 Java 而全面轉至 kotlin,鑒於基於 JVM 的 kotlin 剛好沒集成基本的 EDA,因此本節將使用 kotlin 來實現一個最基本的 EDA。

目前我在 kotlin 中還無法創建關鍵字 event,不過我們可以通過 type alias 來實現類別名匹配。

事件委託 EventHandler

事件委託是一類有著特定簽名的處理函數,此處我們可以比照 C# 中的 EventHandler 委託簽名:

/**
 * Represents the method that will handle an event when the event provides data.
 * @param TEventArgs The type of the event data generated by the event.
 * @param sender The source of the event.
 * @param args An object that contains the event data.
 */
typealias EventHandler<TEventArgs> = (sender: Any, args: TEventArgs) -> Unit

事件 Event

事件是一系列事件委託的集合。

/**
 * Represents a message sent by an object to signal the occurrence of an action.
 * @param TEventArgs The type of the event data generated by the event.
 */
typealias Event<TEventArgs> = MutableList<EventHandler<TEventArgs>>

為從簡,此處使用一個簡單列表 MutableList 來託管事件委託。

前面說過,一個 event 還可以 invoke 集合中的所有事件處理委託,我們可以通過 kotlin 的拓展(Extensions)方法來實現:

/**
 * Invokes each handler with [sender] and [args].
 * @param sender event sender.
 * @param args event arguments, with default value [EventArgs.EMPTY].
 * This extension is only attached to a list of [EventHandler].
 */
operator fun <TEventArgs> Event<TEventArgs>.invoke(sender: Any, args: TEventArgs) = forEach { handler -> launch(CommonPool) { handler.invoke(sender, args) } }

為從簡,此處通過 kotlin.coroutines.experimentallaunch 來實現異步執行事件處理委託,以免堵塞線程。

示例重寫

至此,我們已經可以用 kotlin 重寫前面的 C# 示例了。

定義 EventArgs

C# 內置的 EventArgs 是一個抽象類,用來定義包含事件需要傳遞的數據,並且提供一個靜態的空值對象 EventArgs.Empty 以供不需要傳遞數據的事件使用。
本示例在 kotlin 中實現相同的類定義:

/**
 * Represents the base class for classes that contain event data, and provides a value to use for
 * events that do not include event data.
 */
abstract class EventArgs {
    companion object {
        @JvmStatic
        val EMPTY = object : EventArgs() {}
    }
}

定義 INotifyPropertyChanged

C# 內置的 INotifyPropertyChanged 接口定義比較簡單,就一個 event 屬性 PropertyChanged
本示例在 kotlin 中將實現相同的接口定義:

/**
 * Notifies clients that a property value has changed.
 */
interface INotifyPropertyChanged {
    /**
     * Occurs when a property value changes.
     */
    val eventPropertyChanged: Event<PropertyChangedEventArgs>
}

何謂 PropertyChangedEventArgs?本例中當然也是會實現跟 .Net 中內置類 PropertyChangedEventArgs 相同的類定義啦:

/**
 * Provides data for the [INotifyPropertyChanged.eventPropertyChanged] event.
 */
data class PropertyChangedEventArgs(val propertyName: String) : EventArgs()

實現 INotifyPropertyChanged

按前例:

class Clazz: INotifyPropertyChanged {
    var property: String by Delegates.observable("") { property, oldValue, newValue ->
        if (newValue == oldValue) return@observable
        onPropertyChanged(property.name)
    }
    override val eventPropertyChangedEventHandler = Event<PropertyChangedEventArgs>();
    private fun onPropertyChanged(name: String) = 
        eventPropertyChangedEventHandler?.invoke(this, new PropertyChangedEventArgs(name))
}

觸發事件

按前例:

fun main(args: Array<String>) {
    // instantiate a new Clazz.
    val clazz = Clazz().apply {
        // add an event handler on property changed.
        eventPropertyChangedEventHandler += { sender, args ->
            println("on ${args.propertyName} changed.")
        }
    };
    // change property.
    clazz.property = "PropertyStringValue" // It will print: on property changed.
}

結論

綜上所述,一個 EDA 須:

  1. 實現監聽者處理事件,
  2. 實現被監聽者註冊註銷以及觸發事件,
  3. 定義必要的事件委託協議,以供監聽者與被監聽者之間實現消息通信,
  4. 定義必要的事件參數對象,以供監聽者與被監聽者之間實現數據傳遞。
  5. 歡迎補充……