重构大型数据对象

2022-09-04 03:35:55

重构大型“仅状态”对象的一些常见策略是什么?

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

我们的数学模型估计/预测超过50个参数,每个实体的过去和未来几个小时的时间线,大约每分钟一次。目前,这些记录被编码为具有大量字段的单个Java类(有些被折叠成一个)。我们的模型正在不断发展,字段之间的依赖关系尚未一成不变,因此每个实例都在一个复杂的模型中徘徊,并在此过程中累积设置。ArrayList

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

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 作为输出产品。我们不使用任何持久性框架,部分原因是缺乏熟悉,部分原因是我们仅使用数据库和手动编码的存储例程就有足够的性能问题......我们对转向额外的抽象持怀疑态度。


答案 1

我遇到了和你一样多的问题。

至少我认为我做到了,听起来像我做到了。表示是不同的,但在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,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-表达式添加一个插件,不需要语法,脑死亡解析。

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

您很可能能够摆脱实际的基于规则的系统。我的系统在运行时不是动态的,也不需要复杂的目标搜索和推理,所以我不需要这样一个系统的开销。但是,如果一个开箱即用,那么快乐的一天。

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


答案 2

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