我是一些家谱软件(用C++和Qt编写)的开发人员。在我的一个客户寄给我一份bug报告之前,我没有遇到任何问题。问题是客户有两个孩子和他们自己的女儿,因此,由于错误,他不能使用我的软件。
这些错误是我对正在处理的家族图的各种断言和不变量的结果(例如,在走了一个周期后,程序声明X不能同时是Y的父亲和祖父)。
如何在不删除所有数据断言的情况下解决这些错误?
撇开潜在的法律影响不谈,你肯定需要将家谱上的“节点”视为前任,而不是假设节点可以是唯一的人。
让树节点包括一个人和继任者——然后你可以在树的深处有另一个节点,包括同一个人和不同的继任者。
我猜你有一些价值,可以唯一地识别一个人,你可以以此为基础检查。
这是一个棘手的问题。假设你想让结构保持一棵树,我建议这样做:
假设:A和自己的女儿有孩子。
A
A将自己作为A和B添加到程序中。一旦扮演了父亲的角色,让我们称之为男朋友。
B
添加一个is_same_for_out()函数,它告诉程序的输出生成部分,在数据呈现时,内部转到B的所有链接都应该转到A。
is_same_for_out()
这将为用户带来一些额外的工作,但我想IT将相对容易实现和维护。
在此基础上,您可以处理代码同步A和B以避免不一致。
这个解决方案当然不是完美的,但这是第一个方法。
我讨厌评论这样一个一团糟的情况,但不重新调整所有不变量的最简单方法是在你的图中创建一个幻影顶点,作为一个代理回到乱伦的父亲。
放松你的断言。
不是通过改变规则,这可能对99.9%的客户在输入数据时发现错误非常有帮助。
相反,将其从错误“无法添加关系”更改为带有“无论如何添加”的警告。
这就是为什么像“Go”这样的语言没有断言的原因之一。它们被用来处理你可能经常没有想到的情况。你应该只坚持不可能的事情,而不仅仅是不可能的事情。做后者会给断言带来坏名声。每次你输入assert(,走开十分钟,真的考虑一下。
assert(
在你特别令人不安的情况下,这种断言在罕见但可能的情况下是虚假的,这既是可以想象的,也是令人震惊的。因此,在你的应用程序中处理它,即使只是说“这个软件不是为了处理你提出的场景而设计的”。
断言你的曾曾曾祖父不可能成为你的父亲是一件合理的事情。
如果我在一家测试公司工作,这家公司被雇来测试你的软件,我当然会提出这个场景。为什么?每个年轻而聪明的“用户”都会做完全一样的东西,并喜欢由此产生的“bug报告”。
看来你(和/或你的公司)对家谱应该是什么有一个根本性的误解。
让我澄清一下,我也为一家公司工作,该公司(作为其产品之一)的产品组合中有家谱,我们一直在努力解决类似的问题。
在我们的例子中,我假设你的例子也是如此,问题来自于GEDCOM格式,它对家庭应该是什么非常固执己见。然而,这种格式包含了一些关于家谱真正样子的严重误解。
GEDCOM有许多问题,例如与同性关系不相容,乱伦等……在现实生活中发生的事情比你想象的要多(尤其是回到1700-1800年)。
我们已经根据现实世界中发生的事情建立了家谱模型:事件(例如,出生、婚礼、订婚、工会、死亡、收养等)。除了逻辑上不可能的事情(例如,一个人不能成为自己的父母,关系需要两个人,等…),我们对这些没有任何限制。
缺乏验证为我们提供了一个更“真实”,更简单,更灵活的解决方案。
至于这个具体案例,我建议删除这些断言,因为它们并不普遍适用。
为了显示问题(将出现),我建议根据需要多次绘制相同的节点,通过在选择其中一个时点亮所有副本来暗示重复。
你应该专注于真正为您的软件创造价值的东西。花在让它为一个消费者工作上的时间值得许可证的价格吗?可能不是。
我建议您向这位客户道歉,告诉他他的情况超出了您的软件范围,并向他退款。
家谱的问题是:它们不是树。它们是有向无环图或DAG。如果我正确理解人类繁殖的生物学原理,就不会有任何循环。
据我所知,即使是基督徒也接受表兄弟之间的婚姻(因此也有孩子),这将把家谱变成一个家庭DAG。
这个故事的寓意是:选择正确的数据结构。
你的家谱应该使用有向关系,这样你就不会有循环。
对一个愚蠢的问题的另一个模拟严肃的回答:
真正的答案是,使用适当的数据结构。人类的家谱不能完全用一棵没有循环的纯树来表达。你应该使用某种图表。此外,在进一步讨论这个问题之前,请咨询一下人类学家,因为在建立家谱模型时,还有很多其他地方可能会出现类似的错误,即使是在最简单的“西方父权制一夫一妻制婚姻”的情况下。
即使我们想忽略这里讨论的局部禁忌关系,也有很多完全合法和完全意想不到的方法将循环引入家谱。
例如:http://en.wikipedia.org/wiki/Cousin_marriage
基本上,表亲婚姻不仅是普遍和预期的,这是人类从成千上万的小家庭群体发展到全世界60亿人口的原因。
在家谱、家庭和血统方面,真的很少有共通性。几乎任何关于规范的严格假设都表明谁可以是阿姨,谁可以嫁给谁,或者为了继承的目的,孩子是如何合法化的,都可能被世界或历史上的某个例外所打破。
您应该将Atreides系列(无论是现代的、沙丘还是古代的、俄狄浦斯·雷克斯)设置为测试用例。您不会通过使用经过消毒的数据作为测试用例来发现错误。
所以,我做了一些家谱软件的工作。我认为你试图解决的问题是你需要能够在树中行走而不会陷入无限循环——换句话说,树需要是非循环的。
然而,看起来你是在断言一个人和他们的祖先之间只有一条路。这将保证没有循环,但太严格了。从生物学上讲,后代是一个有向无圈图(DAG)。你的情况当然是一个退化的情况,但这种类型的事情总是发生在更大的树上。
例如,如果你看看你在第n代的2^n个祖先,如果没有重叠,那么你在公元1000年的祖先比活着的人还多。所以,一定有重叠。
然而,你也倾向于得到无效的循环,只是坏数据。如果你正在遍历树,那么必须处理循环。你可以在每个单独的算法中或在加载时这样做。我在加载时做了。
在一棵树中找到真正的循环可以通过几种方法来完成。错误的方法是标记来自给定个体的每个祖先,并且在遍历时,如果你要下一步的人已经被标记,则切断链接。这将切断潜在的准确关系。正确的方法是从每个个体开始,并用指向该个体的路径标记每个祖先。如果新路径包含当前路径作为子路径,那么它是一个循环,应该被打破。你可以将路径存储为向量(MFMF、MFFFMF等),这使得比较和存储非常快。
还有一些其他方法可以检测循环,例如发送两个迭代器并查看它们是否与子集测试发生冲突,但我最终使用了本地存储方法。
另请注意,你不需要实际切断链接,你可以将其从普通链接更改为“弱”链接,这不会被你的一些算法所遵循。在选择将哪个链接标记为弱链接时,你也需要小心;有时你可以通过查看出生日期信息来确定应该在哪里打破循环,但通常你无法弄清楚任何事情,因为缺少太多数据。
一些答案已经展示了保持断言/不变量的方法,但这似乎是对断言/不变量的误用。断言是为了确保应该是真的东西是真的,不变量是为了确保不应该改变的东西不会改变。
你在这里断言的是乱伦关系不存在。显然它们做存在,所以你的断言是无效的。你可以解决这个断言,但真正的bug在断言本身。断言应该被删除。
最重要的是avoid creating a problem,所以我相信你应该使用直接关系来避免循环。
avoid creating a problem
正如@mark myWords所说,#包含“fritzl. h”。
最后我不得不说recheck your data structure。也许那边出了问题(也许双向链表解决了你的问题)。
recheck your data structure
不要删除所有断言,你仍然应该检查一个人是他/她自己的父母或其他不可能的情况并显示错误。如果不太可能,也许会发出警告,这样用户仍然可以检测到常见的输入错误,但如果一切正确,它将起作用。
我会将数据存储在一个向量中,每个人都有一个永久整数,并将父母和孩子存储在人对象中,其中所述int是向量的索引。这在代际之间会非常快(但对于名称搜索之类的事情来说会很慢)。对象将按创建时间的顺序排列。
复制父亲(或使用符号链接/引用)。
例如,如果您使用分层数据库:
$ #each person node has two nodes representing its parents.$ mkdir Family$ mkdir Family/Son$ mkdir Family/Son/Daughter$ mkdir Family/Son/Father$ mkdir Family/Son/Daughter/Father$ ln -s Family/Son/Daughter/Father Family/Son/Father$ mkdir Family/Son/Daughter/Wife$ tree FamilyFamily└── Son├── Daughter│ ├── Father│ └── Wife└── Father -> Family/Son/Daughter/Father 4 directories, 1 file
系谱数据是循环的,不适合非循环图,所以如果你有针对循环的断言,你应该删除它们。
在不创建自定义视图的情况下,在视图中处理此问题的方法是将循环父节点视为“幽灵”父节点。换句话说,当一个人既是同一个人的父亲又是祖父时,则祖父节点会正常显示,但父亲节点会呈现为“幽灵”节点,该节点有一个简单的标签,如(“见祖父”)并指向祖父。
为了进行计算,您可能需要改进处理循环图的逻辑,以便在有循环的情况下不会多次访问节点。
通常断言无法在与真实世界数据的接触中存活下来。决定要处理哪些数据以及哪些数据超出范围是软件工程过程的一部分。
关于家族“树”(实际上它是完整的图,包括循环),有一个很好的轶事:
我娶了一个寡妇,她有一个成年的女儿。我父亲经常来看我们,他爱上了我的继女,并娶了她。结果,我父亲成了我的儿子,我女儿成了我的母亲。一段时间后,我给我妻子生了一个儿子,他是我父亲的兄弟,也是我的叔叔。我父亲的妻子(也是我的女儿和我的母亲)生了一个儿子。结果,我同一个人有了一个兄弟和一个孙子。我的妻子现在是我的祖母,因为她是我母亲的母亲。所以我是我妻子的丈夫,同时也是我妻子的继孙子。换句话说,我是自己的爷爷。
事情变得更加奇怪,当你考虑到代理人或“模糊的父亲身份”时。
您可以决定您的软件不应该处理这种罕见的情况。如果发生这种情况,用户应该使用不同的产品。这使得处理更常见的情况更加健壮,因为您可以保留更多的断言和更简单的数据模型。
在这种情况下,为您的软件添加一些良好的导入和导出功能,以便用户可以在必要时轻松迁移到不同的产品。
你可以允许用户添加手动关系。这些关系不是“一等公民”,即软件按原样处理它们,不检查它们,也不在主数据模型中处理它们。
然后用户可以手动处理罕见的情况。您的数据模型仍然非常简单,您的断言将继续存在。
小心手动关系。有一种诱惑使它们完全可配置,从而创建一个完全可配置的数据模型。这是行不通的:你的软件将无法扩展,你会得到奇怪的错误,最终用户交互界面将变得不可用。这种反模式称为“软编码”,“每日WTF”充满了这方面的例子。
最后的手段是让你的数据模型更加灵活。你必须跳过几乎所有的断言,并将你的数据模型建立在一个完整的图表上。正如上面的例子所示,很容易成为你自己的祖父,所以你甚至可以有循环。
在这种情况下,您应该广泛测试您的软件。您必须跳过几乎所有的断言,因此很有可能出现其他错误。
使用测试数据生成器来检查不寻常的测试用例。有haskell、Erlang或C的快速检查库。对于Java/Scala有ScalaCheck和Nyaya。一个测试想法是模拟一个随机群体,让它随机杂交,然后让你的软件先导入然后导出结果。期望是,输出中的所有连接也在输入中,反之亦然。
一个属性保持不变的情况称为不变量。在这种情况下,不变量是模拟人群中个体之间的“浪漫关系”的集合。尝试找到尽可能多的不变量,并使用随机生成的数据测试它们。不变量可以是泛函的,例如:
它可以是技术性的:
通过运行模拟测试,你会发现很多奇怪的角落情况。修复它们需要很多时间。此外,你会失去很多优化,你的软件运行速度会慢得多。你必须决定,这是否值得,以及这是否在你的软件范围内。