3

我试图想出一个命名约定来准确地传达我正在设计的课程中发生的事情。在第二点上,我试图在两个几乎等效的用户 API 之间做出决定。

情况如下:

我正在构建一个科学应用程序,其中一个中央数据结构具有三个阶段:1)积累、2)分析和 3)查询执行。

在我的例子中,它是一种空间建模结构,在内部使用 KDTree 来划分 3 维空间中的点集合。每个点都描述了周围环境的一个或多个属性,对测量本身具有一定的置信度。

在向集合添加(可能大量)测量后,对象的所有者将查询它以获取适用字段内某处新数据点处的插值测量。

API 看起来像这样(代码是用 Java 编写的,但这并不重要;为清楚起见,代码分为三个部分):

// SECTION 1:
// Create the aggregation object, and get the zillion objects to insert...
ContinuousScalarField field = new ContinuousScalarField();
Collection<Measurement> measurements = getMeasurementsFromSomewhere();

// SECTION 2:
// Add all of the zillion objects to the aggregation object...
// Each measurement contains its xyz location, the quantity being measured,
// and a numeric value for the measurement. For example, something like
// "68 degrees F, plus or minus 0.5, at point 1.23, 2.34, 3.45"
foreach (Measurement m : measurements) {
   field.add(m);
}

// SECTION 3:
// Now the user wants to ask the model questions about the interpolated
// state of the model. For example, "what's the interpolated temperature
// at point (3, 4, 5)
Point3d p = new Point3d(3, 4, 5);
Measurement result = field.interpolateAt(p);

对于我的特定问题域,可以在第 2 节期间执行少量增量工作(将点划分为平衡的 KDTree)。

在第 3 节期间可能会发生少量工作(执行一些线性插值)。

但是在第 2 节和第 3 节之间必须执行大量工作(构建核密度估计器并执行快速高斯变换,使用泰勒级数和 Hermite 函数,但这完全是题外话) 。

有时在过去,我只是使用惰性求值来构造数据结构(在这种情况下,它会在第一次调用“interpolateAt”方法时),但是如果用户调用“field.add ()" 方法,我必须完全丢弃那些数据结构并从头开始。

在其他项目中,我要求用户显式调用“object.flip()”方法,从“附加模式”切换到“查询模式”。这样的设计的好处是用户可以更好地控制核心计算开始的确切时刻。但是对于 API 使用者来说,跟踪对象的当前模式可能会很麻烦。此外,在标准用例中,调用者在开始发出查询后永远不会向集合添加另一个值;数据聚合几乎总是完全在查询准备之前。

你们是如何设计这样的数据结构的?

您是否更喜欢让对象懒惰地执行其繁重的分析,当新数据进入集合时丢弃中间数据结构?或者您是否需要程序员显式地将数据结构从附加模式转换为查询模式?

你知道像这样的对象有什么命名约定吗?有没有我没有想到的模式?


编辑:

我在示例中使用的名为“ContinuousScalarField”的类似乎有些困惑和好奇。

通过阅读这些维基百科页面,您可以很好地了解我在说什么:

假设您想创建一个地形图(这不是我的确切问题,但在概念上非常相似)。因此,您在一平方英里的区域内进行了一千次高度测量,但您的测量设备在高度上的误差范围为正负 10 米。

一旦你收集了所有的数据点,你就可以将它们输入一个模型,该模型不仅可以插值,还可以考虑每次测量的误差。

要绘制地形图,您需要在模型中查询要绘制像素的每个点的高程。

至于单个类是否应该同时负责追加和处理查询的问题,我不是 100% 肯定,但我认为是的。

这是一个类似的示例:HashMap 和 TreeMap 类允许添加和查询对象。没有用于添加和查询的单独接口。

这两个类也与我的示例相似,因为必须持续维护内部数据结构以支持查询机制。HashMap 类必须定期分配新内存,重新散列所有对象,并将对象从旧内存移动到新内存。TreeMap 必须使用红黑树数据结构持续保持树平衡。

唯一的区别是,如果我的班级一旦知道数据集已关闭,它就可以执行所有计算,那么它将表现最佳。

4

6 回答 6

4

如果一个对象有两种这样的模式,我建议向客户端公开两个接口。如果对象处于附加模式,则确保客户端只能使用 IAppendable 实现。要切换到查询模式,请向 IAppendable 添加一个方法,例如 AsQueryable。要返回,请调用 IQueryable.AsAppendable。

您可以在同一个对象上实现 IAppendable 和 IQueryable,并在内部以相同的方式跟踪状态,但是有两个接口可以让客户端清楚地知道对象处于什么状态,并强制客户端故意使(昂贵) 转变。

于 2008-10-29T18:35:16.657 回答
2

你的对象应该有一个角色和责任。在您的情况下,ContinuousScalarField 应该负责插值吗?

也许你最好做一些类似的事情:

IInterpolator interpolator = field.GetInterpolator();
Measurement measurement = Interpolator.InterpolateAt(...);

我希望这是有道理的,但是如果不完全了解您的问题域,就很难给您一个更连贯的答案。

于 2008-10-29T18:31:15.713 回答
2

我通常更喜欢进行显式更改,而不是懒惰地重新计算结果。这种方法使实用程序的性能更加可预测,并且减少了我为提供良好的用户体验而必须做的工作量。例如,如果这发生在 UI 中,我在哪里需要担心弹出沙漏等?哪些操作会阻塞一段时间,并且需要在后台线程中执行?

也就是说,与其显式更改一个实例的状态,我建议使用Builder 模式来生成一个新对象。例如,您可能有一个聚合器对象,它在您添加每个样本时执行少量工作。void flip()然后,我将使用一种Interpolator interpolator()方法来获取当前聚合的副本并执行所有繁重的数学运算,而不是您提出的方法。您的interpolateAt方法将在这个新的 Interpolator 对象上。

如果您的使用模式允许,您可以通过保留对您创建的插值器的引用来进行简单的缓存,并将其返回给多个调用者,仅在修改聚合器时清除它。

这种职责分离有助于产生更多可维护和可重用的面向对象程序。Measurement可以在请求时返回 a 的对象Point是非常抽象的,也许很多客户可以使用您的 Interpolator 作为实现更通用接口的一种策略。


我认为您添加的类比具有误导性。考虑一个替代类比:

Key[] data = new Key[...];
data[idx++] = new Key(...); /* Fast! */
...
Arrays.sort(data); /* Slow! */
...
boolean contains = Arrays.binarySearch(data, datum) >= 0; /* Fast! */

这可以像一个集合一样工作,实际上,它比Set实现(使用哈希表或平衡树实现)提供更好的性能。

平衡树可以看作是插入排序的一种有效实现。每次插入后,树处于排序状态。平衡树的可预测时间要求是由于排序成本分布在每个插入上,而不是发生在某些查询上而不是其他查询上。

哈希表的重新散列确实会导致性能不一致,因此不适合某些应用程序(可能是实时微控制器)。但即使是重新散列操作也只取决于表的负载因子,而不是插入和查询操作的模式。

为了使您的类比严格成立,您必须对添加的每个点“排序”(做毛茸茸的数学)您的聚合器。但这听起来成本太高了,这导致了构建器或工厂方法模式。这让您的客户清楚地知道他们何时需要为冗长的“排序”操作做好准备。

于 2008-10-29T18:56:05.367 回答
1

“我刚刚使用惰性求值来构造数据结构”——很好

“如果用户再次调用“field.add()”方法,我必须完全丢弃那些数据结构并从头开始。” ——有趣

“在标准用例中,调用者在开始发出查询后永远不会向集合添加另一个值”——哎呀,误报,实际上并不有趣

由于惰性评估适合您的用例,请坚持使用。这是一个非常常用的模型,因为它非常可靠并且非常适合大多数用例。

重新考虑这一点的唯一原因是 (a) 用例更改(混合添加和插值),或 (b) 性能优化。

由于不太可能更改用例,因此您可能会考虑分解插值对性能的影响。例如,在空闲时间,您可以预先计算一些值吗?或者每次添加都有一个可以更新的摘要?

此外,一个高度有状态(并且不是很有意义)的flip方法对您的类的客户不是那么有用。但是,将插值分成两部分可能仍然对他们有所帮助——并帮助您进行优化和状态管理。

例如,您可以将插值分解为两种方法。

public void interpolateAt( Point3d p );
public Measurement interpolatedMasurement();

这借用了关系数据库 Open and Fetch 范式。打开游标可以做很多前期工作,可能会开始执行查询,你不知道。获取第一行可能会完成所有工作,或者执行准备好的查询,或者只是获取第一缓冲行。你真的不知道。你只知道这是一个两部分的操作。RDBMS 开发人员可以自由地进行他们认为合适的优化。

于 2008-10-29T18:52:33.843 回答
0

您是否更喜欢让对象懒惰地执行其繁重的分析,当新数据进入集合时丢弃中间数据结构?或者您是否需要程序员显式地将数据结构从附加模式转换为查询模式?

我更喜欢使用允许我通过每次添加“多做一点工作”来增量添加数据的数据结构,并通过每次提取“多做一点工作”来增量提取我需要的数据。

也许如果你在你所在区域的右上角进行一些“interpolate_at()”调用,你只需要对右上角的点进行计算,离开其他 3 个象限并没有什么坏处对新添加的内容“开放”。(等等递归KDTree)。

唉,这并不总是可能的——有时添加更多数据的唯一方法是丢弃所有以前的中间和最终结果,并从头开始重新计算所有内容。

使用我设计的界面的人——尤其是我——是人,容易犯错误。所以我不喜欢使用那些人们必须记住以某种方式做事的对象,否则就会出错——因为我总是忘记那些事情。

如果一个对象在从中获取数据之前必须处于“计算后状态”,即在 interpolateAt() 函数获取有效数据之前必须运行一些“do_calculations()”函数,我更喜欢让 interpolateAt() 函数检查它是否已经处于该状态,运行“do_calculations()”并在必要时更新对象的状态,然后返回我预期的结果。

有时我听到人们将这样的数据结构描述为“冻结”数据或“结晶”数据或“编译”或“将数据放入不可变的数据结构”。一个示例是将(可变)StringBuilder 或 StringBuffer 转换为(不可变)String。

我可以想象,对于某些类型的分析,您希望提前获得所有数据,并且在所有数据放入之前提取一些插值会得到错误的结果。在这种情况下,我更愿意进行设置,以使“add_data()”函数在任何 interpolateAt() 调用之后(错误地)被调用时失败或抛出异常。

我会考虑定义一个延迟评估的“interpolated_point”对象,它不会立即真正评估数据,而只会告诉该程序将来某个时候需要该点的数据。集合实际上并没有被冻结,所以可以继续向它添加更多数据,直到某个点实际从某个“interpolated_point”对象中提取第一个实际值,该对象在内部触发“do_calculations()”函数并冻结目的。如果您不仅提前知道所有数据,还知道所有需要插值的点,它可能会加快速度。然后你可以扔掉离插值点“远”的数据,只在“近”区域做繁重的计算

对于其他类型的分析,您可以尽可能地利用现有数据,但是当以后有更多数据出现时,您希望在以后的分析中使用这些新数据。如果这样做的唯一方法是丢弃所有中间结果并从头开始重新计算所有内容,那么这就是您必须做的。(最好是对象自动处理这个问题,而不是要求人们记住每次都调用一些“clear_cache()”和“do_calculations()”函数)。

于 2012-11-21T17:07:01.787 回答
-1

你可以有一个状态变量。有一种启动高级处理的方法,该方法仅在状态位于 SECTION-1 时才有效。它将状态设置为 SECTION-2,然后在完成计算后设置为 SECTION-3。如果程序请求插入给定点,它将检查状态是否为 SECTION-3。如果不是,它将请求开始计算,然后插入给定的数据。

通过这种方式,您可以同时完成这两个任务 - 程序将在第一个插入点的请求时执行其计算,但也可以请求更早地执行此操作。如果您想在一夜之间运行计算,这将很方便,例如,不需要请求插值。

于 2008-10-29T18:35:52.100 回答