從釋出-訂閱模式談談 Flask 的 Signals

leetao94發表於2020-07-02

釋出-訂閱模式

釋出-訂閱模式,顧名思義,就像大家訂報紙一樣,出版社釋出不同型別的報紙雜誌不同的讀者根據不同的需求預定符合自己口味的的報紙雜誌,付費之後由郵局安排人員統一派送.

上面一段話,提到了釋出-訂閱模式三個比較重要的點:

  1. 釋出者:報社
  2. 訂閱者:讀者
  3. 排程中心:郵局

不難看出上述過程中出版社和讀者完全沒有任何接觸,在他們沒有感知到對方的情況下通過郵局完成了整個流程,郵局就是傳說中的中介(Broker)

那麼使用釋出-訂閱模式的有什麼優點呢?這裡就簡單的說兩點:鬆耦合,可擴充性,稍後通過例子進行講解. 關於更深入的理解可以參考:

  1. Publish/Subscribe-MSDN
  2. Publish/Subscribe-Wikipedia

對這個模式有所瞭解之後,讓我們再回到 Flask 的 Signals

Flask - Signals

說明

有了前面這個鋪墊,不難意識到 Flask 的 Singals 其實就是我們上面說的釋出-訂閱模式的實現.官方文件對 Signals 的介紹過於簡單,容易讓初學者直接忽略過去,但是實際上這知識點十分重要,尤其在開發比較複雜的系統中,正確地使用 Singals 能夠幫助我們實現系統的鬆耦合.

這種鬆耦合是通過某些行為被觸發時,自動傳送定義好的一種訊號,與這個訊號繫結的一些業務邏輯或行為,接收到這個訊號後,會自動執行各自相應的業務邏輯。這些行為的產生者就是我們在釋出訂閱模式中釋出者,通過排程中心,訊息被轉發到相應的訂閱者,然後每個訂閱者執行自己的邏輯,互不干擾.

就像我們在釋出-訂閱模式看到的那樣,我們可以隨時新增訂閱者. 同樣地,與該訊號繫結的業務邏輯,可以是我們事先預定義好的,也可以是在後續開發中隨需求變動新增上去的. 在基於 Signals 的機制下,系統會更加穩定和可擴充套件,也使得系統的業務邏輯更加清晰.

既然有這麼多好處,那麼該怎麼使用呢?彆著急,看一下幾個例子.

例子

Signal 的建立

兩行程式碼就可以建立 Singals

 from blinker import signal
 test= signal('test')

不過 Flask 文件中有另外一種寫法

from blinker import Namespace
my_signals = Namespace()
model_saved = my_signals.signal('model-saved')

兩者本質上是沒有任何區別的,原因我們可以看一下 blinker 的原始碼

# https://github.com/jek/blinker/blob/master/blinker/base.py
signal = Namespace().signal

很顯然從原始碼看兩者基本上可以等價起來,前者只是幫助我們簡化了一個步驟

Signal 的傳送

signal 建立好了之後,接下來就是使用了,使用很簡單通過呼叫 send() 函式.需要注意的是,官方文件給了一個建議:

Try to always pick a good sender. If you have a class that is emitting a signal, pass self as sender. If you are emitting a signal from a random function, you can pass current_app._get_current_object() as sender.

也就說明我們在實際使用過程中,最好將 send() 函式的第一個引數為 signal 的傳送者

  1. 在類中傳送者(sender) 為 self
class Model(object):
    def save(self):
        model_saved.send(self)
  1. 在函式中傳送者(sender) 為 current_app._get_current_object()
def save():
    model_saved.send(current_app._get_current_object())

傳送完訊息,訊息需要有人看,自然需要訂閱者了.

Signal 的訂閱

訂閱指定的 signal 可以通過使用 connect() 函式,當通過 send() 傳送 signal 時,會自動觸發這些訂閱者,然後執行相應邏輯,從而完成相應的功能. 使用起來很簡單,只需要給指定的函式加上一個 connect_via 或者 connect 的裝飾器就可以了

# connect_via
from flask import Flask,current_app
app = Flask(__name__)

from blinker import Namespace
my_signals = Namespace()
test = my_signals.signal('test')

@test.connect_via(app)
def subscriber(sender,**kwargs):
    print(f'Got a signal sent by {sender},{kwargs}')

@app.route('/')
def hello_world():
    test.send(current_app._get_current_object(),data=3)
    test.send('test')
    return 'Hello, World!'

if __name__ == '__main__':
    app.run()

讓我們執行一下然後看一下結果:

 * Serving Flask app "test" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 326-510-904
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Got a signal sent by <Flask 'test'>,{'data': 3}
127.0.0.1 - - [24/Jun/2019 15:07:31] "GET / HTTP/1.1" 200 -

似乎少了依次輸出?彆著急,我們修改一下這個例子,使用 connect

#  connect
@test.connect
def subscriber(sender,**kwargs):
    print(f'Got a signal sent by {sender},{kwargs}')

再次執行看一下結果

 * Serving Flask app "test" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 326-510-904
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Got a signal sent by <Flask 'test'>,{'data': 3}
Got a signal sent by test,{}
127.0.0.1 - - [24/Jun/2019 15:10:43] "GET / HTTP/1.1" 200 -

connect_via 和 connect

從上面最後一次輸出,不難發現輸出兩次了,為什麼第一次的時候只輸出了一次呢? 很顯然這就是 connect_viaconnect 的區別,從上面的例子,我們不難看到, connect_via 多了一個引數,這個引數就是 sender,使用 connect 的訂閱方式並不支援訂閱指定的釋出者,如果我們需要訂閱指定的釋出者需要使用 connect_via(sender)

最後

Signals 是個好東西,大家應該學會使用它.

參考

  1. 釋出-訂閱模式解釋

  2. Publish/Subscribe-MSDN

  3. Publish/Subscribe-Wikipedia

  4. bliker

  5. Signals

相關文章