浅谈对象之间通信的解决方案——Event机制

  欢迎参与讨论,转载请注明出处。

前言

  在程序设计的时候,不同对象与模块间总是不可避免会发生相互调用的情况,如果选择将对象互相作为参数传入给对方使用,那么这种现象一般被称为耦合,这样实际上就让两个部分连在了一块。当然这样子实际上并没有什么问题,只要这符合你的设计预期。只是一旦开发规模增大、开发人员增多、耦合程度加剧的话,程序的维护成本也便会随之剧增。往往会出现某个模块在另一个模块处被做了一些修改而不自知,以及在脚本语言的情况下,没有private保护的对象到了他处等同彻底的暴露,随便的修改的话,封装性也随之不存。那么由此看来,必须拥有一套对象之间通信的解决方案了,本文便是提供了一种思路供给参考。
  以下代码演示将使用Lua语言,接下来的内容对阅读者的Lua水平有一定的要求。如果你不知道在Lua中class的实现方式,可参考这篇文章

使用机制之前的做法

  首先我们演示一下不用Event机制之前的做法,也就是一般人会用的做法。

1
2
3
4
5
6
7
8
9
10
11
12
-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 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机制后的做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 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时的函数调用次数比传统方式有所增加,但我认为这是值得的。当然这只是一种思路,也未尝不可优化。

疑难解答

  问:关于两个平级对象需要相互调用要怎么做?
  答:参考以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 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本身也是有性能代价以及构建成本的,不可能面面俱到。在某些你认为必要且可掌控的情况下,直传对象也并非不可以。毕竟解耦的目的也是为了提高可维护性,只要你觉得这样做是可以接受的,那么便可以了。

后记

  以上内容仅为提供一个思路,它或许并非最完善的,也不一定能适用于所有编程语言中,对此有所心得者,欢迎前来探讨,提供更佳的思路。