12.26.2016

A[nother] Simpler Tutorial for Reactive Extensions(譯)

不經意之間看到這一篇文章,讓我更清楚Reactive Extensions可以如何被應用。Push-style觀察者模式的實作,需要我們轉換思考的方式,以獲得它的好處。

來自A[nother] Simpler Tutorial for Reactive Extensions

多年前,我在VB6中編寫了一個小Console應用程序,通過使用邏輯閘來模擬數位邏輯電路。這是一個很好的學習OOP的方法,因為每個類別直接對應一個日常中見到的實體,這種方法比通過dogs barking或cows mooing等傳統學習OOP所提及的範例更有用。通過連接一些事件和一個基本的輸入形式,我能夠模擬相當複雜的電路的結果。這個問題的一部分是,由於事件模型架構,它永遠在記算結果值,和數百行程式碼。

快進到去年,當我聽到Reactive Extensions for .NET,一個真正建立在IEnumerable <>之上的push-style觀察者模式的實作。對於在事件和同步編程的環境中成長的我,很難轉換想法,但像許多概念,我最終“got it”。但是,如果有更好的教學,我會更快地達到這個目標。所以,這個教學目的在作為一個更好的入門,或者至少是較簡單的教學。

大部份的Rx教學讀起來像“Ok,你有一些X及Y值,現在這是你如何可以使用五十種鏈接函式來合併它們的方式。“一旦你使用Rx,這很容易掌握,但你不知道可以實際上用Rx來做些什麼,很難從這種進階的水平來開始學習。所以,這個教學會採用一些簡單的東西,並告訴你為何用pushing值會比傳統的pulling值的方式來的簡單。好吧,我們用一個AND邏輯閘。 一個AND邏輯閘具有兩個輸入位元,每個位元為0或1,只有當兩個輸入均為1時,才輸出1。如果任一輸入為0,則輸出為0。這個AND邏輯閘的輸出可以是最終電路輸出,或者可以將其作為輸入放置到另一個邏輯閘。電路中所有邏輯閘的組合輸出一些最終值,其值皆基於該電路的輸入。本教學將只模擬一個單一的AND邏輯閘,其餘留待未來的文章。

那麼,Rx在這裡可以幫到什麼?AND邏輯閘取得輸入值,此值可能隨時間改變(由於使用者操作,或其他輸入閘的結果改變)。我們不想只坐在那裡,不斷地輪詢輸入以查看它是否改變,我們只想要在輸入改變時重新計算我們的輸出值。所以,我們可以設置一些事件來模擬這個,或者我們可以簡單地將新值Push到邏輯閘中。Rx在這裡是允許你將值推送給任何觀察者的中介者。

Rx要求我們重新思考輸入值發生的方法。在Windows Forms(甚至WPF)中,我們習慣於把使用者的互動想成是單一的隔離的事件,例如”按下按鈕1,按下checkbox,按下按鈕2“。實際上,最好將使用者的互動模塑成一連串的stream,例如,如果使用者按下兩個按鈕,中間有數百個MouseMove事件,也可能是標籤頁更改,窗口大小調整或任何數量的事件。因此,使用者互動是關於使用者正在使用所有輸入設備做什麼的恆定數據串流。同樣,數位電路的輸入不是一系列隔離事件,它是1秒或0秒的恆定串流,輸出會由於在整個系統中發生的某件事的結果而改變。 所以,為什麼我們不能簡單地訂閱輸入資訊串流,而要靠著不斷地查看輸入值來檢測變化?然後我們可以在取得任何新的輸入時即做出決定,節省不必要的代碼和處理時間。

因此,讓我們考慮一些可以在這裡建模的實體。AND邏輯閘有兩個我們需要訂閱的輸入,任何時候其中一個輸入給我們一個新的值,我們應該馬上更新輸出值。然而,就像我們的輸入被認為是一個串流,我們的輸出也應該是一個串流,因為值是不斷變化的。我們應該自訂一個Bit類型而不是使用Boolean true / false,它可以是high或low,所以列舉會很適合。除此,我們希望將來與其他邏輯閘一起工作,因此我們將創建一個通用Gate類別,並使用繼承來提供自定義功能。所以,讓我們開始吧。

public enum Bit { Low = 0, High = 1 }
public class Input
{
     public void SetValue(Bit value)
     {
          // something to update the value stream
     }
}

Ok,這裡我們定義了Bit類別,以及一個公開的允許改變輸入值的基本方法。但請記住,我們不是簡單地來回切換值,而是將新值發佈到串流中。那麼,我們如何模擬這樣的串流呢?這是Rx發揮作用的地方。Rx的核心是稱為IObservable <T>的介面。介面很簡單,它只是提供一種訂閱”型別T“串流的方法。因此,為了此目的,任何輸入或輸出皆可由Bit串流表示,在Rx中可用IObservable <Bit>代表。但是,你可能知道我們不能簡單地向Input類別添加型別為IObservable <Bit>的參數,因為它是一個介面,而我們需要一個具體型別。我們可以在我們的類中實現IObservable,但是我們必須追踪訂閱者,並提供一種處理訂閱的方式。在生產代碼中應採用這種方式,但為了我們的目標,我們使用Rx團隊為此提供的類別,Subject <Bit>。這簡單地提供我們一個從Bit值到Bit串流的快速方式。所以,讓我們更新我們的輸入代碼:

public class Input
{
     private Subject<Bit> valueStream = new Subject<Bit>();
     public IObservable<Bit> Value { get { return valueStream; } }
     public void SetValue(Bit value)
     {
          valueStream.OnNext(value);
     }
}

所以,在這裡,我們建了一個private的valueStream的Subject<Bit>。通過Value屬性讓串流可被訂閱(IObservable)。每當設定一個值時,我們只需要讓串流附加該值即可。現在,我們的Input類別有它需要操作的一切。它公開了一個可被訂閱的串流,並且每當新值被指定時更新串流。現在我們的輸入是函數式的,讓我們做一個通用的邏輯閘:

class Gate
{
     public Gate(IObservable<Bit> x, IObservable<Bit> y)
     {
          valueStream = x.CombineLatest(y, Calculate);
     }
     protected IObservable<Bit> valueStream;
     public IObservable<Bit> Value { get { return valueStream; } }
     protected virtual Bit Calculate(Bit x, Bit y)
     {
          return Bit.Low;
     }
}

所以,在這裡你可以看到邏輯閘的輸出也是類似的模式,使用valueStream和Value。這個閘可以像任何輸入一樣被訂閱。主要的差別在,我們必須提供兩個IObservable <Bit>以讓其計算輸出,這一行valueStream = x.CombineLatest(y, Calculate);簡單地意味著它需要X和Y輸入,並使用每個的最新值來決定下一個輸出,將該值直接指定給輸出串流。你可能注意到我們不必“處理”任何事件,或明確地告訴它輸出新的值。它的行為很像一個管道,每當它得到一個新的輸入,就會輸出基於Calculate()函式計算的結果。 在預設邏輯閘類別中,不管輸入什麼,我們簡單地輸出low值。

現在我們有了一個通用的邏輯閘,就可以基於此來實作And邏輯閘了:

class AndGate : Gate
{
    public AndGate(IObservable<Bit> x, IObservable<Bit> y) : base(x, y) { }
    protected override Bit Calculate(Bit x, Bit y)
    {
        return (x == Bit.High && y == Bit.High) ? Bit.High : Bit.Low;
    }
}

如你所見,我們讓它引用父類別的建構式,並覆載Calculate()函式以提供And邏輯所需的計算。其它的邏輯閘,如OR, XOR, NAND都可以此方式建構。現在,我們通過連接一些輸入值來測試,並定義我們對And邏輯閘的訂閱(在這種情況下只是將其寫入Console),然後更改輸入值,以便讓你看到它的作業:

class Program
{
    static void Main(string[] args)
    {
        Input a = new Input();
        Input b = new Input();
        AndGate a1 = new AndGate(a.Value, b.Value);
        a1.Value.Subscribe(z => Console.WriteLine(z.ToString()));

        a.SetValue(Bit.High);
        b.SetValue(Bit.High); // High + High = High
        b.SetValue(Bit.Low); // High + Low = Low
        b.SetValue(Bit.High); // High + High = High
        a.SetValue(Bit.Low); // Low + High = Low
        b.SetValue(Bit.Low); // Low + Low = Low

        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
    }
}

執行它,你可以看到它的作用:

High
Low
High
Low
Low
Press any key to exit

後續文章,我們將介紹如何連接更多邏輯閘,以及對Rx更詳細的介紹。

Written with StackEdit.

沒有留言:

張貼留言