luacluster 的面向对象

2022-04-28 15:12:19 +08:00
 gantleman

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 规定了对象外都是异步,对象内都是同步。在对象内才能考虑代码的继承和复用。如果我们把继承代码复用放到运行时态就很容易陷入到这种递归陷阱里。

好吧今天就到这里,祝大家节日快乐。 原文地址: https://zhuanlan.zhihu.com/p/506616009

1311 次点击
所在节点    游戏开发
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/849827

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX