淺談對象之間通信的解決方案——Event機制

  歡迎參與討論,轉載請註明出處。
  本文轉載自https://musoucrow.github.io/2017/04/19/event_mechanism/

前言

  在程序設計的時候,不同對象與模塊間總是不可避免會發生相互調用的情況,如果選擇將對象互相作爲參數傳入給對方使用,那麼這種現象一般被稱爲耦合,這樣實際上就讓兩個部分連在了一塊。當然這樣子實際上並沒有什麼問題,只要這符合你的設計預期。只是一旦開發規模增大、開發人員增多、耦合程度加劇的話,程序的維護成本也便會隨之劇增。往往會出現某個模塊在另一個模塊處被做了一些修改而不自知,以及在腳本語言的情況下,沒有private保護的對象到了他處等同徹底的暴露,隨便的修改的話,封裝性也隨之不存。那麼由此看來,必須擁有一套對象之間通信的解決方案了,本文便是提供了一種思路供給參考。
  以下代碼演示將使用Lua語言,接下來的內容對閱讀者的Lua水平有一定的要求。如果你不知道在Lua中class的實現方式,可參考這篇文章

使用機制之前的做法

  首先我們演示一下不用Event機制之前的做法,也就是一般人會用的做法。

-- A.lua

local Class = require("class")
local A = Class()
local B = require("b")

function A:Ctor()
    self.value = 1
    self.b = B.New(self)
end

return A
-- B.lua

local Class = require("class")
local B = Class()

function B:Ctor(a)
    self.a = a
end

function B:Print()
    print(self.a.value)
end

return B

使用機制之後的做法

  由上可以看出,這種直傳對象的做法其實是十分危險的,它很容易破壞封裝性,並且會達到「你中有我,我中有你」的效果,並且這還是在上下級的情況下。接下來便演示下使用了Event機制後的做法。

-- A.lua

local Class = require("class")
local A = Class()
local B = require("b")

function A:Ctor()
    self.value = 1
    self.event = function(self, ...) self:OnEvent(...) end
    self.b = B.New(self.event)
end

function A:OnEvent(type, ...)
    if (type == "GetValue") then
        return self.value
    end

    return "No Event"
end

return A
-- B.lua

local Class = require("class")
local B = Class()

function B:Ctor(upperEvent)
    self.upperEvent = upperEvent
end

function B:Print()
    print(self.upperEvent("GetValue"))
end

return B

  通過對比可以看出,使用了Event機制後,不僅保證了封裝性,而且還隔離了A對象的實現,換句話說,B對象的upperEvent已經不僅限於A了,只要是提供了一致的Event接口即可,並且A對象還能清除的知道自己對外究竟提供了什麼接口,可維護性也隨之提高了。
  當然代價還是有的,那便是調用Event時的函數調用次數比傳統方式有所增加,但我認爲這是值得的。當然這只是一種思路,也未嘗不可優化。

疑難解答

  問:關於兩個平級對象需要相互調用要怎麼做?
  答:參考以下例子:

-- A.lua

local Class = require("class")
local A = Class()
local B = require("b")
local C = require("c")

function A:Ctor()
    self.value = 1
    self.event = function(self, ...) self:OnEvent(...) end
    self.b = B.New(self.event)
    self.c = C.New(self.event)
end

function A:OnEvent(type, ...)
    if (type == "GetValue") then
        return self.b:GetValue()
    elseif (type == "SetTag") then
        return self.c:SetValue(...)
    end

    return "No Event"
end

return A
-- B.lua

local Class = require("class")
local B = Class()

function B:Ctor(upperEvent)
    self.upperEvent = upperEvent
    self.value = 1

    self.upperEvent("SetTag", "123")
end

function B:GetValue()
    return self.value
end

return B
-- C.lua

local Class = require("class")
local C = Class()

function C:Ctor(upperEvent)
    self.upperEvent = upperEvent
    self.tag = ""
end

function C:SetTag(tag)
    self.tag = tag
end

function C:Print()
    print(self.upperEvent("GetValue"))
end

return C

  由此可見,平級對象的相互調用,只需要統一由上級管理好需要調用的接口,然後平級對象從上級Event中獲取即可。

  問:一個對象可以擁有多個OnEvent()麼?也就是針對不同對象派出不同的Event。
  答:一般來說不推薦這麼做,因爲這隻會使得維護成本提升。當然你很清楚自己的需求以及這麼做的代價的話,但試無妨。

  問:使用Event機制後有效的保證了封裝性,但是對於上級管理下級的時候並不存在這種封裝性的保護,那麼該怎麼辦呢?
  答:這個在腳本語言裏是一個沒辦法的事,原則上最好是不要對外直接暴露變量,只使用函數。哪怕這會帶來更高的性能代價,對於維護性而言也是值得的,當然這個問題或許也可以通過特殊的手段解決(參考Lua元表的內容)。

  問:是不是嚴格意義所有情況下都不應該直傳對象而採用Event呢?
  答:這樣顯然是不現實的,比如某些類的業務本身就需要獲取到對象本身(如容器),以及Event本身也是有性能代價以及構建成本的,不可能面面俱到。在某些你認爲必要且可掌控的情況下,直傳對象也並非不可以。畢竟解耦的目的也是爲了提高可維護性,只要你覺得這樣做是可以接受的,那麼便可以了。

後記

  以上內容僅爲提供一個思路,它或許並非最完善的,也不一定能適用於所有編程語言中,對此有所心得者,歡迎前來探討,提供更佳的思路。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章