65.9K
CodeProject 正在变化。 阅读更多。
Home

罗密欧与朱丽叶

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (34投票s)

2011年12月3日

CPOL

11分钟阅读

viewsIcon

64595

downloadIcon

432

将关系提升为一等公民。

阅读第二部分:面向关系的编程 - 建模罗密欧与朱丽叶元模型

引言

数据库中有外键,面向对象设计中有“is a kind of”和“has a”等概念来建模关系。但这是否足以描述现实生活中关系的复杂性、阴谋和戏剧性,例如当 JSOP 和他的妻子密谋制造胡萝卜推进武器来控制草坪鹿群时?我认为不是。冒着人身受伤的风险,JSOP 的生活似乎相对平淡,与罗密欧与朱丽叶相比。让我们研究一下我朋友 Mura 提出的“编程心理学”,因为在现实世界的模型中,关系才是关键。每一个精彩的故事不仅仅是关于乔,更是关于乔与人、事、地之间关系的起伏。是时候将关系提升为一等公民了!

面向关系编程

我们将使用我最近写过的 XTree 应用程序,来定义 《罗密欧与朱丽叶》 中的各种实体及其关系。

家族

似乎有三个家族涉及其中

  • 凯普莱特
  • 蒙太古
  • 埃斯卡勒斯

所以,我们需要一个 Family 实体。

现在的问题是,Name 应该是一个属性还是一个独立的实体?我认为它应该是一个实体,因为一个名字的属性并没有被清晰地定义。例如,一个名字可以包含名字、姓氏、中间名首字母、中间名、前缀(如先生、女士等)、后缀(如“第三”)、学位(“学士、博士”等)等等。名字并非小事。

所以,我们需要一个 Name 实体,它有一个名为“name”的属性。

我们现在可以在 Family 和 Name 之间建立关系,在这种情况下是一对一(一个很好的简化视图,即拥有相同家族姓氏的人属于同一个家族)。

现在我们已经建立了两个实体之间的关系,它们的属性以后可以根据需要进行扩展。

人物

最初,我们认识了几个人物

  • 桑普森(凯普莱特家族的仆人)
  • 格雷戈里(凯普莱特家族的仆人)
  • 亚伯拉罕(蒙太古家族的仆人)
  • 巴尔萨泽(蒙太古家族的仆人)
  • 蒙太古勋爵
  • 本尼沃里奥(蒙太古勋爵的侄子)
  • 凯普莱特勋爵和夫人
  • 提伯尔特(凯普莱特夫人之子)
  • 维罗纳王子(埃斯卡勒斯家族的?)
  • 罗密欧(蒙太古之子)
  • 罗瑟琳(罗密欧爱慕的对象)
  • 帕里斯(维罗纳王子亲属)
  • 朱丽叶(凯普莱特家族的)
  • 保姆(无名,但朱丽叶的保姆)
  • 麦丘西奥(不知道他属于哪个家族,我们忽略他)
  • 官兵(试图阻止一场打斗)

所以,我们已经有很多人物卷入了各种各样的关系中

  • 配偶
  • 侄子
  • 仆人
  • 爱慕
  • 熟人
  • 亲属
  • 保姆(但保姆没有名字)

当然,还有大量的争吵和坠入爱河。这需要建模的东西太多了,让我们从人物开始。

人物有名字,他们属于某个家族,并且彼此之间存在各种关系,这最终是导致所有悲剧的原因。

我们还有几个实体需要建模:官兵和保姆,他们两人都是匿名的。

这涵盖了“保姆”和“官兵”。

贞洁,和坠入爱河

我们还有一些有趣的状态信息。例如

  • 罗密欧爱上了罗瑟琳
  • 罗瑟琳是贞洁的
  • 后来,罗密欧爱上了朱丽叶
  • 朱丽叶也爱上了罗密欧
  • 我们假设罗密欧不再爱罗瑟琳了

这里有两种状态信息。“贞洁”是一个人的状态,并且是作为属性的一个绝佳候选,因为它不是一个关系概念。然而,“坠入爱河”(或“脱离爱河”)是一种带有关系概念的状态。一个人“爱上”了某事/某人(或自己,但我们不去那里,关键是它仍然是一个关系概念)。

我们在现实生活中也会遇到这些。名字、姓氏等都是非关系属性。“成本”是另一个例子。我们将这些非关系属性强行放入独立的实体中,原因有几个

  1. 它让我们思考得更抽象
  2. 抽象提高了系统的灵活性
  3. 我们可以隐含地获得变更历史(稍后会详细介绍)

所以。贞洁。我们通过创建一个名为“Sexual Behavior”(维基百科称之为“chaste”)的实体来建模贞洁,该实体有一个名为“Behavior”的属性,该属性有一个包含可能选项的拾取列表。Person 实体与“Sexual Behavior”实体有关系。听起来有点奇怪。

那么,“坠入爱河”呢?这是一种关系状态,而且,即使一个人爱上了另一个人,也不意味着第二个人也爱上了第一个人!我们可以对此进行建模!

我可以选择添加一种新的“Person-Person”关系,类型不同,但我认为这样更清楚:人与人的关系可以有几种“类型”,一种是关系(如父子),另一种是爱情生活,如“爱上-不爱”。

正如你所见,建模决策并非易事。我想,如果这方面继续深入,我可能需要一本“最佳实践”指南。

活动

莎士比亚的戏剧通常只有两种活动:打斗和谈话。哦,还有死亡,但我们稍后会讲到。一项活动通常涉及一群人(除了死亡部分,除非是战争)。

所以,我们有一个 Action 实体,带有一个 Activity 属性,该属性有一个活动拾取列表。人物与 Actions 相关联。

哦,是的。死后,还有鬼魂。我忘了。

地点

事件发生在 意大利维罗纳。但我们也有一些描述不那么详细的地方,比如“在派对上”。与地点的一个明显关系是活动:活动发生在某个地方,即使只是在你的脑海里。人物和物体也在各种地方占据空间:他们住在某个地方,他们在某个地方工作,他们在某个地方停放。但目前,让我们专注于维罗纳和派对。

  • 这场悲剧中发生的各种活动(打斗、谈话、死亡)都发生在维罗纳。
  • 派对也在维罗纳。
  • 我们可以说,派对上人们采取的行动是“派对”。够简单了。

目前,一个地点只有一个名字,并且与一个活动相关联。

这种关系是多对一,因为某个活动可能跨越多个地点。一场追车可能是这种概念的一个例子:它始于某处,终于别处。这取决于你想追踪哪些信息。

其他关系和更正

我们还需要匿名群体与人物之间的关系,因为保姆和朱丽叶说话,官兵试图阻止打斗等等。我们不需要匿名人物实体,因为人物的名字就是“保姆”。所以,把它去掉,但我们需要一个匿名群体与活动之间的关系。想想“占领华尔街”。

哦,地点和活动也有名字。我怎么会忘了呢?

Graphviz 很棒,能够可视化关系在辩论架构方面非常有用,即使是独自辩论!

关系是短暂的

关于关系的一个关键点,以及为什么我们将“Is Chaste”这样的属性建模为“Sexual Behavior”实体,并与其与 Person 实体的关系。我们这样做是因为我们不能保证“is chaste”是该 Person 的永久状态。当然,我们可以将其实现为一个标志,但我们会丢失一些非常重要的东西:这个人是什么时候变得贞洁的?他们又是什么时候停止贞洁的?将关系提升为一等公民的最有用的功能之一是我们可以追踪关系事件的年代顺序。

以“成本”为例。一个物品有成本。如果我们有一个名为 cost 的字段,我们只能更新成本,除非我们有变更跟踪机制,否则我们将丢失该物品与其成本在成本有效期间的关系。但是,如果我们每次成本更改时都创建一个新关系,我们现在就有了该物品成本历史(本质上是内置的变更跟踪)。

这些是属性的简单示例,它们本质上不是关系性的,但我们正在强制它们变得关系化,以获取关于该属性可以采取的值的历史的信息。为了使之有意义,重要的是要认识到关系永不被破坏。关系永不消亡,它们只是到期。

关于活动(如派对,或死亡日期)的时间信息在哪里?它就在关系本身!并且有两种类型的时间信息

  • 在何时,例如“出生于此日期”或“参加派对于此时”
  • 开始和结束时间:“爱上罗瑟琳,从 1635 年到 1638 年”

由于时间信息内置在关系实现中(基本上,在关联表中),我们不需要显式建模一个“When”实体。

审计

既然如此,我们不妨内置审计:谁创建了实例和关系,以及何时创建。在这个抽象级别上,这很容易做到。

一些有趣的(理论上的)结果

查询

这种抽象有什么用?嗯,首先,它使得构造一些非常有趣的查询成为可能。例如,因为日期和时间在同一个地方管理,而不是在不同实体的属性中管理日期字段,所以我们可以查询“这一天发生了什么事情?”。因为地点在一个地方管理,我们可以问,“在这个地点 5 英里范围内发生了什么事情?”我们可以进一步过滤这些信息:“在某个特定日期、某个特定地点死亡的所有人是谁?”

属性

您会注意到,实际使用的属性非常少(除了隐式的“Name”)。相反,系统的“知识”在于关系信息。

关联表

这种架构打破了传统的自关联模型。通常,一个 Person 表将具有指向 Person、Family、Place、Action 的外键。相反,关联是极其自由形式的,要求关联表还要跟踪关联类型。这种关联是指向另一个 Person、Family、Place 还是 Action?我们需要知道这一点,作为进入关联表的信息的一部分,以便可以提取、显示和管理正确的数据。

信息管理

正如前面提到的,这种方法的一个副产品是数据和关系永远不会被销毁。你不会因为某人搬家就删除与某个地点相关的关系。你会让关系到期,然后创建一个新的。级联删除的概念毫无意义,因为你从不删除任何东西。唯一的例外是你意外创建了一些需要删除而不是更改的错误信息。即使是更改信息的概念也不同。如果你有关于一个人身体特征的信息,如眼睛颜色、体重、头发长度、面部毛发等,当你需要更新他们的特征时,你不会更改这些信息。相反,你会创建一个新的“Characteristics”记录,使旧的关系失效,然后创建一个 Person-Characteristics 关系指向新记录。是的,我们需要一个强大的 UI 来处理所有这些,以便用户在点击“更新”按钮时不必知道所有这些正在发生!

代码在哪里?

哇。我真的要给你们看一些代码了。因为整个架构都是用 XTree 应用程序建模的,所以树方面没有什么激动人心的地方。有很多轻量级的容器类。我也可以将它们抽象出来,但即使对我来说也有极限。这些类太简单了……

public class Entity : IHasCollection
{
  // We can look up our own entity collection for the name,
  // when the entity is referenced by something else, 
  // like the relationship "Described By" field.
  [Category("Name")]
  [XmlAttribute()]
  [TypeConverter(typeof(EntityNameConverter))]
  public string Name { get; set; }

  [Browsable(false)]
  public List<EntityAttribute> EntityAttributes { get; set; }

  [XmlIgnore]
  [Browsable(false)]
  public Dictionary<string, dynamic> Collection { get; protected set; }

  public Entity()
  {
    EntityAttributes = new List<EntityAttribute>();

    Collection = new Dictionary<string, dynamic>() { 
    {"EntityAttribute", EntityAttributes},
    };
  }
}

……以至于真的没有必要谈论它们。相信我。

我将向您展示生成图的代码。Graphviz 很酷!

生成图

关系图和实体图都调用一个特定方法来生成图

protected void Generate(StringBuilder sb)
{
  string filename = Path.GetTempFileName() + ".dot";
  File.WriteAllText(filename, sb.ToString());

  string progFilePath = Environment.GetEnvironmentVariable("ProgramFiles");
  string progFilex86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
  progFilePath = progFilex86 ?? progFilePath;

  Process.Start(progFilePath + @"\Graphviz 2.28\bin\dotty.exe", filename);
  File.Delete(@"c:\temp\graph.png");
  var pr = Process.Start(progFilePath + @"\Graphviz 2.28\bin\dot.exe", 
              "-Tpng " + filename + @" -o c:\temp\graph.png");

  while (!pr.HasExited)
  {
    Thread.Sleep(100);
  }

  // Process.Start(@"c:\temp\graph.png");
  Clipboard.SetText(sb.ToString());
}

关于这段代码有几点需要注意

  • 它期望在特定位置找到 Graphviz,由于文件夹名称包含版本号,它期望的是版本 2.28。
  • Graphviz DOT 数据被放入剪贴板,以便您可以检查它。

生成实体关系图

protected void OnGenerateEntityRelationships(object sender, EventArgs e)
{
  StringBuilder sb = new StringBuilder();
  sb.AppendLine("digraph G {");

  Schema.Instance.RelationshipsContainer.ForEach(t=>t.Relationships.ForEach(r=>
  {
    sb.AppendFormat(" {0} -> {1} [label={2}, taillabel={3}, headlabel={4}, labeldistance=2.0];\r\n",
       r.EntityA.Quote(), r.EntityB.Quote(), r.Label.Quote(), 
       r.CardinalityALabel.Quote(), r.CardinalityBLabel.Quote());
  }));

  sb.AppendLine("}");
  Generate(sb);
}

生成实体属性图

“record”形状在样式方面受到限制,因此根据 Graphviz 人员的反馈,我最终会将其更改为输出 HTML。

protected void OnGenerateEntityAttributes(object sender, EventArgs eventArgs)
{
  StringBuilder sb = new StringBuilder();
  sb.AppendLine("digraph G { graph [rankdir=\"LR\"];\r\n");

  // Define the entities and their attributes.
  Schema.Instance.EntitiesContainer.ForEach(t => t.Entities.ForEach(e =>
  {
    sb.AppendFormat("{0} [ label = \"<f0> {1}", e.Name.Quote(), e.Name);
    int n=1;

    e.EntityAttributes.ForEach(a =>
    {
      sb.AppendFormat("| <f{0}> {1}", n.ToString(), a.Name);
      ++n;
    });

    sb.AppendLine("\" shape = \"record\"]; ");
  }));

  // Create the graph by inspecting the relationships
  Schema.Instance.RelationshipsContainer.ForEach(t=>t.Relationships.ForEach(r=>
  {
    sb.AppendFormat(" {0} -> {1} [label={2}, taillabel={3}, headlabel={4}, labeldistance=2.0];\r\n",
      r.EntityA.Quote(), r.EntityB.Quote(), r.Label.Quote(), 
      r.CardinalityALabel.Quote(), r.CardinalityBLabel.Quote());
  }));

  sb.AppendLine("}");
  Generate(sb);
}

下一步?

现实检验。核心部分。实际实现底层结构以支持这种类型的架构和管理信息的 UI。UI 当然必须是动态生成的,但我还希望生成的 UI 可以编辑,以便为用户调整呈现。这将在下一篇文章中介绍。到目前为止,我只讲到了罗密欧与朱丽叶相遇的派对!

参考文献

特别感谢 Dmitri Nesteruk,他在两年前将 Graphviz 引入我的 项目可视化工具 时。我终于花了点时间学习更多关于 Graphviz 的知识!

© . All rights reserved.