- 发布于
回顾:CCLE 儿童牙科项目

背景
今年四月,我非常幸运地获得了在UCLA自家学习管理系统 CCLE 开发部门的实习机会。在熟悉了 Moodle、使用 Jira 的敏捷工作流程的入职培训后,我接触到了CCLE学生程序员已经开发了一段时间的这个副项目:儿童牙科项目。
目标是创建一个移动优化的Web应用程序,用于收集儿童牙科学生在牙科诊所学习和执行操作过程中的信息。这些数据将被处理,以确定他们是否通过了临床课程并达到了牙科学院的能力要求。目前,有一个基于纸质表格的系统来完成这些任务。我们的应用程序带来的价值是自动化,我的工作是充分熟悉这个项目,以便在夏天全职投入其中,并希望能顺利完成它。
贯穿这个项目的一个挑战是我们必须理解和处理的复杂业务规则。作为一项热身练习,我的任务是查看当前的数据库模式,并找出哪些种子数据应该放入表中。它包含二十多个表,每个表代表一种被认为与业务逻辑相关的实体类型。由于同一学期我正在修读数据库课程,我跳读了教科书的相关内容以理解模式设计,并成功编译了必要的种子数据和脚本,生成了一个数据库转储文件,该文件后来分发给了其他团队成员。回想起来,这个初步练习与我项目剩余时间里的重点完美契合,那就是理解和控制我们正在构建的系统中不断涌现的复杂性。
在为期三个月的全职实习期间,我与我的伙伴 Bruce Liu 紧密合作,他负责了大部分设计和前端工作,而我则负责前端逻辑和后端。许多好点子都源于我们就设计选择进行的长时间讨论。没有他的投入,这个项目绝对不会有今天的成果!
功能演示
为了让大家对项目有个整体了解,让我首先向你们展示我们这个夏天所完成的工作:
- 该应用程序通过 CCLE 课程中嵌入的外部工具链接启动,使用 LTI 协议。我们在此选择的链接会以“日间模块”模式打开应用程序,学生可以在此模式下完成课程的初步要求。用户通过从 CCLE 收到的 JWT进行身份验证,并看到应用程序的主页。请注意顶部和问候语中显示的名字如何提供了定制化的体验。
- 用户可以通过选择一个临床地点来创建一个“日志”,该日志对应半天的临床工作。
在每个半天中,学生可能会治疗几位患者。用户通过提供患者的基本信息来创建患者条目。(这些信息并非旨在唯一识别患者,以保护隐私。)
学生在每个患者的文本字段中回答问题。他们还可以记录在该患者身上观察/协助/执行的操作。“婴儿口腔护理”和“局部麻醉管理”操作被标记为红色,因为它们需要进一步的操作;如果学生观察了任一操作,则需要回答更多问题。已完成或不需要进一步操作的操作被标记为绿色。
为完成这两个特殊操作,用户点击查看按钮打开一个模态框,其中包含一份观察清单。请注意某些清单项目如何包含用于说明的信息框,以及在完成清单后操作如何变为绿色。
- 为了提交日志,学生必须首先找导师签字,以证明其在诊所的出勤。
- 验证出勤后,“提交日志”按钮将启用。我们在后端实施了完整性检查,因此不完整的日志无法提交。
现在我们已经看完了“日间模块”中几乎所有值得注意的内容!您可能想知道课程中所有其他链接的用途。它们会以不同模式打开应用程序,这些模式对应课程的后续阶段。工作流程是相同的,但某些组件扩展了更强大的功能。例如,上面的视频显示,在“每周模块”中,我们在创建患者条目时会请求更多信息,并且一些自由格式的文本字段已被特定属性取代。
进度模态框总结了每个模块的要求。它指明了某些操作需要在特定角色(观察者、协助者、能力评估或能力考核之一)下记录,学生至少应接诊多少患者,以及至少需要多少日志等。学生可以使用此界面跟踪自己的进度。此视图也对应于导师使用的报告界面。
“每周模块”中的“添加操作”模态框也得到了增强,允许用户选择角色。在“日间模块”中,所有操作都假定是以观察者身份完成的。“临床检查”的“能力考核”选项是禁用的,因为学生必须首先通过“能力评估”(模拟考试)。注意:“患者护理评估”是一项特殊操作,如果添加的操作角色是 CA(能力评估)或 CE(能力考核),则会自动添加此操作。
“日间模块”和“每周模块”之间的另一个区别是,在“每周模块”中,学生需要进行1到3分的自我评估。之后,他们将手机交给导师,导师将评价他们的表现并给出最终成绩。
验证出勤后,学生可以提交他们的“每周模块日志”,就像在“日间模块”中一样。提交后,进度条和模态框会立即更新。它正确地显示:1)用户已提交日志;2)“患者护理评估”和“临床检查”已在“能力评估角色”下完成。
最后,我想向大家展示我们为导师实现的报告界面。它实质上显示了学生在进度模态框中看到的内容,但是是汇总形式,以便导师可以轻松跟踪每个学生的进度。
反思
哇哦……刚才的功能演示是不是很长?这还是在省略了许多次要功能和边缘场景之后。从它的长度你大概能看出我们实现的业务逻辑相当复杂。
是的,今年夏天我们的时间在规划和实施之间分配得相当均匀。第一部分包括收集需求、根据我们对客户需求和技术限制的理解来设计用户界面、展示我们的设计并进行修改以获得批准。
收获一:沟通是关键
回想起来,我的第一个,或许也是最有启发性的认识是:我们项目的成功绝对依赖于我们在利益相关者(尤其是客户)和我们开发者之间保持的有效沟通渠道。沟通之所以如此关键,是因为许多技术挑战可以通过探索合理的替代方案来规避。
例如,我们的客户向我们展示了我们的应用程序旨在取代的纸质表格。由于是独立开发的,每个模块的表格都有不同的结构。我们没有为每套表格创建多个用户界面,而是向客户沟通了为什么这样做会耗费大量时间来实现,并合作设计了一个适用于所有模块的统一界面。
在另一个反复出现的情况中,对于我们为满足某个需求可能采取的几种方法,客户并无偏好,我们能够采用与现有方案逻辑上最一致且因此最容易实现的方法,因为我们与她明确了这一点。
通过有效的沟通,我们还能够识别客户的痛点并提出“杀手级”功能。进度条/模态框和报告界面就是这方面的例子。我们站在牙科学生和导师的角度思考,我们的应用程序如何在简单自动化表格之外提供更多价值。
收获二:当业务需求尚不明确时,坚持敏捷工作流程
团队使用敏捷方法,以便能够逐步向客户交付价值,这降低了风险,并为所有利益相关者提供了项目走向的更清晰的图景。当我春天刚加入团队时,我们所处的情况是整个数据库模式已经设计完毕,超过50%的后端代码已经编写完成,但前端组件却一个都没有完全实现。前端人员经常需要后端以某种特定方式提供数据,而这与之前的设想不同。因此后端代码需要更改,甚至数据库模式也可能需要更改。后端人员的大部分时间都花在处理这类请求上,我们也不断错过交付目标。
我们当时的做法非常不敏捷,并在生产力方面付出了沉重代价。更重要的是,当我和 Bruce 在七月初仔细研究提议的前端设计时,我们意识到它在模型图之外根本无法工作。它使用了卡片堆叠设计,并且不具备可扩展性。在简单的“日间模块”中使用3张卡片可能还行,但在后续模块中,用户界面需要显示大约10张卡片的堆叠,这种设计会表现得很差。
这一发现导致我们进行了一次痛苦的重构,重写了大部分前端代码和紧密耦合的后端代码。此后,我们遵循了基于用户故事的方法,以避免历史重演。
退一步说,既然我已经完成了这个项目,我相信在需求不确定的情况下应该使用敏捷工作流程。但是,如果让我从头再做这个项目,鉴于我现在所了解的,我会采取一种更激进的方法:一开始让每个人都专注于前端开发,并在需要时使用硬编码的API响应。最有成效的事情是向客户展示可工作的原型,客户可以基于此给我们下一组需求。那么后端和数据库呢?嗯,后端本质上是一个CRUD应用程序;在我们弄清楚大部分前端之后,它无法完成的风险很小。这样,我们可以更快地发现需求,并最大限度地减少重写后端所花费的时间。
收获三:扁平优于嵌套
当我们重新设计应用程序时,我们根据表单的逻辑层次结构来构建前端:一个模块 (Block) 包含若干日志 (Log),每个日志包含一个或多个患者 (Patient),每个患者包含一个或多个操作 (Procedure),每个操作包含一个或多个步骤 (Step)。我们创建的组件也恰好具有这些父子关系。
回想起来,采用这种高度嵌套的设计可能并非最佳方法,因为它在 React.js 中实现时带来了一些挑战。首先是多层组件之间共享状态的问题。随着应用程序复杂度的增加,将所有东西都作为 props 传递是不可扩展的。因此我们研究了 Context API,并将每个组件可能感兴趣的状态放入一个 GlobalContext 对象中。虽然这在很大程度上解决了我们的问题,但我仍然认为,如果我们一开始没有那么多层级的组件,状态管理会简单得多。(那么 Redux 呢?Bruce 认为 Redux 需要太多样板代码,并不真正适合我们的应用程序)。
我们遇到的另一个问题是确定哪部分状态应该放在哪里。我们不得不在最大限度减少 API 调用次数和简化我们对组件的推理方式之间做出妥协。虽然网络效率倾向于让根组件通过单个 API 调用批量下载状态,但在将组件视为管理自身状态时,编程要容易得多。我们还实现了乐观渲染,以达到接近原生应用的响应水平,这进一步使事情复杂化。最终,我们取得了平衡,让 Patient 组件管理其子组件的状态,而 Patient 的祖先组件则管理它们自己的状态。
我会怎么做不同呢?可能会选择一个更扁平、看起来更传统的移动应用布局。
收获四:不要吝啬测试
测试是软件工程中控制风险的另一种非常有效的方法。另一方面,编写大量代码却从不运行它是危险的!代码中隐藏的错误几乎肯定比你想象的要多。
当我开始从事后端工作时,我花了不少时间来设置 Jest 进行测试,结果回报丰厚。多亏了测试环境,我可以隔离并运行单个函数,而不必同时处理整个服务器。即使在调试完成后,拥有一个能够检测到大多数破坏性更改的测试套件也是一个非常令人安心的工具。
不过,单元测试并不是唯一的测试方法。我认为在某个点上,编写更多单元测试的边际效益太低了。当我获得第一个可工作的前端,并能在 Chrome 检查器的网络面板中看到我的后端代码运行情况时,我感到我的软件开发周期变得更加有趣和投入。
基于这些观察,我建议尽早并经常测试你的代码,使用你认为最有效的方法,尤其是在使用像 Javascript 这样的动态类型语言编写代码时,因为在这种情况下,IDE 的帮助不大。
技术栈
不简要介绍一下我们使用的技术栈,我就无法完成这篇总结,嘿嘿。
你们已经知道我们前端使用了 React.js,但我想补充一点,Bruce 发现了一个非常有用的库 styled-components,我们用它制作了一个(希望你们也会同意)非常漂亮的用户界面!
至于后端,我们使用了 Express.js 和 MySQL。这次真正的新东西是我们广泛使用了 Sequelize,一个 ORM(对象关系映射器),它被证明非常有用。
后记
我非常享受在 CCLE 实习的这个夏天。这个牙科项目充满挑战,令人难忘。我们成功完成了它,我为此感到高兴。更重要的是我在实习期间遇到的人们。实习结束后我们仍然是朋友,谁知道未来我们是否会再次携手,共同开发一些更有趣、更激动人心的项目呢?