棋牌类手机游戏项目反思。Review of the Kingz project
本项目是 H5 移动应用开发技术课程的作业,我们计划开发一个双人联机实时对战的棋牌类手机游戏。 我在此项目中负责系统设计和主要编码工作。 我决定用开发面向移动用户的 web 应用,给出了前端组件的功能设计和整体结构设计,并给出了后端 API 接口定义。 经过漫长的开发阶段,我们小组很多成员对我们的最终作品不太满意。我也认为这一项目非常失败。
概要
本项目前端的几乎全部代码都由我完成,就编码工作而言,其最大的特点是反反复复的重头开始,当我对目前的实现不满意时总想重新开始重头再来。 此外一个很大的弊端是面向过程编程,除了游戏实体外,我没有对诸过程设计抽象。所使用的开发框架也未有此支持,我甚至认为是裸金属风格的开发。
面向过程编程
我采用了一种基于事件总线的写法,任何实体都可广播消息,任何实体都可以注册一些监听的事件,并提供事件回调函数。 这种方法是规定了系统内各个组件之间的沟通方式,我觉得这本身没有什么特别好或者特别坏的地方。
但我感觉我的实现很不对劲。 首先是,有很多事件纯粹是为了做客户端路由的功能,然后是事件之间有隐式的依赖。 最后是如果在React.useEffect
里面注册事件订阅函数的话React.StrictMode
会在开发环境调用两次,生产环境调用一次。 这种一次两次的切换,会导致整个系统的行为有变化,甚至有错误。这个问题虽然能轻易的解决但是我认为这表示我的系统设计得并不好。
纯粹为了切换页面
切换页面这个事情本来就关系全局,各组件都使用同一个全局变量是必须的。我觉得不好的地方在于,我把切换页面的监听函数和其他业务逻辑写到 了一起,这让源码很混乱,代码的内聚程度低,不相干的东西被拼凑到一起。
期望的写法是,在不同的文件中、组件中、模块中监听同样的事件,做不同的事情。
1
2
3
4
5
// fileOne.ts
eventBus.subscribe("eventOne", change_page);
// fileTwo.ts
eventBus.subscribe("eventOne", save_game_state);
现在的写法是,全部都写到一起。
1
2
3
4
5
// fileOne.ts
eventBus.subscribe("eventOne", () => {
change_page();
save_game_state();
});
组件之间的隐式依赖
最简单的例子是订阅某事件的函数调用,必须得在发布这个事件的函数调用之前执行。如果这两个函数调用就在上下两行,这种隐式依赖容易被发现。 但如若不然,这种软件错误比较难被发现,而且也未必能被一致地复现。 这个例子在本项目中就出现了,其背景是在系统初始化的阶段中,有连续多个向事件总线订阅事件的函数调用。其中有的事件处理回调函数会发布新的事件。 这些在事件回调中发布的新事件,有的就还没有订阅者。示意如下
1
2
eventBus.subscribe("first", () => eventBus.publish("second"));
eventBus.subscribe("second", some_critical_function());
在上面的代码段中,如果事件总线像下面这样顺序同步地来实现,some_critical_function()
是不会被调用的。
1
2
3
4
5
function publish(event) {
for (const subscriber of subscribersOf(event)) {
subscriber(event);
}
}
我采用的解决方法是如果某个事件没有订阅者, 就先把这个事件放到一个等待区, 等到这个事件的第一个订阅者订阅时再把等待区中的时间全部交给他处理。
反复重头开始
下面简要列出重头到尾,我做过的失败的开发尝试:
- tk2:2022-04-23 做了 5 个小时,游戏,React,对游戏、棋盘、玩家的兵力、玩家都作了抽象。
- tk3:没有版本控制,React,游戏,对游戏、棋盘、玩家的兵力、玩家、游戏过程作了抽象,写了单元测试,主要是游戏规则和地图生成部分。
- tk4:08-30 到 10-03,Svelte,游戏,采用 Clean Architecture,对游戏逻辑、交互界面、网络模块作了抽象,写了单元测试。
- tk4-explanation:09-01 和 02 两天,Vanilla,注册、游戏、存档,用了事件总线写法、最简陋的图形界面。
- tk5:没有版本控制,Svelte,明显是为了解决 tk4 中太复杂,方法调用太多的问题,缩短了调用链,取消了很多不必要的类、接口等。
- tk6:10-13 不到 1 个小时,Vanilla,手动渲染 HTML 标记语言,引入了事件总线的写法,没有实现任何功能。
- tk7:10-14 到 10-31 ,React,写了其他非游戏内容,玩家注册、云端保存的游戏、等待匹配、游戏,用的是事件总线的写法,写的是井字棋。
- tk8:没有版本控制 ,Backbone,没有实现任何功能,只是为了了解这个框架。
- tk9:1024 和 25 两天 ,Django,除了框架自带的玩家注册功能,实现了两人对弈,创建房间,房间列表等,最简陋的图形界面。
我认为反复重头开始是因为缺少前期的设计,凭感觉编码,一段事件之后才发现捉襟见肘。 编码错误多,添加新的功能困难,出现很多意料不到的错误。但这一个判断,好像也很难从 git 历史中找到依据。
使用 git 的习惯不好
对一个 commit message,有很多无关的改动,或者说看不出有的改动怎么就跟这个 commit message 有关。 例如这个 commit,说是 实现了”prompt enter nickname before game start”,但从 diff 看,这只是把控制游戏生命周期的函数调用 沿移到原调用链下移。没有任何跟用户交互有关的内容。
每一个 commit message 都带一个 label 似乎也不好,因为我还想 commit 很多中间步骤,这些中间步骤单个看其实 没有什么作用
可笑代码摘编
- 在条件分支谓词之前保证该谓词为假,见此提交。