选举2029:数据模型
数据模型(及视图模型)及其使用方式
我曾考虑在文章标题中使用“架构”一词,但觉得对于这个网站的规模来说显得过于浮夸。虽然我能证明其合理性,但每次使用这个词都会让我感到不适。不过,本文将基本描述我在选举网站中如何处理数据以及这些数据是什么。
在许多系统中,数据有多种用途,系统设计需要考虑所有这些用途。对于我的选举网站,我只需要担心两种用途:
- 网站本身,纯粹是只读的
- 维护数据的工具,是读写式的
工具仅由我在家中运行——因此我不需要担心多个并发写入的并发问题。但我需要确保网站在我写入过程中检查更新时始终读取一致的数据视图——我将在后续文章中讨论这一点。
本文仅涉及数据的内部表示,我基本上控制一切。大部分网站数据来自外部数据源,我将在后续的另一篇文章中介绍如何处理这些数据源。
网站如何使用数据
网站的代码涉及以下项目(暂时忽略测试项目)。此列表按依赖顺序排列:列表中的任何项目可能依赖于较早的项目,但不依赖于较晚的项目。
- 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列表所污染。理论上,我可以增加模型中的“层数”,以便政党可以拥有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(至少名义上)方法后,我现在有很多选择可以选择下一步写什么。我想我可能会深入存储方面……