重构大数据对象

重构大型“仅状态”对象有哪些常见策略?

我正在研究一个特定的软实时决策支持系统,该系统可以对国家空域进行在线建模/模拟。 该软件消耗大量实时数据馈送,并对空域中的大量实体的“状态”进行每分钟一次的估计。 这个问题整齐地分解,直到我们达到目前最低级别的实体。

我们的数学模型估计/预测每个这些实体的过去和未来几个小时的时间线的50个参数,大约每分钟一次。 目前,这些记录被编码为具有许多字段的单个Java类(一些字段被折叠到ArrayList )。 我们的模型正在不断发展,并且各个领域之间的依赖关系还没有一成不变,因此每个实例都会在一个复杂的模型中漫游,随着它的进展积累设置。

目前我们有类似下面的东西,它使用构建器模式方法来构建记录的内容,并强制执行已知的依赖项(作为程序员错误的检查演变模式。)一旦估计完成,我们使用.build()类型方法将以下内容转换为不可变forms。

 final class OneMinuteEstimate { enum EstimateState { INFANT, HEADER, INDEPENDENT, ... }; EstimateState state = EstimateState.INFANT; // "header" stuff DateTime estimatedAtTime = null; DateTime stamp = null; EntityId id = null; // independent fields int status1 = -1; ... // dependent/complex fields... ... goes on for 40+ more fields... void setHeaderFields(...) { if (!EstimateState.INFANT.equals(state)) { throw new IllegalStateException("Must be in INFANT state to set header"); } ... } } 

一旦完成了大量这些估算,就会将它们汇总到时间轴中,分析汇总的模式/趋势。 我们已经考虑过使用嵌入式数据库,但却遇到了性能问题。 我们宁愿在数据建模方面对其进行整理,然后将部分软实时代码逐步移动到嵌入式数据存储中。

完成“时间敏感”部分后,产品将刷新为平面文件和数据库。

问题:

  • 这是一个巨大的阶级,有太多的领域。
  • 在课堂上编码的行为非常少; 它主要是数据领域的持有者。
  • 维护build()方法非常麻烦。
  • 手动维护“状态机”抽象仅仅是为了确保大量依赖建模组件正确填充数据对象,这一点感到笨拙,但随着模型的发展,它给我们带来了很多挫败感。
  • 存在大量重复,特别是当上述记录被汇总成非常相似的“汇总”时,其相当于时间序列中的滚动总和/平均值或上述结构的其他统计产品。
  • 虽然有些领域可能聚集在一起,但它们在逻辑上都是彼此“对等”,而我们所尝试的任何细分导致行为/逻辑被人为地分裂并且需要在间接中达到两个层次。

开箱即用的想法,但这是我们需要逐步发展的东西。 在其他人说出来之前,我会注意到,如果该模型的数据表示很难得到,那么我们的数学模型可能会表明我们的数学模型不够清晰。 公平的,我们正在努力,但我认为这是一个研发环境的副作用,有很多贡献者,还有很多并发的假设在起作用。

(这不重要,但这是用Java实现的。我们使用HSQLDB或Postgres作为输出产品。我们不使用任何持久性框架,部分原因是缺乏熟悉性,部分原因是我们只有数据库有足够的性能问题单独的和手工编码的存储例程……我们对进一步的抽象持怀疑态度。)

我遇到了很多同样的问题。

至少我想我做过,听起来像我做的那样。 表示方式不同,但在10,000英尺处,声音几乎相同。 离散的,“任意的”变量和它们之间的一系列临时关系(基本上是业务驱动的)的大量负载,可能会在瞬间发生变化。

您还有另一个问题,您可以提及,这就是性能要求。 听起来更快更好,并且很可能一个缓慢完美的解决方案将被抛弃为快速糟糕的解决方案,仅仅因为较慢的一个无法满足基线性能要求,无论它有多好。

简单地说,我所做的是为系统设计了一个简单的特定于域的规则语言。

DSL的全部意义在于隐含地表达关系并将它们打包到模块中。

非常粗糙,人为的例子:

 D = 7 C = A + B B = A / 5 A = 10 RULE 1: IF (C < 10) ALERT "C is less than 10" RULE 2: IF (C > 5) ALERT "C is greater than 5" RULE 3: IF (D > 10) ALERT "D is greater than 10" MODULE 1: RULE 1 MODULE 2: RULE 3 MODULE 3: RULE 1, RULE 2 

首先,这不代表我的语法。

但是你可以从模块中看到它是3个简单的规则。

关键是,由此可见,规则1取决于C,取决于A和B,B取决于A.这些关系是隐含的。

因此,对于该模块,所有这些依赖关系“随之而来”。 你可以看看我是否为模块1生成了它可能看起来像这样的代码:

 public void module_1() { int a = 10; int b = a / 5; int c = a + b; if (c < 10) { alert("C is less than 10"); } } 

如果我创建了第2单元,那么我将得到的是:

 public void module_2() { int d = 7; if (d > 10) { alert("D is greater than 10."); } } 

在第3单元中,您会看到“免费”重用:

 public void module_3() { int a = 10; int b = a / 5; int c = a + b; if (c < 10) { alert("C is less than 10"); } if (c > 5) { alert("C is greater than 5"); } } 

因此,即使我有一个“汤”规则,模块根据依赖关系的基础,从而过滤掉它不关心的东西。 抓住一个模块,摇动树木并保持悬挂的状态。

我的系统使用DSL生成源代码,但您也可以轻松地创建一个迷你运行时解释器。

简单的拓扑排序为我处理了依赖图。

因此,关于这一点的好处在于,尽管最终生成的逻辑中存在不可避免的重复,至少在模块之间,但规则库中没有任何重复。 您作为开发人员/知识工作者所维护的是规则库。

同样好的是你可以改变一个方程式,而不用担心副作用。 例如,如果我改变做C = A / 2,那么,突然,B完全退出。 但IF(C <10)的规则根本没有变化。

使用一些简单的工具,您可以显示整个依赖图,您可以找到孤立的变量(如B)等。

通过生成源代码,它将以您想要的速度运行。

在我的例子中,有趣的是看到规则删除单个变量并且看到500行源代码从结果模块中消失。 这是500行,我不需要手动爬行并在维护和开发过程中删除。 我所要做的就是在规则库中更改一条规则,让“魔法”发生。

我甚至可以做一些简单的窥视孔优化并消除变量。

这并不难。 您的规则语言可以是XML,也可以是简单的表达式解析器。 如果你不想,没有理由去Yacc或ANTLR。 我会为S-Expressions插入一个插件,不需要语法,脑死解析。

实际上,电子表格也是一个很好的输入工具。 只要严格格式化。 在SVN中合并很糟糕(所以,不要这样做),但最终用户喜欢它。

您可能能够使用基于规则的实际系统。 我的系统在运行时并不是动态的,并且不需要复杂的目标搜索和推理,因此我不需要这种系统的开销。 但如果一个人开箱即用,那么快乐的一天。

哦,对于一个实现说明,对于那些不相信你可以在Java方法中达到64K代码限制的人,我可以向你保证可以做到:)。

拆分大型数据对象与规范化大型关系表(第一和第二范式)非常相似。 遵循规则至少达到第二范式,您可能会对原始类进行良好的分解。

从具有软实时性能约束(有时是怪物脂肪类)的R&D工作经验来看,我建议不要使用OR映射器。 在这种情况下,您最好处理“触摸金属”并直接使用JDBC结果集。 这是我对具有软实时约束和每个数据包的大量数据项的应用程序的建议。 更重要的是,如果需要持久化的不同类(不是类实例,但类定义)的数量很大, 并且您的规范中也有内存约束 ,那么您还需要避免使用像Hibernate这样的ORM。

回到你原来的问题:

你似乎有一个典型的问题:1)将多个数据项映射到OO模型中; 2)这样的多个数据项没有表现出良好的分组或隔离方式(并且任何分组尝试都往往不会感觉正确。 )有时领域模型不适合这种聚合,并且提出一种人为的方式通常最终导致不满足所有设计要求和愿望的妥协。

更糟糕的是,OO模型通常需要/期望您将类中的所有项目作为类的字段。 这样的类通常没有行为,所以它只是一个类似struct的构造,即data envelopedata shuttle

但是这种情况会产生以下问题:

您的应用程序是否需要一次读/写所有40,50多个数据项? * 必须始终存在所有数据项吗? *

我不知道您的问题域的具体细节,但总的来说,我发现我们很少需要一次处理所有数据项。 这是关系模型闪耀的地方,因为您不必一次查询表中的所有行。 您只需将所需的内容作为相关表格/视图的投影

在我们有大量数据项的情况下, 平均而言,通过网络传递的数据项的数量小于最大值,您最好使用属性模式。

而不是定义包含所有项目的怪物信封类:

 // java pseudocode class envelope { field1, field2, field3... field_n; ... setFields(m1,m2,m3,...m_n){field1=m1; .... }; ... } 

定义字典(例如基于地图):

 // java pseudocode public enum EnvelopeField {field1, field2, field3,... field_n); interface Envelope //package visible { // typical map-based read fields. Object get(EnvelopeField field); boolean isEmpty(); // new methods similar to existing ones in java.lang.Map, but // more semantically aligned with envelopes and fields. Iterator fields(); boolean hasField(EnvelopeField field); } // a "marker" interface // code that only needs to read envelopes must operate on // these interfaces. public interface ReadOnlyEnvelope extends Envelope {} // the read-write version of envelope, notice that // it inherits from Envelope, but not from ReadOnlyEnvelope. // this is done to make it difficult (but not impossible // unfortunately) to "cast-up" a read only envelope into a // mutable one. public interface MutableEnvelope extends Envelope { Object put(EnvelopeField field); // to "cast-down" or "narrow" into a read only version type that // cannot directly be "cast-up" back into a mutable. ReadOnlyEnvelope readOnly(); } // the standard interface for map-based envelopes. public interface MapBasedEnvelope extends Map MutableEnvelope { } // package visible, not public class EnvelopeImpl extends HashMap implements MapBasedEnvelope, ReadOnlyEnvelope { // get, put, isEmpty are automatically inherited from HashMap ... public Iterator fields(){ return this.keySet().iterator(); } public boolean hasField(EnvelopeField field){ return this.containsKey(field); } // the typecast is redundant, but it makes the intention obvious in code. public ReadOnlyEnvelope readOnly(){ return (ReadOnlyEnvelope)this; } } public class final EnvelopeFactory { static public MapBasedEnvelope new(){ return new EnvelopeImpl(); } } 

无需设置read-only内部标志。 您需要做的就是将信封实例向下转换为Envelope实例(仅提供getter)。

期望读取的代码应该在只读信封上运行,并且期望更改字段的代码应该在可变信封上运行。 实际实例的创建将在工厂中划分。

也就是说,您使用编译器通过建立一些代码约定来强制执行只读(或允许事物变得可变),规则管理在哪里以及如何使用哪些接口。

您可以将代码分层为需要与仅需要读取的代码分开编写的部分。 完成后,简单的代码审查(甚至grep)可以识别使用错误接口的代码。)

问题:

非公共父接口:

Envelope未被声明为公共接口,以防止错误/恶意代码将只读信封转换为基本信封,然后再返回可变信封。 预期的流程仅从可变到只读 – 它不是双向的。

这里的问题是Envelope扩展仅限于包含它的包。 这是否是一个问题将取决于特定的域和预期用途。

工厂:

问题是工厂可能(并且很可能会)非常复杂。 再一次,野兽的本质。

validation:

这种方法引入的另一个问题是,现在您必须担心期望字段X存在的代码。 拥有原始的怪物信封类可以让你免于担心,因为至少在语法上,所有领域都在那里……

……无论这些领域是否已经确定,这是我提出的这个新模型仍然存在的另一个问题。

因此,如果您有希望查看字段X的客户端代码,则客户端代码必须抛出某种类型的exception,如果该字段不存在(或者以某种方式读取计算机或读取合理的默认值。)在这种情况下,您将不得不

  1. 确定现场存在的模式。 期望字段X存在的客户端可以与期望存在其他字段的客户端分开(分开分层)。

  2. 将自定义validation器(只读信封接口的代理)关联,根据某些规则(以编程方式提供的规则,使用解释器或使用规则引擎,为丢失的字段计算exception或计算默认值)。

缺乏打字:

这可能是有争议的,但过去使用静态类型的人可能会因为使用基于地图的松散方法而失去静态类型的好处而感到不安。 反驳的是,大多数网络都采用松散的打字方式,即使是在Java方面(JSTL,EL)。

除了问题之外,可能字段的最大数量越大,并且在任何给定时间出现的平均字段数越少,这种方法的最有效性能就越好。 它增加了额外的代码复杂性,但这就是野兽的本质。

这种复杂性不会消失,并且会出现在您的class级模型或validation码中。 然而,序列化和传输线路的效率要高得多,特别是如果您期望大量的单个数据传输。

希望能帮助到你。

实际上,这似乎是游戏开发人员面临的一个常见问题,由于深度inheritance树等原因,包含大量变量和方法的臃肿类。

这篇关于如何以及为什么选择合成而不是inheritance的博客文章 ,也许会有所帮助。

您可以智能地分解大型数据类的一种方法是查看客户端类的访问模式。 例如,如果一组类仅访问字段1-20而另一组类仅访问字段25-30,则这些字段组可能属于单独的类。