可翻转数据结构的模式名称?

时间:2008-10-29 18:23:35

标签: algorithm language-agnostic design-patterns data-structures

我正在尝试一种命名约定,准确地传达我正在设计的类中正在发生的事情。在次要说明中,我试图在两个几乎相当的用户API之间做出决定。

情况如下:

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

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

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

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节中可能会有少量工作(执行一些线性插值)。

但是有大量的工作(使用泰勒级数和Hermite函数构建核密度估计和执行快速高斯变换,但这完全不是重点)必须在之间执行2和3。

有时在过去,我刚刚使用了lazy-evaluation来构造数据结构(在这种情况下,它是在“interpolateAt”方法的第一次调用中),但是如果用户调用“ field.add()“方法再次,我必须完全丢弃这些数据结构并从头开始。

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

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

您是否希望让对象懒惰地执行其重要分析,在新数据进入集合时丢弃中间数据结构?或者您是否要求程序员明确地将数据结构从追加模式转换为查询模式?

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


ON EDIT:

我在我的例子中使用的类似乎有一些混乱和好奇心,名为“ContinuousScalarField”。

通过阅读这些维基百科页面,您可以很好地了解我所说的内容:

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

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

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

关于单个班级是否应该对追加和处理查询负责的问题,我不是百分百肯定,但我想是的。

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

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

唯一的区别是,如果我的类能够在知道数据集关闭后执行所有计算,那么它将以最佳方式执行。

6 个答案:

答案 0 :(得分:4)

如果一个对象有两个这样的模式,我建议将两个接口暴露给客户端。如果对象处于追加模式,则确保客户端只能使用IAppendable实现。要转换到查询模式,可以向IAppendable添加方法,例如AsQueryable。要回头,请调用IQueryable.AsAppendable。

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

答案 1 :(得分:2)

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

也许你最好做一些事情:

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

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

答案 2 :(得分:2)

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

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

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

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


我认为您添加的类比具有误导性。考虑另一种类比:

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

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

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

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

对于你严格控制的类比,你必须在你添加的每个点上“排序”(做毛茸茸的数学运算)你的聚合器。但这听起来似乎成本过高,这导致了构建器或工厂方法模式。这使得客户在需要为冗长的“排序”操作做好准备时会清楚。

答案 3 :(得分:1)

“我刚刚使用延迟评估来构建数据结构” - Good

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

“在标准用例中,调用者在开始发出查询后从不向集合添加其他值” - 哎呀,误报,实际上没有意思

由于lazy eval适合您的使用案例,请坚持使用它。这是一个非常使用的模型,因为它非常可靠,非常适合大多数用例。

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

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

此外,一个非常有状态(而且没有意义的)flip方法对您班级的客户来说并不那么有用。但是,将插值分为两部分可能仍然对他们有所帮助 - 并帮助您进行优化和状态管理。

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

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

这借用了关系数据库Open和Fetch范例。打开游标可以做很多初步的工作,并且可能会开始执行查询,你不知道。获取第一行可以完成所有工作,或执行准备好的查询,或者只是获取第一个缓冲行。你真的不知道。你只知道这是一个两部分的操作。 RDBMS开发人员可以根据需要自由优化。

答案 4 :(得分: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()”函数。

答案 5 :(得分:-1)

你可以有一个状态变量。有一个启动高级处理的方法,只有当STATE在SECTION-1中时才能工作。它将状态设置为SECTION-2,然后在完成计算时设置为SECTION-3。如果要求程序插入给定点,它将检查状态是否为SECTION-3。如果没有,它将请求计算开始,然后插入给定的数据。

通过这种方式,您可以完成两者 - 程序将在第一次插入点的请求时执行计算,但也可以提前请求执行此计算。如果你想在一夜之间运行计算,这将是很方便的,例如,无需请求插值。