简单的投票系统实现(一)

🐍 本文中大部分代码使用 Python 或 JavaScript 实现

没错,写这篇文章正是因为在业余开发一个简单的投票系统。要注意,这是简单的投票系统,因此并不具有广泛的应用前景,只是突然想到了这样的方法,分享一下。还是像往常一样,在开发一个功能之前,我们往往需要思考这个功能的细节部分该如何实现。这次的投票,我们大概需要思考如下内容:

  • 如何设计投票
  • 投票的数据如何存储
    • 我们需要哪些数据
  • 投票者应对数据有着怎样的权限
  • 对投票项目的数目造成的更改会对数据产生怎样的影响
  • 过滤重复的投票者
  • 可视化地展示投票结果
  • ...

请不要把上面的内容看成目录,我不确定会不会 100% 按照它写

围绕着上面这些问题,从「多选式投票」的角度出发,我们来逐一解决并实现。

设计投票

一般来说,一个投票大概长这样:

我是投票的题目

  • 选项 A
  • 选项 B
  • 选项 C

标题并不重要,因为它只是一个纯文本,并不牵涉到任何复杂的操作,我们主要探讨选项部分。如果从组件的角度解释选项,大概就是同一个组件被利用了多次,每一个组件副本均有着不一样的文字,对应和控制着不一样的数据值。也正因为重复性,我们可以直接利用循环来「生成」这些选项。

我们首先需要定义一个描述该选项内容的文本数组,以及一个与文本数组相等长度的布尔数组,并将这个数组事先全部填为 false 以便记录。例如,如果我们要实现上面例子里的选项渲染,应当做的是:

let items = ["选项 A", "选项 B", "选项 C"]
let dataArray = [false, false, false]

更广泛的方法:

let items = ["A", "B", /* ... */]
let dataArray = [];
dataArray.length = items.length;
dataArray = dataArray.fill(false);

这样就可以轻松借助循环来实现在两个不同数组相同位置的数据关联。以 Vue 为例子:

<div>
  	<whatever v-for="(x, i) in items" :key="i" v-model="dataArray[i]">
    	{{ x }}
  	</whatever>
</div>

给 checkbox 或者 radio 组件绑定一个 v-model 指向数据数组的相应位置,那么该选项就与该位置的布尔值建立了联系。被选中为 true,未被选中或被反选则为 false

数据存储

我们选择使用最简单的方法——将数组以字符串的形式存储到数据库的表中。一般来说,对于复杂的情况,我们也许要记录用户或者其它杂七杂八的,但对于这种简单的情况,我们只需要收集有多少人选择了这一项。那么首先我们需要弄清楚的一个问题,如何将 [false, true, false, false] 这样的数组,转换为一个记录每项选中人数的数组。

既然我们的数组里每一个布尔值,都跟一个唯一的选项对应,那么我们可以记录所有为 true 的值在数组中的位置。例如,我们可以创造一个函数 getTrueIndexes(array: Array<boolean>): Array<number>

getTrueIndexes([false, false, true, false])
// => [2]

getTrueIndexes([true, false, true, true])
// => [0, 2, 3]

getTrueIndexes([false, false, false, false])
// => []

简单实现:

function getTrueIndexes(array: Array<boolean>): Array<number> {
	let arr = [];
	// 用 for 也可以
	array.forEach((k, i) => {
		if (k) arr.push(i)
	})
	return arr;
}

接下来我们来讲讲如何去存储每个选项选中的人数。一些人可能开始会想到这样的结构:

{
    a: 1,
    b: 2,
    c: 4
}

也就是利用对象的键值构建选项与人数之间的关系。

这种办法的缺点也很明显——显得很没必要。因为正如前文所介绍,投票项目呈有序排列的(即每一个项目的位置均与最终数据有对应联系),因此使用数组并无大碍;对象则将每一个选项的独特标识关联到了一个特定的数据,这种结构似乎更适用于那种可能会被打乱的数据集,或者更为复杂的投票结构,对于本文中的「简单」投票系统没有实际意义,同时也会对后面的操作造成困难。

所以,我们最终所需要的只是

[1, 2, 4]

这是什么意思?还是上文中的例子,这代表着选项 A 有一人选中,选项 B 有两人选中,选项 C 有四人选中。

对于此数组的初始化,不同于前文的 [false, false, false],它应当位于后端。使用 Python 可以轻松做到

# items 即前端中的选项文字数组
voteData = [0 for i in range(len(items))]

那么这个时候 voteData 就会被初始化为一个全为 0 的数组,就像 [0, 0, 0, 0],代表还没有任何人投任何项。

回到之前的话题来,我们获得了一个标注了「被选中」项目(即为 true 的项目)的位置的数组,然后此时我们要以它为依据对位于后端的统计人数的数组做出修改。实际上很简单

# 假设:来自前端的数据
voteTargets = [0, 1, 3, 4]
# 假设:后端记录人数的数据
voteData = [0, 0, 0, 0, 0, 0]

for i in voteTargets:
	voteData[i] += 1

也就是这样,可以直接实现对数组指定位置的修改。其中 += 1 是定值,除非是想要让每个人的一票变成多票。

修改投票

如果我们要对原先的投票项目做出修改(不是修改数据)怎么办?实际上,这是不被允许的——因为如果我们将投票的项目文本修改,就有可能影响投票本身的客观性,例如我们可以将「支持」项改成「反对」,这是很恐怖的。不过,本文只作技术讨论,具体地可以看情况进行取舍。

修改投票时,对应的数据的意义将会受到影响,在这里我们讨论两种极端情况,即「新建」和「清除」。

在对投票的项目进行编辑时,我们需要记录究竟删除了哪些项目,增加了多少项目?这些记录的代码可以放在删除或增加时所要调用的函数内。

<!-- ... -->

<script lang="js">
{
	data() {
		return {
			deletedCount: 0,
			// ...
		}
	},
	methods: {
		deleteVoteItem() {
			this.deletedCount += 1;
			// ...
		},
	},
	// ...
}
</script>

如果删除了项目,则对应的数据也应当被删除。因此,还需要加上记录该被删除项目在数组中的位置的机制;如果增加了项目,那么也应当在数据数组的最后追加相应数量的 0。具体流程应遵循先删除旧项再添加新项的原则,否则会导致数据的对应问题。删除时,使用 del 关键字可以实现:

# 删除
del voteData[index]

增加时,我们先创建上下文场景以便更好地叙述:

# 假设这是原先的投票项目数据
itemsOld = ["选项 A", "选项 B"]
# 假设这是添加后的投票项目数据
itemsNew = ["选项 A", "选项 B", "选项 C", "选项 D"]
# 假设这是原先的记录人数数据
voteData = [0, 0]

原理与先前大致相同,添加了简单的计算:

voteDataNew = voteData.extend([0 for i in range(len(itemsNew) - len(itemsOld))])

上述代码有没有问题?是有的。extend 方法会改变原先数组的数据而不会返回一个新的值,这与往常情况不同。因此,我们要去掉前面的赋值部分,否则会导致 voteDataNew 的值变为 None

voteData.extend([0 for i in range(len(itemsNew) - len(itemsOld))])
# 然后再对 voteData 进行操作就行了

以及请不要使用 append 方法,否则可能让你的数组变成 [0, 0, [0, 0]]

To be honest, no one likes it.

未完待续

由于个人原因,此文章的续集可能需要较长时间完成。

最后更新于: 2020/6/25
Powered by VuePress