使用组件实体系统架构构建应用程序(而非游戏)是否合理?


24

我知道,在构建Apple AppStore或Google Play应用商店中的应用程序(本机或Web)时,通常使用Model-View-Controller架构。

但是,使用游戏引擎中常见的组件实体系统架构创建应用程序是否合理?


Answers:


39

但是,使用游戏引擎中常见的组件实体系统架构创建应用程序是否合理?

对我来说,绝对。我从事视觉FX的研究,研究了该领域的各种系统,其体系结构(包括CAD / CAM),对SDK的渴望以及任何可使我对看似无限的体系结构决策的利弊有所了解的论文即使是最微妙的事物也不会总是产生微妙的影响。

VFX与游戏非常相似,因为它有一个“场景”的中心概念,其视口可显示渲染的结果。在动画环境中,也经常有很多中央循环处理围绕此场景进行,其中可能发生物理现象,粒子发射器生成粒子,对动画进行渲染和渲染的网格,运动动画等,最终渲染它们最后全部交给用户。

至少与非常复杂的游戏引擎类似的另一个概念是需要一个“设计者”方面,设计师可以灵活地设计场景,包括自己进行一些轻量级编程(脚本和节点)的能力。

多年来,我发现ECS最适合。当然,这从来没有完全脱离主观性,但是我要说,它似乎带来了最少的问题。它解决了我们一直在努力解决的许多主要问题,而只给了我们一些新的未成年人的回报。

传统OOP

如果您牢牢把握了设计需求,而不是实施需求,那么更传统的OOP方法将非常强大。无论是通过扁平化的多接口方法还是在更嵌套的层次化ABC方法中,它都趋于巩固设计并使其更难更改,同时使实现更容易,更安全地进行更改。超过单个版本的任何产品都始终存在不稳定的需求,因此OOP方法倾向于使稳定性(变化的难度和缺乏变化的原因)趋向于设计水平,以及不稳定的状态(易于变化和变化的原因)到实施水平。

但是,针对不断发展的用户端要求,可能需要经常更改设计和实现。您可能会发现一些奇怪的事情,例如强烈要求用户端同时需要植物和动物的类比生物,从而完全破坏了您构建的整个概念模型。普通的面向对象方法在这里并不能保护您,有时可能会使这种意料之外的,破坏概念的更改更加困难。当涉及性能非常关键的区域时,设计更改的原因会进一步增加。

组合多个细粒度接口以形成对象的合规接口可以在很大程度上稳定客户端代码,但是在稳定子类型方面却无济于事,这有时会使客户端依赖项的数量相形见war。例如,您可以让一个接口仅被系统的一部分使用,但可以使用一千种不同的子类型来实现该接口。在那种情况下,维护复杂的子类型(之所以复杂是因为它们要履行许多不同的接口职责)可能会成为噩梦,而不是代码通过接口使用它们。OOP倾向于将复杂性转移到对象级别,而ECS则将复杂性转移到客户端(“系统”)级别,当系统很少,但有一堆合格的“对象”(“实体”)时,这是理想的选择。

在此处输入图片说明

一个类还私有地拥有其数据,因此可以独自维护所有不变式。尽管如此,当对象之间进行交互时,实际上仍然很难维护某些“粗糙”的不变式。为了使一个复杂的系统整体处于有效状态,即使适当地维护了它们的各个不变量,通常也需要考虑对象的复杂图。传统的OOP风格方法可以帮助维护粒度不变式,但是如果对象集中在系统的微小方面,则实际上可能很难维护宽泛的粗糙不变式。

在这种情况下,此类构建乐高积木的ECS方法或变体可能会非常有用。同样,由于系统的设计比通常的对象要粗糙,因此在系统的鸟瞰图上维护这些类型的粗糙不变量变得更加容易。许多小对象交互变成了一个专注于一项广泛任务的大型系统,而不是小对象专注于具有覆盖一公里纸张的依赖图的小任务。

尽管我一直是面向数据的心态之一,但我还是不得不向游戏行业以外的领域学习ECS。而且,很有趣的是,我自己反复尝试并尝试提出更好的设计,就差一点就走向了ECS。我并没有完全做到这一点,而错过了一个非常关键的细节,那就是“系统”部分的形式化,并将组件压缩到原始数据。

我将尝试介绍如何最终解决ECS,以及最终如何解决以前的设计迭代中的所有问题。我认为这将有助于突出说明为什么答案可能是非常肯定的“是”,因为ECS的潜在应用范围远远超出游戏行业。

1980年代蛮力建筑

我在VFX行业工作的第一个架构具有很长的历史,自我加入公司以来已经过去了十年。一直以来都是蛮力的C编码(我并不喜欢C,但对C偏爱,但是在这里使用它的方式确实很粗糙)。微型且过于简单的切片类似于如下所示的依赖项:

在此处输入图片说明

这是系统的一小部分的极大简化图。图中的每个客户端(“渲染”,“物理”,“运动”)都将获得一些“通用”对象,通过它们它们可以检查类型字段,如下所示:

void transform(struct Object* obj, const float mat[16])
{
    switch (obj->type)
    {
        case camera:
            // cast to camera and do something with camera fields
            break;
        case light:
            // cast to light and do something with light fields
            break;
        ...
    }
}

当然,要比这复杂得多且更复杂的代码。通常,会从这些切换案例中调用其他功能,这些功能将递归地一次又一次地进行切换。此图和代码几乎看起来像ECS-精简版,但没有强大的实体部分的区别(“ 该对象的相机?”,而不是‘没有这个对象提供运动?’),并没有‘系统’的形式化(只是一堆嵌套函数遍布各处并混淆了职责)。在这种情况下,几乎所有事情都很复杂,任何功能都有可能导致灾难等待发生。

我们这里的测试程序经常必须检查像网格物体之类的东西与其他类型的物体是否分开,即使两者都发生了相同的事情,因为这里编码的强力本质(通常伴随着大量的复制和粘贴)从一个项目类型到下一个项目类型,完全相同的逻辑很可能会失败。即使有强烈表达的用户端需求,尝试扩展系统以处理新类型的物品也几乎是没有希望的,因为当我们为处理现有类型的物品而付出很多努力时,这太困难了。

一些优点:

  • 呃...我没有任何工程经验,我猜是吗?该系统甚至不需要多态性等基本概念的知识,它完全是蛮力的,所以我想即使是初学者也可以理解一些代码,即使调试专家几乎不能维护它。

一些缺点:

  • 维护噩梦。我们的营销团队实际上感到有必要夸耀我们在一个3年的周期内修复了2000多个独特的错误。对于我来说,我们首先遇到这么多错误是件令人尴尬的事情,而且该过程可能仍然只修复了总数不断增加的错误总数的10%。
  • 关于最不灵活的解决方案。

1990年代COM体系结构

视觉特效行业的大多数人都使用我收集的这种架构风格,阅读有关其设计决策的文档并浏览其软件开发套件。

在ABI级别上,它可能不完全是COM(这些体系结构中的某些只能使用相同的编译器编写插件),但是与对对象进行接口查询以查看其组件支持的接口具有很多相似的特性。

在此处输入图片说明

通过这种方法,transform上面的类比功能变得类似于这种形式:

void transform(Object obj, const Matrix& mat)
{
    // Wrapper that performs an interface query to see if the 
    // object implements the IMotion interface.
    MotionRef motion(obj);

    // If the object supported the IMotion interface:
    if (motion.valid())
    {
        // Transform the item through the IMotion interface.
        motion->transform(mat);
        ...
    }
}

这是旧代码库的新团队所采用的方法,最终得以重构。在灵活性和可维护性方面,这是对原始版本的显着改进,但是在下一节中,我仍然会涉及一些问题。

一些优点:

  • 与以前的暴力解决方案相比,它具有更大的灵活性/可扩展性/可维护性。
  • 通过使每个接口完全抽象(无状态,无实现,仅纯接口)来促进与SOLID的许多原则的高度一致性。

一些缺点:

  • 很多样板。我们的组件必须通过注册表发布才能实例化对象,它们所支持的接口既需要继承(在Java中为“实现”)接口,又需要提供一些代码以指示查询中可以使用哪些接口。
  • 纯接口的结果促进了各地的重复逻辑。例如,对于所有功能,实现的所有组件IMotion将始终具有完全相同的状态和完全相同的实现。为了减轻这种情况,我们将开始集中整个系统中的基类和辅助功能,以便针对相同的接口以相同的方式冗余地实现这些事情,并且可能在幕后进行了多个继承,但这确实很不错。即使客户端代码很简单,也很混乱。
  • 效率低下:vtune会话通常显示基本QueryInterface功能,几乎总是显示为中上热点,有时甚至出现在#1热点上。为了减轻这种情况,我们会做一些事情,例如让代码库的渲染部分缓存已知支持的对象列表IRenderable,但这极大地提高了复杂性和维护成本。同样,这更难以衡量,但是与之前每个接口都需要动态分配的C风格编码相比,我们注意到了一定的放慢速度。诸如分支错误预测和优化障碍之类的事情很难在代码的一小部分之外进行衡量,但是用户通常只是注意到用户界面的响应能力,而诸如此类的事情则是通过并行比较旧版本和较新版本的软件而变得更糟,对于算法复杂度不变的区域,只有常数。
  • 在更广泛的系统级别上,仍然很难对正确性进行推理。尽管它比以前的方法要容易得多,但是仍然很难掌握整个系统中对象之间的复杂交互,尤其是在一些针对它的必要的优化中。
  • 我们很难正确设置接口。即使系统中可能只有一个广阔的地方使用接口,但用户端的要求仍会随着版本的变化而变化,我们最终将不得不对实现该接口的所有类进行级联更改,以适应添加到该接口的新功能。接口,例如,除非有一些抽象的基类已经将逻辑集中在幕后(其中一些会在这些级联更改的中间表现出来,希望不要一次又一次地重复执行此操作)。

在此处输入图片说明

务实回应:组成

我们之前(或者至少在我之前)注意到的引起问题的一件事IMotion可能是由100个不同的类实现,但是它们具有完全相同的实现和状态。此外,它只能由少数几个系统使用,例如渲染,关键帧运动和物理。

因此,在这种情况下,我们可能会喜欢使用接口接口的系统之间的3对1关系,以及实现接口接口的子类型之间的100对1关系。

然后,复杂性和维护将大大偏向于100个子类型的实现和维护,而不是依赖于3个客户端系统IMotion。这将我们所有的维护困难转移到了这100个子类型的维护上,而不是使用界面的3个地方。使用很少或没有“间接传出耦合”来更新代码中的3个地方(就像依赖于它,但通过接口间接地,不是直接依赖),没什么大不了的:用一堆“间接传出耦合”来更新100个子类型的地方,相当重要*。

* 我意识到从实现的角度来理解“高效耦合”的定义是很奇怪和错误的,我只是没有找到更好的方法来描述当接口和相应的一百个子类型的实现相关联的维护复杂性必须改变。

因此,我不得不努力,但我建议我们尝试变得更加实用一些,并放松整个“纯界面”的想法。对我来说,使诸如IMotion完全抽象和无状态之类的东西变得毫无意义,除非我们看到它具有多种实现方式对它有好处。在我们的案例中,IMotion拥有各种各样的实现实际上会变成维护方面的噩梦,因为我们不想多样化。取而代之的是,我们反复尝试制作一个对改变客户需求确实非常有用的单个运动实现,并且经常围绕纯接口思想工作,试图迫使每个实现者IMotion使用相同的实现和相关的状态,以便我们不这样做。重复目标。

因此,接口变得更像Behaviors与实体相关联的广泛对象。IMotion会简单地变成一个Motion“组件”(我将我们从COM定义“组件”的方式更改为更接近通常定义的组成“完整”实体的组件)。

代替这个:

class IMotion
{
public:
    virtual ~IMotion() {}
    virtual void transform(const Matrix& mat) = 0;
    ...
};

我们将其演变为以下形式:

class Motion
{
public:
    void transform(const Matrix& mat)
    {
        ...
    }
    ...

private:
    Matrix transformation;
    ...
};

这是对依赖反转原则的公然违反,它开始从抽象转向具体,但是对我来说,这样的抽象水平仅在我们能够在合理的怀疑范围内预见到未来真正的需求时才有用。为了实现这种灵活性,请使用完全脱离用户体验的荒谬的“假设情况”场景(无论如何都可能需要更改设计)。

因此,我们开始发展这种设计。QueryInterface变得更像QueryBehavior。此外,在这里使用继承似乎毫无意义。我们改用合成。对象变成了一组组件,这些组件的可用性可以在运行时查询和注入。

在此处输入图片说明

一些优点:

  • 在我们的情况下,与以前的纯接口COM样式的系统相比,维护起来容易得多。通过非常集中和明显的Motion实现,可以更轻松地解决诸如需求变更或工作流投诉之类的不可预见的意外情况,例如,这些意外情况不会分散在一百个子类型中。
  • 赋予了我们实际所需的那种全新的灵活性。在我们以前的系统中,由于继承为静态关系建模,因此我们只能在C ++编译时有效地定义新实体。我们无法从脚本语言中做到这一点,例如,采用组合方法,我们可以在运行时通过将新实体仅附加到组件并将它们添加到列表中而将它们串起来。一个“实体”变成了一块空白的画布,我们可以在上面随意地将我们需要的一切拼凑在一起,相关的系统会自动识别并处理这些实体。

一些缺点:

  • 我们在效率部门仍然很艰难,在性能至关重要的领域也很难维护。每个系统仍然最终希望缓存提供这些行为的实体的组件,以避免重复遍历所有组件并检查可用的组件。每个要求性能的系统在执行此操作时都将稍有不同,并且在某些情况下,如果无法更新此缓存列表和数据结构(如果涉及某种形式的搜索,例如平截头剔除或射线跟踪),则可能会出现不同的错误集。晦涩的场景更改事件,例如
  • 与所有这些细小的行为,简单对象有关的东西,仍然有些尴尬而复杂。我们仍然产生了很多事件来处理有时有时是必需的这些“行为”对象之间的交互,结果是代码非常分散。每个小物体都易于测试正确性,并且单独采取,通常都是完全正确的。但是,我们仍然感觉像是我们试图维持一个由小村庄组成的庞大生态系统,并试图对它们各自所做的事情进行推理,并将其汇总为一个整体。C风格的80年代代码库感觉就像是一个史诗般的,人口过多的特大城市,这绝对是一场维护噩梦,
  • 由于缺乏抽象而失去了灵活性,但是在一个我们从未真正遇到过真正需求的领域,所以几乎没有实际的缺点(尽管肯定至少是理论上的缺点)。
  • 保持ABI兼容性总是很困难,这通过要求稳定的数据而不仅仅是与“行为”相关的稳定接口而变得更加困难。但是,如果需要更改状态,我们可以轻松地添加新行为,并简单地弃用现有行为,这可以说比在子类型级别的接口下进行后空翻来处理版本控制问题要容易得多。

发生的一种现象是,由于我们失去了对这些行为组件的抽象,因此我们拥有了更多的行为。例如,IRenderable我们用一个具体的MeshPointSprites组件代替一个对象,而不是一个抽象的组件。渲染系统将知道如何渲染MeshPointSprites组件,并将找到提供此类组件的实体并进行绘制。在其他时候,我们有其他可渲染对象,就像SceneLabel事后发现我们需要的那样,因此SceneLabel在这种情况下,我们会将a附加到相关实体(可能除了之外Mesh)。然后,将更新渲染系统工具,以了解如何渲染提供实体的实体,这很容易进行更改。

在这种情况下,由组件组成的实体也可以用作另一个实体的组件。我们将通过连接乐高积木来构建事物。

ECS:系统和原始数据组件

就我个人而言,最后一个系统是我自己制作的,而我们仍在使用COM对其进行混用。感觉好像是想成为一个实体组件系统,但当时我还不熟悉它。当我本来应该从AAA游戏引擎中汲取建筑灵感时,我一直在寻找COM风格的示例,这些示例使我的研究领域更加充实。我终于开始这样做了。

我所缺少的是几个关键思想:

  1. 将“系统”形式化以处理“组件”。
  2. “组件”是原始数据,而不是将行为对象组合成一个更大的对象。
  3. 实体不过是与组件集合相关联的严格ID。

我最终离开了那家公司,开始作为印品的ECS(仍在努力工作,同时耗尽了我的积蓄),到目前为止,它是最容易管理的系统。

我使用ECS方法注意到的是,它解决了我仍然在上面挣扎的问题。对我而言最重要的是,感觉就像我们在管理大小健康的“城市”,而不是管理互动复杂的小村庄。它并不像一个庞大的整体大城市那样难以维护,人口众多而无法有效地进行管理,但它并不像一个充满小村庄的世界那样混乱,它们只是在相互思考贸易路线。他们之间形成了一个噩梦图。ECS将所有复杂性提炼为庞大的“系统”,例如渲染系统,健康大小的“城市”,而不是“人口过剩的特大城市”。

起初,成为原始数据的组件真的让我感到很奇怪,因为它甚至破坏了OOP的基本信息隐藏原理。这是我对OOP所珍视的最大价值之一,这是一种挑战,它是保持不变性的能力,而不变性要求封装和信息隐藏。但是,它变得无关紧要,因为很快就很明显,只有十几个广泛的系统正在转换数据,而不是将这样的逻辑分散在数百个成千上万个实现接口组合的子类型中,从而发生了什么。除了在系统提供访问数据的功能和实现,组件在提供数据,实体在提供组件的地方扩展之外,我倾向于以OOP样式的方式来考虑它。

当只有少数笨重的系统以大范围转换数据时,就更容易,反直觉地推断由系统引起的副作用。系统变得非常“扁平”,每个线程的调用堆栈变得比以往任何时候都更浅。我可以在监督者级别上考虑该系统,而不会遇到奇怪的惊喜。

同样,就消除那些查询而言,它甚至使性能至关重要的区域也变得简单。由于“系统”的概念变得非常形式化,因此系统可以订阅其感兴趣的组件,而只是交给满足该条件的实体的缓存列表。每个人都不必管理该缓存优化,而是将其集中到一个地方。

一些优点:

  • 似乎可以解决我职业生涯中遇到的几乎所有主要建筑问题,而不会遇到意料之外的需求时陷入设计角落。

一些缺点:

  • 有时候,我仍然很难解决这个问题,即使在游戏行业中,这也不是最成熟或最成熟的范例,人们在其中确切地争论着这意味着什么以及如何做事。这绝对不是我与之合作的前团队所能做的,前团队由与原始代码库的COM风格思维或1980年代C风格思维紧密相连的成员组成。有时候让我感到困惑的地方就像如何对组件之间的图形样式关系进行建模,但是我总能找到一种解决方案,后来我发现我只能使一个组件依赖于另一个组件,这并不是一个可怕的解决方案。组件依赖于另一组件作为父组件,并且系统将使用备忘录来避免重复进行相同的递归运动计算”,例如)
  • ABI仍然很困难,但是到目前为止,我什至敢说它比纯接口方法更容易。这是一种观念上的转变:数据稳定性成为ABI的唯一重点,而不是接口稳定性,并且在某些方面实现数据稳定性要比接口稳定性更容易(例如:没有因为仅仅需要一个新参数而更改功能的诱惑。这种事情发生在不破坏ABI的粗略系统实现中。

在此处输入图片说明

但是,使用游戏引擎中常见的组件实体系统架构创建应用程序是否合理?

因此,无论如何,我个人肯定会说“是”,我的VFX示例就是一个不错的选择。但这仍然与游戏需求十分相似。

我没有在完全远离游戏引擎关注的更偏远地区进行实践(VFX十分相似),但是在我看来,更多的地区是ECS方法的良好候选者。甚至一个GUI系统都可能适合,但是我仍然在那儿使用更多的OOP方法(例如,与Qt不同,它没有深度继承)。

这是一个尚未开发的领域,但是,只要您的实体可以由丰富的“特征”组合(以及它们所提供的特征组合可能随时发生变化)组成,并且您可以将其归纳为少数,这对我来说似乎很合适处理具有必要特征的实体的系统。

在任何情况下,如果您可能想使用多重继承或概念模拟(例如混合),仅在深度继承层次结构中产生数百个或更多连击或数百个连击,那么它成为一种非常实用的选择平面层次结构中的类,实现特定的接口组合,但是您的系统数量很少(例如数十个)。

在那些情况下,代码库的复杂性开始与系统的数量而不是类型组合的数量成比例,因为每种类型现在只是一个组成组件的实体,这些组件不过是原始数据。GUI系统自然适合这些规格,其中它们可能具有数百种可能的控件类型与其他基本类型或接口组合,但是只有少数几个系统可以处理它们(布局系统,渲染系统等)。如果GUI系统使用ECS,则当少数几个系统提供所有功能而不是具有继承的接口或基类的数百种不同的对象类型提供所有功能时,就可以更轻松地推断系统的正确性。如果GUI系统使用ECS,则小部件将没有功能,只有数据。只有少数处理窗口小部件实体的系统才具有功能。如何处理小部件的可覆盖事件超出了我的范围,但是仅根据我到目前为止的有限经验,我还没有发现无法以某种方式将这种类型的逻辑集中传输到给定系统的情况。后见之明,产生了我所期望的更加优雅的解决方案。

我很想看到它在更多领域中的应用,因为它是我的救星。如果您的设计没有以这种方式分解,那当然是不合适的,从实体聚集组件到处理这些组件的粗糙系统,但是如果它们自然地适合这种模型,那是我迄今为止遇到的最美妙的事情。


1)从用户的角度来看,您的示例VFX程序做了什么?2)您现在在做什么ECS项目?♥感谢您撰写本文!♥
小狗

1
非常详尽的解释-谢谢。关于ECS在游戏之外的适用性,我觉得我得出了许多相同的结论。就我而言,特别是复杂的GUI。它肯定觉得很奇怪,在第一次去那么反对什么,通常做(深继承层次是在UI框架尤为突出)的粮食,但它令人振奋看到别人谁发现这种方法更有效。
Danny Yaroslavski

1
谢谢您的精彩文章!对于基于组件的GUI,我建议您看一下Unity3d的UGUI。与基于继承的CocoaTouch相比,它具有极大的灵活性和可扩展性。
伊万·米尔

16

由于游戏软件的性质及其独特的特性和质量要求,因此游戏引擎的组件-实体-系统体系结构适用于游戏。例如,实体提供了一种统一的方式来处理和处理游戏中的事物,其目的和使用可能完全不同,但是需要由系统以统一的方式进行渲染,更新或序列化/反序列化。通过将组件模型合并到此体系结构中,您可以使它们保持简单的核心结构,同时根据需要以较低的代码耦合添加更多功能。有许多不同的软件系统可以从此设计的特性中受益,例如CAD应用程序,A / V编解码器,

TL; DR-设计模式仅在问题域足够适合它们强加于设计的功能和缺点时才能正常工作。


8

当然,如果问题域非常适合它。

我当前的工作涉及一个需要根据一系列运行时因素来支持多种功能的应用程序。使用基于组件的实体将所有这些功能解耦,并允许隔离性中的可扩展性和可测试性对我们来说是田园般​​的。

编辑: 我的工作涉及提供与专有硬件(在C#中)的连接。根据硬件的外形尺寸,安装的固件,客户端购买的服务级别等,我们需要为设备提供不同级别的功能。根据设备的版本,甚至具有相同接口的某些功能也具有不同的实现。

这里以前的代码库具有非常广泛的接口,其中许多未实现。一些接口具有许多瘦接口,然后将它们静态地组合成一个类。一些简单地使用string-> string字典对其进行建模。(我们有很多部门都认为他们可以做得更好)

这些都有其不足之处。宽界面是有效模拟/测试的一个痛苦。添加新功能意味着更改公共接口(以及所有现有的实现)。许多瘦接口导致代码使用起来非常丑陋,但是自从我们最终通过一个庞大的胖对象测试以来,仍然遭受了麻烦。另外,瘦接口不能很好地管理其依赖关系。字符串字典具有通常的解析和存在问题,以及性能,可读性和可维护性方面的难题。

我们现在使用的是一个非常苗条的实体,它的组件是根据运行时信息发现并组成的。依赖关系是由声明完成的,并由核心组件框架自动解决。组件本身可以单独进行测试,因为它们可以直接使用其依赖项,并且缺少依赖项的问题可以尽早发现-在一个位置而不是第一次使用依赖项。可以插入新的(或测试)组件,并且不会影响现有的代码。消费者要求实体提供组件的接口,因此我们可以相对自由地随意修改各种实现(以及如何将实现映射到运行时数据)。

对于这样的情况,其中对象及其接口的组合物可以包括常用的组件的一些(高度变化)的子集,它工作非常好。


1
假设您被允许,能否提供当前工作的更多详细信息?我很想知道CES如何使您的建筑变得田园风光。
安德鲁·德·安德拉德

是否有关于您的经历的文章,论文或博客?另外,我想了解更多技术细节:)
user1778770 2014年

@ user1778770-不公开可用,不。你有什么问题?
Telastyn 2014年

好吧,让我们从简单的事情开始,您的概念是否涵盖了整个应用程序堆栈(例如,从业务到前端)?或单个用例中只有一个层?
user1778770 2014年

@ user1778770-在我的实现中,实体/组件存在于一层中。不同的实体可能存在于不同的层中,但它们通常不是1:1的(否则,这些层将不会带来任何好处)。
Telastyn 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.