使用实体组件系统构建业务应用程序有什么优势吗?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用实体组件系统构建业务应用程序有什么优势吗?相关的知识,希望对你有一定的参考价值。

我理解使用数据驱动的实体组件系统进行游戏开发的吸引力。当然,我正试图找到其他领域来应用这种范式。当我即将开始开发一个小型企业应用程序时,我一直想知道Entity-Component如何适应它。但是,除了游戏之外,我找不到任何关于在Entity-Component中使用Entity-Component的示例或讨论。有原因吗?除了游戏之外,在软件中使用Entity-Component会有什么优势吗?

答案

(为死灵法术道歉)

来自企业背景,我最近一直在考虑这个问题。实体组件系统相对较新,代表了与大多数业务开发人员将体验到的完全不同的设计范例。

考虑到我自己公司的例子,我已经看到了一些实体组件系统可以带来好处的场景。

例如,在我们的主要应用程序中,地址与联系人和组织相关联。 (在我们的数据库中有ContactAddress和OrganisationAddress连接表。)一个客户希望将项目与地址相关联。有很多方法可以实现这一点,但基于实体组件的方法对我来说似乎相当优雅 - 只需将一个可寻址组件添加到Project实体,GUI就可以自行排序。

相反,我们可能会添加一个新的连接表和新的数据输入页面(虽然重新使用常用控件)。

我认为,主要的缺点是(初始)缺乏开发人员对将这种范例应用于商业软件的最佳方式的认识,正是因为它似乎以前没有做过。一旦你开始采用这种方法,你就会致力于它 - 如果一旦你的项目达到一定的复杂性就会让人感到沮丧,那么没有重大改写就没有出路。

另一答案

我最终承担了风险并尝试在游戏领域之外使用ECS(现在作为一名独立的,以前的公司员工)并且结果令我震惊。我现在不会做任何其他方式的事情,并且拥有比以往更容易维护的系统(不完美,但比我们过去在我的行业中使用的COM风格的架构要好得多)。我主要是因为它似乎为我所有的事情提供了答案,而我的团队过去一直在努力使用COM架构,尽管我想到了这样一个冒险的举动,我可能最终会交换一组问题另一个(因为我独自承担风险)。原来我没有把一罐蠕虫换成另一罐。 ECS几乎解决了所有这些问题,而几乎没有引入任何新问题。

也就是说,我在VFX领域并没有与游戏有所不同。我们仍然需要动画像角色,发射粒子,与网格交互,纹理,播放声音片段,渲染结果,允许人们编写插件,脚本等。

尝试在业务领域中应用ECS更加愚蠢。也就是说,如果您处理大量实体组合的系统相对较少,我认为它可以真正帮助创建一个可维护的系统。

可维护性

我发现,与以前的面向对象方法相比,使ECS更容易维护,甚至在我的个人项目中,以前的方法通常将维护开销从使用类的客户端转移到类本身。但是,会有数十个接口,数百个子类,都继承不同的东西,并实现不同的接口来单独维护。对于如此多的粒度类以及需要进行模拟测试,测试也变得困难。

我的大脑只能处理这么多,并且数以百计的子类彼此交互远远超出了极限。很快我发现自己不再能够推断出发生了什么事情,更不用说在何时何地被复杂的相互作用所淹没导致复杂的副作用,并且从未如此自信以至于我可以将新代码夹在其中而不会产生不必要的副作用。

计算科学家的主要挑战不是让自己制造的复杂性感到困惑。 - E. W. Dijkstra

这甚至适用于我自己编写的项目。有一个突破点,通常在几十万左右的LOC之后,我甚至无法理解我自己的创作。我会在这里和那里重构,拿起一点动力,只是休假,回来,再次迷失。

ECS消除了这一挑战,我并不意味着我可以休假2周,回到代码库,查看一些代码,并获得我写作时的清晰视觉首先。 ECS并没有在这方面做太多改进,而且我仍然需要一些时间才能重新认识一段时间没有看过的代码。 ECS帮助那么多的原因是我不需要回忆我为了扩展和改变软件而写的所有东西。系统是如此分离,如果我忘记了一个人是如何工作的,那就没什么大不了的了。我可以专注于我需要做的事情,而不必担心通过控制流的复杂交互触发的副作用的复杂相互作用。我可以专注于我需要做的事情,而不必考虑其他任何事情。

即使引入了集成到产品中的全新核心级功能,这也适用。如今,当我为产品引入新的核心功能时,就像产品的核心全新音频系统一样,我唯一需要考虑的是如何将其集成到用户界面中。与我以前使用的架构相比,将其集成到架构中相对容易。

与ECS同时,我只需要维护几十个系统,以提供比上述功能更少的功能。它们内部确实有一些复杂的逻辑,但我不需要维护数百种不同的实体组合,因为它们只是存储组件,而且我不需要维护组件类型,因为它们只存储原始数据而我很少永远发现需要回去改变它们(非常接近永远)。

可扩展性

能够事后用中心概念扩展ECS架构是我迄今为止遇到的最简单的事情,并且需要对现有代码库如何工作的最少知识。

作为一个非常新的例子,我最近遇到了一个强烈的愿望,即使用我的软件的脚本编写者能够使用简单的全局名称访问场景中的实体。在他们必须指定一个完整的场景路径之前,Scene.Lights.World.Sunlight而不是简单地,Sunlight

通常在我之前使用过的架构中,这些架构的范围从高度侵入性到中度侵入性的变化。围绕纯接口的COM风格系统可能需要引入新接口,或者更糟糕的是,更改现有接口,并更新几百个子类型以实现新功能。如果我们有一个中心抽象基类,所有内容都已经继承,我们可以集中修改它来实现这个新接口(或现有接口的新部分),但如果有一个中心基础,它可能是怪异的对于可能需要这样一个名字的所有内容的类,并要求涉及许多精细的代码。

使用ECS,我所要做的就是引入一个新的组件GlobalName,它有一个处理GlobalName组件的系统,可以通过指定的名称快速找到实体。它还处理确保没有两个GlobalName组件具有匹配的名称。由于ECS的性质,当这个GlobalName组件被破坏时,由于实体被销毁或者组件从其中移除以保持数据结构用于加速按名称搜索(tie),因此也很容易取出) 同步中。

之后,我只能将这个GlobalName组件附加到脚本编写者想要通过全局名称引用的任何内容。他们也可以自己附加它,然后通过该名称引用给定的实体。组件也以大部分保留向后兼容性的方式自行序列化(例如:以前版本的软件不知道GlobalName在加载引用它的场景数据时会忽略它)。

考虑到事后很晚才对一个4年前的软件进行了更新,并且预计不需要这样做,这就像我可以改变想象的那样无痛而且无干扰。我设法让它在第一次尝试时工作得很好。作为奖励,为使这项工作新增加的所有非平凡代码在其自己的空间中独立存在;如果我使用抽象接口或基类,那么它不会混淆其他任何东西的复杂性,因为它不可避免地会出现这种情况。除了几行简单的脚本和一些简单的GUI代码以显示这些全局名称(无可用)之外,我没有必要修改任何中心功能。

“继承任何地方”

您是否曾希望在不实际修改代码的情况下从代码中的任何位置扩展类的功能?例如:

// In some part of the system exists a complex beast of a class
// which is tricky modify:
class Foo {...};

// In some other part of the system is a simple class that offers
// new behavior we'd like to have in 'Foo', with abstract functionality
// (virtual functions, i.e.) open to substitution:
class Bar {...};

// In some totally different part of the system, maybe even a script,
// make Foo inherit Bar's behavior on the fly, including its default
// constructor, copy constructor, and destructor behavior for Bar's state.
Foo.inherit(Bar);
  • 上面提出了一个问题:Bar的抽象功能将在何处实现,因为Foo没有提供这样的实现?这就是系统类似于ECS的地方。

我认为,对于我们大多数人来说,不得不趟过现有的一些复杂的代码来让它做一些新的事情,同时冒着引起不必要的副作用/毛刺/脚趾踩踏的风险,或者我们甚至可能面对一个对于我们无法控制的第三方库的诱惑只是提供一些功能,如果它只提供“这一件事”,我们可能会讨厌这个想法,我们会发现这些功能在整个代码中使用这个第三方库非常有用不得不改变我们同事的现有代码(不想踩脚趾),即使我们的任务是提供新的中心行为。

ECS为您提供了这种灵活性,尽管与上述示例完全不同(但为您提供了类比优势)。它允许您从任何地方扩展任何行为/功能/状态。与上面的可扩展性示例一样,我不必修改任何存在以提供全局名称搜索功能和状态的内容。我可以从外部扩展这些实体的行为,甚至可以从脚本扩展,只需向我想要的任何实体添加一种新类型的组件,此时我对这些组件感兴趣的任何系统都可以使用它来获取和处理鸭子打字方法(“如果它有一个GlobalName组件,它可以提供一个全局名称,可用于非常快速地找到匹配的组件”)。

关联数据

与上述类似,您是否曾经面临过将数据与代码中的现有对象相关联的诱惑?在这种情况下,我们可能必须维护并行数组或关联容器(如字典/映射),并且这些代码可能很难正确编写,因为它必须在添加和删除新对象时保持同步。

ECS在中央级别解决了这个问题,因为现在您只需附加组件并从中高效地删除组件。这成为您动态关联新数据的手段。您不再需要手动同步关联数据结构。

测试

我个人的另一个问题,也许是因为我从未掌握过单元测试的艺术(虽然我确实与一位真正研究过这个主题的同事合作过),但是它从未让我相信系统是相对的bug -自由。整合测试让我对这方面更有信心。对我来说问题是:即使单元测试通过,你怎么知道客户端不会滥用接口?如果他们在错误的时间使用它怎么办?如果他们故意不设计为线程安全的话,他们试图从多个线程使用它会怎么样?

看到单元测试通过后,我没有感到宽慰,因为遇到的大多数错误与正在测试的接口之间发生的事情有关,尽管我们编写了数百个单元测试,但我们还是有许多传入通过。我喜欢测试驱动开发,我确实在单元测试中找到了价值,告诉我这个单元正在做它应该做的事情,这使我能够在整个代码库中更自信地使用它,但单元测试从未给我对整个代码库的正确性有一种巨大的解脱感。

ECS为我解决了这个问题,并使单位测试更有价值,甚至对于像我这样从未掌握测试艺术的人来说更有价值,因为有一些系统,他们各自做大量的工作(不是细小的小物件),他们具体的。如果我们必须做类似模拟测试的任何事情,只需插入运行它们所需的组件/实体并测试它们。它开始觉得测试系统比单元测试更接近集成测试,即使系统是最小的可测试单元。

同质处理

要应用ECS,需要采用更循环的逻辑,更一致的循环一次做一件事。许多OOP倾向于鼓励非均匀控制流和复杂的交互,导致在系统的任何给定阶段/状态中发生许多事情。这是我最初发现的最困难的部分,因为我想一次将不同的任务应用于给定的实体/组件,而且我的诱惑不能直接给出,因为解耦系统一次只能执行一个任务。因此,我必须学习如何推迟处理,为下一个系统存储一些状态,我还使用(至少)一个事件队列,以便系统可以触发其他人处理的事件。

尽管如此,我找到了一种方法,可以通过一系列简单的循环一次做一件事来编写复杂交互的等价物。它从来没有像我想象的那样难以强迫自己以这种方式工作,同时在一组实体上应用一个统一的任务。并被迫做了一段时间并保持结果 - 哇!我应该一直这样做。这实际上有点令人沮丧,反映了十年来维护架构的难度,这些架构在获得ECS架构的新鲜空气后需要比它们需要的更难维护。

互动

这是一个简化的“交互”图(不一定表示直接耦合,因为耦合版本将从具体对象到抽象接口)比较我采用ECS之前和之后的差异。这是以前的:

enter image description here

除了它只是在少数类型之间(我懒得画几百个)。这就是为什么我总是努力维护这些东西并感到纠结于代码中。这是因为代码之间的交互实际上是一个混乱的混乱,导致你在系统中的各种远程功能导致副作用。之后(现在组件只是原始数据,它们不包含自己的功能):

enter image description here

第二个版本是这样,更容易理解,更容易扩展,更容易维护,更容易在正确性,更容易测试等方面进行推理。如果您的业务架构可以有效适合第二种类型的模型,我不能夸大它可以简化一切。

不变

当我开始开发ECS引擎时,对我来说最可怕的部分之一是缺乏信息隐藏。当组件只是原始数据时,它们悬挂着我认为应该是空闲的私人空间,让任何人都可以触摸。这在业务领域可能会倍加可怕,这可能对任务更具关键性。

然而,我发现不变量同样易于维护,如果不是更多,由于访问任何给定组件的系统数量有限(通常如果数据被修改,只有整个代码库中的一个系统才有意义) ,极其简单的控制流程,以及由此产生的极其可预测的副作用。当你有一些系统担心功能时,很容易测试代码库的正确性。

结论

因此,如果您愿意承担风险,我认为它可能会在某些业务领域得到非常有效的应用。我认为首先要考虑的主要问题是,如果你可以将整个软件的需求建模为少数系统处理存储在组件中的数据,每个系统仍然执行笨重但单一的责任(RenderingSystem的类比等价物) ,GuiSystemPhysicsSystemInputSystem等)。当然,如果您发现需要数百个不同的系统来捕获业务逻辑,ECS的好处就会减少。

如果你感兴趣的话,我可以在稍后的迭代中扩展我的答案,并尝试回顾一下我在ECS面前完全浑身湿透的一些轻微的挣扎。

以上是关于使用实体组件系统构建业务应用程序有什么优势吗?的主要内容,如果未能解决你的问题,请参考以下文章

业务流程管理系统都有哪些?

低代码是什么?有什么优势

低代码是什么?有什么优势

低代码是什么?有什么优势

什么是私有云?您应该知道的 6 个优势

你真的理解微服务架构吗