Skip to content

Latest commit

 

History

History
246 lines (188 loc) · 13.5 KB

2.entity的面向对象.md

File metadata and controls

246 lines (188 loc) · 13.5 KB

luacluster的面向对象

在luacluster中实现了在lua语言中最复杂,最全面的面向对象功能。为了避免功能上有遗漏,我还重新翻看了《C++面向对象程序设计》。

在《Programming in Lua》中的第16章,用lua语言实现了一个简单的面向对象功能。只能实向单继承,没有多态,链表继承的方式导致对象的操作非常复杂。最大的问题是很霸道的占用了lua的元表。导致无法再用元表进行其他的功能扩展。当然《Programming in Lua》本身就是玩具性质的编程教程不能对他苛责太多。

对象,继承,多重继承,多态

概述面向对象

面向对象本身是一个让人又爱又恨的东西。尤其是C++的面向对象,复杂!非常的复杂!因为C++的面向对象中掺入了很多语言层面不应该有的东西。面向对象的概念还是很先进的。以数据和函数作为一个整体来划分软件的功能。通过继承的方式实现软件的分层。这样在开发软件的过程中就可以从基类开始到子类进行依次开发。也可以多人协同开发。这样开发的软件和我们的应用场景上是一一对应的。也就是说面向对象的开发方法能很好的对接各种应用的复杂性。

我们可以回忆一下在80年代流行的面向函数编程。哪个时候的软件开发喜欢把变量归变量库归库。最早的linux系统需要把你的软件库和配置文件都分别放入系统的指定目录。从应用的角度来说很好理解,应用面对的都是系统的。但从开发者的角度来说就很难受了。在多人协同开发的中大型软件里每个开发者不可能把熟悉所有系统。所以产生了面向对象的开发方式。每个开发者只负责自己开发的部分。在项目内尽可能的复用代码。以减少不同模块之间的耦合错误。

所以lua也好C++也好,代码的面向对象概念是给开发者或开发团队使用的。主要用于开发过程中代码功能的划分。哪么继承,多重继承和多态就是代码缝合的一种方法。unity中的组件也是代码缝合的一种方法。组件是一种弱缝合的方法。不同组件之间是相对独立的。而继承是一种强缝合的方法。通过继承实现的对象访问方法时不需要指定功能。组件的方式可以在软件运行时动态挂接。我觉得这样不好,可能会将编译时态的问题扩展到运行时态。

这一个小节里阐述了,不要把面向对象神话。开发过程中的面向对象就是一种组织代码的方式。运行时态下的面向对象和开发时态的面向对象虽然都叫对象。但却是完全不同的东西。就像设计图纸和工业成品的区别。

继承,多重继承,多态

通过上一节我们知道继承,多重继承和多态是一种代码的组织方式。在luacluster中通过entity创建的对象都具有继承,多重继承和多态的功能。

以client.lua对象为例entity.New()创建一个新的对象。这个对象通过Inherit继承spaceplugin对象。

function accountFactory.New()
  local obj = entity.New()
  obj:Inherit("spaceplugin")

Inherit函数定义在entity.lua中我通过注释的方式来简单说明下流程。

    function rawobj:Inherit(parant) 
    	--1.检查当前的继承列表检查是否已经被继承过了,不允许重复继承
        if self.__inherit[parant] ~= nil then
            error("Repeated inheritance ["..parant.."]")
            return
        end
		--2.调用new创建被继承的对象
        local parantFactory = require(parant)
        local parantObj = parantFactory.New()
        if parantObj == nil then
            error("Inherit error to New ["..parant.."]")
            return
        end
		--3.检查父类的继承列表是否有重复继承,没有就添加到当前的继承列表中
    	--__inherit是当前类继承的列表,__allparant是所有继承树的类列表
        for k, v in pairs(parantObj.__allparant) do
            if self.__allparant[k] ~= nil then
                error("Repeated inheritance ["..k.."]")
                return
            end
            self.__allparant[k] = v
        end 

        self.__inherit[parant] = parantObj

        if self.__allparant[parant] ~= nil then
            error("Repeated inheritance ["..parant.."]")
            return    
        end
        self.__allparant[parant] = parantObj
 
    	--4.将父类的__rawobj用户数据拷贝到当前类,注意是浅拷贝
        if parantObj.__rawobj ~= nil  then
            for k, v in pairs(parantObj.__rawobj) do self.__rawobj[k] = v end
        else
            for k, v in pairs(parantObj) do self.__rawobj[k] = v end
        end
    
    	--5.将标志过滤数据复制到当前类
        if parantObj.__FlagFilter ~= nil then
            for k, v in pairs(parantObj.__FlagFilter) do
                for key, fun in pairs(v) do
                    self:AddOneFlagFilter(k, fun)
                end
            end
        end
    
    	--5.将key的过滤数据复制到当前类
        if parantObj.__KeyFlags ~= nil then
            for k, v in pairs(parantObj.__KeyFlags) do self.__KeyFlags[k] = v end
        end
		
    	--6.将key的过滤函数复制到当前类
        if parantObj.__FlagFilterFun ~= nil then
            for k, v in pairs(parantObj.__FlagFilterFun) do self.__FlagFilterFun[k] = v end
        end
    
    	--7.将需要刷新到数据库的key复制到当前类
        if parantObj.__FreshKey ~= nil then
            for k, v in pairs(parantObj.__FreshKey) do self.__FreshKey[k] = v end
        end
    end

我们可以在New()函数中多次调用Inherit实现多重继承。通过上述代码可以看到所有被继承的类都放入了__allparant变量中。

这样我们就可以轻松的使用这个变量来实现对entity对象的多态调用。即有相同函数名但在继承过程中被覆盖的函数。例如在dbentity对象中我们在dbplugin父类和dbentity子类中都实现了SaveBack函数。在dbentity子类中需要调用dbplugin父类的函数。就可以通过__allparant实现。

    function  obj:SaveBack(dbid)
        self.__allparant["dbplugin"].SaveBack(self,dbid)
        print("dbentity id",dbid)
        self.b = {a = 3, b = 4}
        self:SaveUpdate()
        self:Load(dbid)
    end

entity的元表

我们知道lua的元表提供对于key的访问过滤。可以在key创建,循环,查询时提供callback函数进行过滤。在entity使用了原表进行各种功能的扩展。

    setmetatable(wrap,{
        __index = function (t,k)
            return t.__rawobj[k]
        end,
        __newindex = function (t,k,v)

            if t.__KeyFlags[k] ~= nil and t.__FlagFilter[t.__KeyFlags[k]] ~= nil then
                for key, fun in pairs(t.__FlagFilter[t.__KeyFlags[k]]) do
                    fun(t,k)
                end

                if type(v) == 'table' then
                    if getmetatable(v) == nil then
                        t.__rawobj[k] = entityFactory.CreateSub(v, t, k, t, k)
                        return
                    else
                        if v.__rawobj == nil or v.__entity == nil then
                            error("An attempt was made to assign an object that cannot be serialized "..k)
                        else
                            t.__rawobj[k] = v
                            return
                        end
                    end
                else
                    t.__rawobj[k] = v
                    return
                end
            end
            t.__rawobj[k] = v
        end,

        __ipairs = function(t)
            return ipairs(t.__rawobj)
          end,

        --__pairs会导致调试器的循环失效,显示错误的数据
        __pairs = function(t)
            return pairs(t.__rawobj)
          end,
    })

主要分为两种因为用户数据都被保存在rawobj中所以添加了 index,newindex和ipairs,pairs用于对rawobj的读取和写入。在newindex中额外添加了entityFactory.CreateSub功能用于对于用户创建的table添加一个元表。当对这些数据访问和修改时触发存储数据或广播的功能。这里我们知道用户数据都是保存在rawobj中。entity提供了一个面对对象功能的壳子。

因为lua语言本质来说是一个在伪汇编基础上实现的类C的脚本语言。所以lua的面向对象就是在类C脚本的基础上通过lua自带的扩展实现的面向对象。不可能像java或c++那样的存面向脚本一样。把面向对象的功能完全透明化。这部分功能在使用过程中门槛还是比较高的。

对象的创建,限制和安全

对象的创建

我将面向对象分开,一部分是继承和多态。哪么另一部分就是创建限制和安全。这两部分中第一部分是针对开发过程中的。而这一部分是针对运行过程中的。这里面还有一个要素是对象的通信。我们下一个小节来讲。

对象的创建有三个方法。一个是可以通过entitymng.EntityToCreate方法通过脚本来实现。EntityToCreate有两个参数,一个是要创建对象的位置。

sc.entity.DockerCurrent = 0 --当前ddocker
sc.entity.DockerRandom = 1 --当前节点的随机ddocker
sc.entity.NodeInside = 2 --任意内部节点
sc.entity.NodeOutside = 3 --任意有对外部通信节点
sc.entity.NodeRandom = 4--任意随机的节点
sc.entity.DockerGlobe = 5 --放入全局节点

一个是对象创建成功后默认数值。例如空间对象的创建如下

        --创建基本的space
        entitymng.EntityToCreate(sc.entity.DockerGlobe , "sudokuex", {bigworld=self.id,
                              beginx = sc.bigworld.beginx - sc.sudoku.girdx,
  	                          beginz = sc.bigworld.beginz - sc.sudoku.girdz,
                              endx = sc.bigworld.endx + sc.sudoku.girdx,
                              endz = sc.bigworld.endz + sc.sudoku.girdz,
                                                                    oid = 0 })

第二方法是在新的tcp链接时会创建一个绑定的脚本对象。通过TCP协议发送的方法调用都被发送到这个对象。

第三个方法是通过sc.lua创建的全局对象。这个全局对象在集群中是唯一。例如空间对象和存储对象。创建的对象会通过entityMng.NewEntity被创建出来。

对象的限制和安全

在传统的面向对象的语言中有对像属性的访问限制。有public,private,protect等。我其实是非常困惑的。因为在开发层面这种限制是没有任何意义的。这就好比你左手限制右手一样。起不到任何作用,还白白增加开发复杂度。但在luacluster中这种限制就是实实在在的问题。因为要限制客户端发过来的调用请求。否则会导致严重的安全事故。

在entity中可以添加sc.keyflags.exposed标志来明确指出用户可以调用的函数。

function accountFactory.New()
    local obj = entity.New()
    obj:Inherit("spaceplugin")
    obj:AddKeyFlags("Ping", sc.keyflags.exposed)
    obj:AddKeyFlags("Move", sc.keyflags.exposed)
    obj.client = tcpproxy.New(obj.id)

例如在client中的Ping和Move函数就是客户端可以调用的。

对象KEY的过滤

除了上一个小节中提到的sc.keyflags.exposed标志外。我们还有其他key的标记

sc.keyflags.exposed = 1--客户端可调用
sc.keyflags.persistent = 2--保存到数据库
sc.keyflags.broadcast = 4--广播给所有可见对象
sc.keyflags.private = 8--同步到客户端

注意这些标记都是针对对象的第一层key。对于key下面的子key是无效的。虽然我在子表内添加了元表进行过滤。但对于子表的过滤开销还是太大了。所以当key被触发后保存,广播和同步的操作都也是针对对象的第一层key的。例如dbentity的保存操作。

    function  obj:SaveBack(dbid)
        self.__allparant["dbplugin"].SaveBack(self,dbid)
        print("dbentity id",dbid)
        self.b = {a = 3, b = 4}
        self:SaveUpdate()
        self:Load(dbid)
    end

self:SaveUpdate()的操作是针对"b"的。所有"b"下面的数据都会被保存到数据库。同理broadcast和private也是一样的。

对象间的通信

在luacluster的对象之间的调用是通过异步通信实现的。必须使用多线程队列或udp通信。虽然这样会损失部分性能。因为同线程的的对象间调用也必须通过多线程队列。性能损失带来的是对象之间的通信必然是异步和有序队列的。在脚本中使用udpProxy或udpProxyList创建对象代理。通过docker.Send发送异步调用。

C++的对象之间没有实现异步通信。却加入了大量通信限制的方法。在不恰当的地方做不恰当的事情。导致了语法上很别扭的行为。

计算机内的对象会代表着在虚拟世界中的一个对象。这个对象和我们现实世界的对象具有一样性质。这种性质导致了像C++友元的语法是砌墙又挖洞的行为。所谓砌墙又挖洞的意识是。你划分了一个边界。然后在边界上不使用正常的通信。而这种行为是又挖了一个洞。为了这个洞很可能又砌墙。就会陷入同步异步的死循环中。在luacluster就不存在这样的问题。因为luacluster规定了对象外都是异步,对象内都是同步。在对象内才能考虑代码的继承和复用。如果我们把继承代码复用放到运行时态就很容易陷入到这种递归陷阱里。

好吧今天就到这里,祝大家节日快乐。