数据模型(和视图模型)及其使用方式
我考虑在文章标题中使用“架构”一词,但感觉对于这个网站的规模来说显得过于夸张。虽然我能证明其合理性,但每次使用这个术语都会让我感到不适。不过,这篇文章基本上将描述我如何在选举网站中处理数据,以及这些数据是什么。
在许多系统中,数据有多种用途,系统设计需要考虑所有这些用途。对于我的选举网站,我只需要关注两种用途:
- 网站本身,纯粹是只读的
- 用于维护数据的工具,是读写式的
工具仅由我在家中运行,因此我不需要担心多个并发写入的并发问题。但我确实需要担心并发性,以确保即使在我写入过程中网站检查更新时,网站始终读取数据的一致视图——我将在后续文章中介绍这一点。
本文仅涉及数据的内部表示,在这里我基本上控制一切。大部分网站数据来自外部数据源,我将在后续的另一篇文章中介绍这些数据源的处理方式。
网站如何使用数据
网站的代码涉及以下项目(暂时忽略测试项目)。此列表按依赖顺序排列:列表中的任何项目可能依赖于较早的项目,但不依赖于较晚的项目。
- Election2029.Common:工具代码
- Election2029.Models:核心数据模型,全部是不可变的(大多是密封记录)
- Election2029.Storage:纯粹关于存储数据的代码,无论是在文件系统中还是在Firestore中,但具有通用接口
- Election2029.ViewModels:围绕模型的不可变包装器,以简化视图代码(不依赖于存储)
- Election2029.Web:ASP.NET Core代码,包含Razor页面等
Razor页面通常没有代码隐藏,而是注入相应的视图模型,这些模型在简单的Razor语法中仅呈现为HTML。目的是保持视图代码非常简单——我不介意在这里或那里使用循环,但大部分视图应该是HTML而不是C#。此外,将任何“真实”逻辑放在视图模型中使它们更容易测试,这就是我如何拥有全面的测试套件并渴望拥有单元测试。
每个页面都注入“该视图的世界状态的当前视图模型”。此视图模型会重复使用,直到基础数据发生变化,这种情况相对较少。但它确实会变化,这意味着视图模型不能作为单例注入。我将在后续文章中重新讨论数据重新加载机制。(我意识到我经常这么说。随着文章的继续,这种情况当然应该减少。)页面渲染的结果也会被缓存,但仅缓存10秒,以在数据新鲜度和过度工作重复之间取得平衡。理论上,我可能可以说“缓存直到我们有新的视图模型”,但这可能比值得的更复杂。
工具如何使用数据
工具的代码涉及以下项目,以及网站的Election2029.Common、Election2029.Models、Election2029.Storage。
- Election2029.Tools.Common:所有工具的通用工具代码(因为除了我在这里讨论的“数据管理器”之外还有一些工具)
- Election2029.Tools.DemocracyClub.Models:用于Democracy Club数据的模型——我从中导入的外部源
- Election2029.Tools.Parliament.Members:用于议会成员API的模型和客户端代码——另一个外部源
- Election2029.Tools.DataManager:用于各种数据管理任务的命令行工具,包括添加投票、从外部数据源更新数据等
重要的是,这完全不使用Election2029.ViewModels。在我开始写这篇文章之前,DataManager项目对ViewModels项目有依赖,这让我有点紧张……但试探性地移除它后,我发现只有少数引用,我们很容易修复了。(可以说,DataManager能够执行一些简单的文本格式化仍然有用,例如“格式化此投票的实地工作日期”——我将来可能会将其移动到模型项目中。)
模型
模型本身不仅仅是记录定义,尽管我将在这里展示这些。我认为数据模型具有以下附加成员是合理的:
- 简单的便利计算。例如,通过汇总每个政党的投票数来计算单个选区结果的投票总数。
- 基于其余数据的查找。例如,“投影集”包含按选区的投影结果列表;我们有一个从选区到“该选区结果”的查找。
这些可以放在视图模型中,但它们通常被多个视图模型使用,因此将它们放在模型中是有意义的。
模型被分组在一起。有一个ElectionCoreContext包含不相互引用的独立模型,然后是ElectionContext,它包含所有内容——它有一个ElectionCoreContext,然后其他模型可以引用核心上下文中的模型,但不能引用其他非核心模型。这意味着创建ElectionContext包括:
- 独立创建核心上下文的每个元素
- 从这些元素创建核心上下文
- 创建每个非核心元素,以核心上下文作为参考
- 从核心上下文和非核心元素创建整体上下文
这使得数据模型比元素不能引用其他模型时更容易使用——例如,这意味着PartyChange可以引用Candidate和Party模型,而不仅仅是包含候选人的ID和旧/新政党ID。核心/非核心分割意味着所有这一切都可以在没有任何循环依赖的情况下完成。
在下面的所有声明中,我省略了记录实现的任何额外接口,以及记录中的任何代码。
快速术语说明:这里的“上下文”只是我用来表示“世界最新状态”的术语。它与Entity Framework无关,与DDD有界上下文的任何相似之处至少有些巧合。(或者我怀疑您可以将整个ElectionContext视为单个有界上下文——系统足够小,一个边界围绕所有内容。我对DDD的了解不足以理解系统与它的契合程度。)
核心模型
ElectionCoreContext具有以下声明:
|
|
这些元素包括:
|
|
关于这些的一些说明:
- 通常我发现,以列表而不是集合或字典的形式建模记录更容易保持数据一致性……即使集合的顺序在逻辑上无关紧要(例如对于选区),拥有规范顺序非常方便,以便相等性测试更简单,差异易于阅读等。
- ConstituencyLinks可以内联到Constituency中(实际上在存储中是这样做的)。这与Candidate如何内联外部数据源的ID不一致。这可能会随时间改变。
- “核心元素不相互引用”的方面在一定程度上被Party具有选举委员会ID列表所污染。理论上,我可以增加模型中的“层数”,以便Party可以拥有ImmutableList
……但事实证明,我很少需要引用选举委员会政党。(背景是有大量政党在选举委员会注册。大多数时候我们只需要主要政党,我们不想或不需要区分,例如“绿党”和“苏格兰绿党”——或者实际上是两个都称为“保守和统一党”的选举委员会政党。) - DataProvider的DataDirectory部分仅由工具使用……这感觉有点奇怪,但并非特别不合理。
- DataProvider(基本上用于投票和席位投影)有一个Enabled标志,允许我在获得在网站上使用的许可时,从提供者摄取数据而不显示它。
非核心模型
到目前为止,相对简单。完整的ElectionContext相当大:
|
|
这些元素包括:
|
|
更多说明:
- ReadOnlyMemory
不一定是不可变的(例如,它可以包装byte[]);我可能会在某个时候将其更改为ImmutableList ,尽管在实践中它不会引起任何问题。邮政编码数据的结构本身就是一个有趣的话题——您猜对了——我将在另一篇文章中介绍。 - 对于非英国读者,补选是在正常周期之外进行的选举,由议员辞职、死亡或通过请愿被驱逐引起。(补选也发生在非议会选区的情况下,但我的网站只处理议会选区。)
- 在系统中,“投票”一词实际上是“选区中的候选人列表”;在日常使用中,它也可以表示“单张投票”,但我想不出更好的术语,这就是Democracy Club使用的术语。
- 没有ResultSet的LastUpdated属性我们可能也能管理,但将其作为自己的类型仍然有意义,因为它包括从选区到结果的简化映射。对于2019年名义结果来说,这样做(还)不值得,因为它们在少得多的地方使用。
- 谈到2019年名义结果……这些是“特殊的”,因为2019年和2024年之间发生了显著的选区边界变化。名义结果是对新边界下2019年实际结果会是什么样子的预测。有关更多细节,请查阅关于该主题的“Rallings & Thrasher”文档。
- Poll.PreviousId属性用于计算ShareChange。它可能应该消失——它在网站中任何地方都没有使用。(写这些文章的一个好副作用是某种代码审查的效果。在我实际发布这篇文章时,我可能已经移除了它。)
- PartyChange没有它可能的那样规范化。可以说,我们应该记录给定人员的政党历史,然后通过考虑“在任何给定时间谁是议员,以及他们在任何给定时间代表哪个政党”的历史在选区视图中反映这一点——这绝对是我在未来几年可能重新审视的领域。(一旦我们有了第一次实际补选,这很快就会发生,推理起来会更容易。)
- CurrentRepresentation同样有点奇怪,因为我们应该能够从2024年选举结果、补选和政党变化的组合中推导出数据。目前它碰巧方便,但随着时间的推移可能会变得不那么方便。
关于PartyChange和CurrentRepresentation的方面让我想知道我是否可能引入一个ConstituencyHistory模型,它不存储在任何地方,而是在ElectionContext构造时派生。
最终想法
如前所述,模型和视图模型都是不可变的。在我尝试使用不可变性的其他项目中,我遇到了相当多的摩擦——但这是一个完全只读的网站,加上C#记录的功能,使其变得非常 straightforward。我一直都知道的好处(使推理其他代码可能做什么更容易,不必担心线程安全)仍然像以往一样有价值……但在这个特定项目中,限制和挫折并不是问题。C#记录有一些挑战和烦恼,但没有什么真正的问题。
我现在要强调的一个方面,并在以后专门发布一些性能问题时讨论,是在单个ElectionContext中,模型的引用相等性是我们所需要的全部。如果您有两个引用不同对象(但属于同一ElectionContext)的ProjectionSet引用,它们肯定是“不同”的投影集。有极少数模型这只是巧合为真,例如ProjectionMeasure,但这些没有任何逻辑身份。所有“主要”模型确实都有某种形式的身份,在ElectionContext中,引用相等性等同于逻辑身份。
描述了数据模型和网站的MVVM(至少名义上)方法后,我现在有很多选项可以选择接下来写什么。我想我可能会深入存储方面的事情……