开源应用程序架构(第二卷)
OSCAR

Jennifer Ruttan

自 EMR(电子病历)系统首次采用以来,一直试图弥合患者护理的物理世界和数字世界之间的差距。世界各国的政府都试图找到一个解决方案,以更低的成本为患者提供更好的护理,同时减少医疗保健中通常产生的纸质记录。许多政府在创建这种系统方面取得了很大成功,而另一些政府,比如加拿大安大略省的政府,则没有成功(一些人可能还记得安大略省所谓的“电子健康丑闻”,根据审计长报告,该丑闻给纳税人造成了 10 亿加元损失)。

EMR 允许对患者病历进行数字化,如果使用得当,应该可以让医生更容易地提供护理。一个好的系统应该为医生提供患者当前和持续状况的鸟瞰图,包括处方历史、最近的实验室结果、过去就诊历史等等。OSCAR(开源临床应用程序资源)是位于加拿大安大略省汉密尔顿市麦克马斯特大学的一个大约十年的项目,是开源社区试图以低成本或免费的方式为医生提供这种系统。

OSCAR 有许多子系统,它们以组件为基础提供功能。例如,oscarEncounter 提供了一个与患者病历直接交互的界面;Rx3 是一个处方模块,它会自动检查过敏和药物相互作用,并允许医生从 UI 中直接将处方传真给药房;集成器是一个组件,用于在多个兼容的 EMR 之间共享数据。所有这些单独的组件共同构建了典型的 OSCAR 用户体验。

OSCAR 并不适合每一位医生;例如,专家可能不会发现该系统的全部功能都有用,而且它也不容易定制。但是,它为日常与患者互动的一般医生提供了全套功能。

此外,OSCAR 已经获得了 CMS 3.0 认证(并已申请 CMS 4.0 认证),这允许医生获得在他们的诊所安装该软件的资金(有关详细信息,请参阅 EMR Advisor)。获得 CMS 认证需要通过安大略省政府的一系列要求并支付费用。

本章将以相当笼统的术语讨论 OSCAR 的架构,描述层次结构、主要组件,最重要的是过去决策对项目的影响。作为结论和总结,将讨论如果现在有机会重新设计 OSCAR,它可能如何设计。

16.1. 系统层次结构

作为 Tomcat Web 应用程序,OSCAR 通常遵循典型的模型-视图-控制器设计模式。这意味着模型代码(数据访问对象,或 DAO)与控制器代码(Servlet)分离,而控制器代码又与视图(Java Server Pages,或 JSP)分离。两者之间的最大区别在于 Servlet 是类,而 JSP 是带有 Java 代码标记的 HTML 页面。数据在 Servlet 执行时被放置到内存中,而 JSP 读取相同的数据,通常通过读取和写入请求对象的属性来完成。几乎 OSCAR 中的每个 JSP 页面都具有这种设计。

16.2. 过去决策

我提到 OSCAR 是一个相当古老的项目。这对 MVC 模式应用的有效性有影响。简而言之,代码中有一些部分完全无视了该模式,因为它们是在更严格地执行 MVC 模式之前编写的。一些最常见的特性就是这样编写的;例如,执行与人口统计(患者记录)相关的许多操作都是通过 demographiccontrol.jsp 文件完成的,这包括创建患者和更新他们的数据。

OSCAR 的年龄是解决目前源代码树中所面临的许多问题的障碍。事实上,已经付出了巨大的努力来改善这种情况,包括通过代码审查流程来执行设计规则。这是目前社区决定采用的一种方法,它将允许未来更好的协作,并将防止不良代码成为代码库的一部分,这在过去一直是一个问题。

这绝不是对我们现在如何设计系统部分的限制;然而,这在决定修复 OSCAR 中过时部分的错误时会变得更加复杂。作为修复人口统计创建功能错误的人,您是使用与当前代码相同的风格修复错误呢?还是完全重写该模块,使其严格遵循 MVC 设计模式?

作为开发人员,我们必须仔细权衡我们在这种情况下的选择。不能保证如果您重新设计系统的一部分,您就不会创建新的错误,当患者数据岌岌可危时,我们必须谨慎地做出决定。

16.3. 版本控制

在 OSCAR 的大部分生命周期中,都使用 CVS 存储库。提交通常不会检查一致性,并且有可能提交可能破坏构建的代码。开发人员很难跟上变化,尤其是那些在项目生命周期后期加入项目的开发人员。新开发人员可能会看到他们想要更改的内容,进行更改,并将更改提交到源分支,而其他人可能要过几周才会注意到有重大内容被修改了(这在长时间的假期中尤其普遍,比如圣诞假期,因为没有多少人监视源代码树)。

事情已经发生了变化;OSCAR 的源代码树现在由 git 控制。对主分支的任何提交都必须通过代码风格检查和单元测试,成功编译,并由开发人员进行代码审查。(这大部分由 Hudson(一个持续集成服务器)和 Gerrit(一个代码审查工具)的组合来处理。)该项目已经变得更加严格控制。由对源代码树的处理不当导致的许多问题(甚至所有问题)都已解决。

16.4. 数据模型/DAO

在查看 OSCAR 源代码时,您可能会注意到有许多不同的方法可以访问数据库:您可以通过一个名为 DBHandler 的类直接连接到数据库,使用旧的 Hibernate 模型,或使用通用的 JPA 模型。随着新的更易于使用的数据库访问模型的出现,它们被集成到 OSCAR 中。结果是,现在关于 OSCAR 如何与 MySQL 中的数据交互的画面略显混乱,三种数据访问方法之间的区别最好用例子来说明。

电子表格(DBHandler)

电子表格系统允许用户创建自己的表格以附加到患者记录,此功能通常用于用数字版本替换纸质表格。在创建特定类型的表格的每次操作时,都会加载表格的模板文件;然后,将表格中的数据存储在数据库中,以便每个实例都与一个患者记录相关联。

电子表格允许您通过自由形式的 SQL 查询(在名为 apconfig.xml 的文件中定义)从患者病历或系统的其他区域中提取某些类型的数据。这非常有用,因为表格可以加载并立即填充人口统计信息或其他相关信息,而无需用户干预;例如,您无需输入患者姓名、年龄、出生日期、家乡、电话号码或为该患者记录的最后备注。

在最初开发电子表格模块时,做出了一个设计决策,使用原始数据库查询来填充一个名为 EForm 的 POJO(普通旧式 Java 对象),该对象位于控制器中,然后传递给视图层以在屏幕上显示数据,有点类似于 JavaBean。在这种情况下使用 POJO 实际上更接近于 Hibernate 或 JPA 架构,正如我将在接下来的部分中讨论的那样。

与保存电子表格实例和模板相关的所有功能都是通过 DBHandler 类运行的原始 SQL 查询来完成的。最终,DBHandler 是一个简单的 JDBC 对象的包装器,在将查询发送到 SQL 服务器之前不会仔细检查它。这里应该补充一点,DBHandler 是一个潜在的安全漏洞,因为它允许将未经检查的 SQL 发送到服务器。任何使用 DBHandler 的类都必须实现自己的检查,以确保不会发生 SQL 注入。

根据您正在编写的应用程序类型,直接访问数据库有时是可以的。在某些情况下,它甚至可以加快开发速度。但是,使用这种方法访问数据库不符合模型-视图-控制器设计模式:如果您要更改数据库结构(模型),则必须更改其他地方的 SQL 查询(在控制器中)。有时,在 OSCAR 的数据库表中添加某些列或更改其类型,需要进行这种侵入性的操作才能实现小的功能。

您可能不会感到惊讶地发现,DBHandler 对象是源代码中仍然完好无损的最古老的代码片段之一。我个人不知道它来自哪里,但我认为它是 OSCAR 源代码中存在的“最原始”的数据库访问类型。不允许使用此类编写新代码,如果提交使用此类的代码,该提交将被自动拒绝。

人口统计记录(Hibernate)

人口统计记录包含有关患者的一般元数据;例如,他们的姓名、年龄、地址、语言和性别;可以将其视为患者在初次就诊时填写的信息表的结果。所有这些数据都将作为 OSCAR 中特定人口统计信息的“主记录”的一部分进行检索和显示。

使用 Hibernate 访问数据库比使用 DBHandler 安全得多。首先,您必须明确定义哪些列与模型对象中的哪些字段匹配(在本例中,为人口统计类)。如果您要执行复杂的联接,则必须将其作为预处理语句进行。最后,在执行查询时,您只会收到所请求类型的对象,这非常方便。

使用 Hibernate 风格的 DAO 和模型对的工作流程非常简单。在人口统计对象的情况下,有一个名为 Demographic.hbm.xml 的文件,它描述了对象字段和数据库列之间的映射。该文件描述了要查找的表以及要返回的对象类型。当 OSCAR 启动时,将读取此文件并进行完整性检查,以确保实际上可以进行这种类型的映射(如果无法进行映射,则服务器启动将失败)。运行后,您将获取 DemographicDao 对象的实例,并对其运行查询。

使用 Hibernate 比使用 DBHandler 更好的地方在于,所有发送到服务器的查询都是预处理语句。这限制了您在运行时运行自由形式的 SQL,但也防止了任何类型的 SQL 注入攻击。Hibernate 通常会构建大型查询来获取数据,而且它的执行效率并不总是很高。

在上一节中,我提到了电子表格模块使用 DBHandler 来填充 POJO 的例子。这是防止编写这种代码的下一个合乎逻辑的步骤。如果模型必须更改,则只需要更改 .hbm.xml 文件和模型类(新字段和新列的 getter/setter),这样做不会影响应用程序的其余部分。

虽然DBHandler比Hibernate方法更新,但Hibernate方法也开始显现出其老化。它并不总是方便使用,并且每个要访问的表都需要一个大的配置文件。设置新的对象对需要时间,如果操作不当,OSCAR甚至无法启动。因此,也不应该编写使用纯Hibernate的新代码。相反,新开发中正在采用通用的JPA。

集成器同意(JPA)

最新的数据库访问方式是通过通用JPA实现的。如果OSCAR项目决定从Hibernate切换到另一个数据库访问API,则符合DAO和模型对象的JPA标准将使迁移变得非常容易。不幸的是,由于这对于OSCAR项目来说是“新的”,因此系统中几乎没有实际使用这种方法获取数据的区域。

无论如何,让我解释一下它是如何工作的。您不是添加.hbm.xml文件,而是向模型和DAO对象添加注释。这些注释描述了要查找的表、字段的列映射以及联接查询。所有内容都包含在两个文件中,而无需其他操作。尽管Hibernate仍在后台运行,但实际上它从数据库中检索数据。

所有集成器的模型都是使用JPA编写的——它们很好地展示了新的数据库访问方式,以及证明它作为一项新技术即将被实施到OSCAR中,但尚未在很多地方使用。集成器是源代码中相对较新的补充。与Hibernate相比,使用这种新的数据访问模型非常有意义。

在本节中,我们触及了一个现在常见的主题,即JPA使用的带注释的POJO允许更简化的体验。例如,在集成器的构建过程中,将创建一个SQL文件,用于为您设置所有表——这是一件非常有用的事情。有了这种能力,就不可能创建不匹配的表和模型对象(与其他类型的数据库访问方法一样),您也不必担心列和表的命名。没有直接的SQL查询,因此不可能创建SQL注入攻击。简而言之,它“正常工作”。

JPA的工作方式可以被认为与Ruby on Rails中的ActiveRecord非常相似。模型类定义数据类型,数据库存储它;介于两者之间的过程——数据的输入和输出——不由用户决定。

Hibernate和JPA的问题

Hibernate和JPA在典型用例中都提供了一些显著的优势。对于简单的检索和存储,它们确实减少了开发和调试时间。

但是,这并不意味着它们在OSCAR中的实现没有问题。由于用户没有定义数据库和引用特定行的POJO之间的SQL,因此Hibernate可以选择最佳方式来执行此操作。这种“最佳方式”可以通过几种方式体现出来:Hibernate可以选择只从行中检索简单数据,或者执行联接并一次性检索大量信息。有时这些联接会失控。

以下是一个示例:casemgmt_note表存储所有患者的笔记。每个笔记对象存储有关该笔记的大量元数据,但它还存储该笔记涉及的所有问题的列表(问题可能是“戒烟”或“糖尿病”,这些描述了笔记的内容)。问题列表在笔记对象中表示为List<CaseManagementIssue>。为了获得该列表,casemgmt_note表将与casemgmt_issue_notes表(充当映射表)以及casemgmt_issue表联接。

当您想要在Hibernate中编写自定义查询时,这种情况需要这样做,您不会编写标准SQL,而是编写HQL(Hibernate查询语言),然后将其转换为SQL(通过插入所有要选择的字段的内部列名),然后再插入参数并将查询发送到数据库服务器。在这个特定案例中,查询是用没有联接列的基本联接编写的,这意味着当查询最终转换为SQL时,它非常庞大,以至于无法立即清楚地知道查询正在收集什么。此外,在几乎所有情况下,这都没有创建足够大的临时表以至于造成影响。对于大多数用户来说,此查询实际上运行得足够快,以至于不会被注意到。但是,此查询效率极低。

让我们退一步。当您对两个表执行联接时,服务器必须在内存中创建一个临时表。在最通用的联接类型中,行数等于第一个表中的行数乘以第二个表中的行数。因此,如果您的表有500,000行,您将其与一个有10,000,000行的表联接,那么您刚刚在内存中创建了一个5×1012行的临时表,然后对该临时表运行select语句,并丢弃该临时表。

在我们遇到的一个极端情况下,跨三个表的联接导致创建一个大约7×1012行的临时表,其中最终选择了大约1000行。此操作大约花费了5分钟,并在运行过程中锁定了casemgmt_note表。

这个问题最终通过使用准备好的语句来解决,该语句在与另外两个表联接之前限制了第一个表的范围。更新且效率更高的查询将要选择的行数降低到非常可管理的300,000,并且极大地提高了笔记检索操作的性能(将执行相同的select语句所需的时间缩短到大约0.1秒)。

故事的寓意很简单,虽然Hibernate做得相当不错,但除非联接非常明确地定义和控制(在.hbm.xml文件中或JPA模型的对象类中的联接注释中),否则它可能会很快失控。处理对象而不是SQL查询要求您将查询的实际实现留给数据库访问库,并且只允许您控制定义。如果您对定义方式不小心,那么在极端条件下,一切都可能崩溃。此外,如果您是一位拥有大量SQL知识的数据库程序员,那么在设计支持JPA的类时,它不会有太大帮助,并且会移除您手动编写SQL语句时会拥有的某些控制权。最终,需要同时了解SQL和JPA注释以及它们如何影响查询。

16.5. 权限

CAISI(客户端访问集成服务和信息)最初是一个独立的产品——OSCAR的一个分支,用于帮助管理多伦多的无家可归者收容所。最终决定将CAISI的代码合并到主源分支中。原始的CAISI项目可能不再存在,但它为OSCAR带来的东西非常重要:它的权限模型。

OSCAR中的权限模型非常强大,可以用来创建尽可能多的角色和权限集。提供者属于项目(作为员工),他们在那里拥有特定的角色。每个项目都在某个设施中进行。每个角色都有一个描述(例如,“医生”、“护士”、“社会工作者”等等)和一组附加的全局权限。权限以一种易于理解的格式编写:“读取护士笔记”可能是一个医生角色可能拥有的权限,但护士角色可能没有“读取医生笔记”权限。

这种格式可能易于理解,但在幕后,它需要相当多的繁重工作才能实际检查这些类型的权限。检查当前提供者拥有的角色名称与其权限列表,以匹配他们尝试执行的操作。例如,一个试图阅读医生笔记的提供者会导致针对医生编写的每张笔记检查“读取医生笔记”。

另一个问题是依赖英语进行权限定义。任何使用OSCAR的人,无论使用什么语言,都需要使用“读取[角色]笔记”之类的格式编写他们的权限,使用英语单词“读取”、“写入”、“笔记”等等。

CAISI的权限模型是OSCAR的重要组成部分,但它不是唯一存在的模型。在实施CAISI之前,开发了另一个基于角色(但不是基于项目)的系统,并且该系统至今仍在系统中的许多部分使用。

对于第二个系统,提供者被分配一个或多个角色(例如,“医生”、“护士”、“管理员”等等)。他们可以根据需要被分配任意多个角色——角色的权限相互叠加。这些权限通常用于限制对系统部分的访问,而不是CAISI的权限,CAISI的权限限制对患者图表上某些数据片的访问。例如,用户必须在分配给他们的角色上具有“_admin”的“读取”权限才能访问管理面板。但是,具有“读取”权限将使他们无法执行管理任务。为此,他们还需要“写入”权限。

这两个系统都实现了大致相同的功能;由于CAISI在项目生命周期的后期合并,它们才同时存在。它们并不总是和谐共处,因此在现实中,仅仅专注于在OSCAR的日常运营中使用其中一个会容易得多。通常,您可以通过了解哪个权限模型先于哪个权限模型来确定OSCAR中的代码日期:提供者类型,然后是提供者角色,然后是CAISI 项目/角色

最古老的权限模型类型“提供者类型”已经过时,以至于实际上在系统的大多数部分中都没有使用,并且在创建新提供者时实际上默认为“医生”——将其设置为任何其他值(例如“接待员”)会导致整个系统出现重大问题。通过提供者角色来控制权限会更容易、更细粒度。

16.6. 集成器

OSCAR的集成器组件是一个独立的Web应用程序,独立的OSCAR实例使用它通过安全链接交换患者、项目和提供者信息。它可以作为组件可选地安装在LHN(本地卫生网络)或医院等环境中的安装中。描述集成器的最简单方法是将其视为一个临时存储设施。

考虑以下用例和使用集成器的论据:在X医院,有一个耳鼻喉科诊所和一个内分泌科诊所。如果耳鼻喉科医生将患者转诊给楼上的内分泌科医生,他们可能需要发送患者病史和记录。这很不方便,并且会产生不必要的纸张——也许患者只看一次内分泌科医生。通过使用集成器,可以从内分泌科医生的EMR中访问患者的数据,并且可以在访问后撤销对患者图表内容的访问权限。

一个更极端的例子:如果一名昏迷的病人出现在急诊室,只有他的健康卡,由于家庭诊所和医院的系统通过集成器连接,该病人的记录可以被提取,并且很快就能发现他被开了抗凝剂华法林。最终,像 OSCAR 这样的 EMR 与集成器相结合能够实现这样的信息检索。

技术细节

集成器仅以源代码形式提供,需要用户手动获取和构建。与 OSCAR 一样,它运行在 Tomcat 和 MySQL 的标准安装环境中。

当访问集成器所在的 URL 时,它似乎没有显示任何有用的内容。这个组件几乎纯粹是一个 Web 服务;OSCAR 通过 POST 和 GET 请求与集成器 URL 进行通信。

作为独立开发的项目(最初作为 CAISI 项目的一部分),集成器在严格遵守 MVC 设计模式方面非常严格。最初的开发人员在模型、视图和控制器之间设置了清晰的界限,做出了非常棒的工作。我之前提到的最新实现的数据库访问层类型——通用 JPA——是项目中唯一的这种层。(有趣的是:由于整个项目在所有模型类上都正确地设置了 JPA 注释,因此在构建时会创建一个 SQL 脚本,可以用来初始化数据库结构;因此,集成器不包含独立的 SQL 脚本。)

通信通过 Web 服务调用来处理,这些调用在服务器上提供的 WSDL XML 文件中描述。客户端可以查询集成器以了解有哪些功能可用并进行适应。这实际上意味着集成器与任何类型的 EMR 兼容,只要有人决定为其编写客户端;数据格式足够通用,可以轻松映射到本地类型。

但是,为了简单起见,OSCAR 构建了一个客户端库并将其包含在主源代码树中。该库只需要在集成器上出现新功能时更新。集成器的错误修复不需要更新该文件。

设计

集成器的数据来自所有连接的 EMR,在预定的时间段内,另一个 EMR 可以请求这些数据。但是,集成器上的任何数据都不会永久存储——它的数据库可以被删除,并且可以从客户端数据重建。

发送的数据集是在连接到特定集成器的每个 OSCAR 实例上单独配置的,并且除了必须将整个病人数据库发送到集成器服务器的情况外,只发送自上次推送至服务器后被查看的病人记录。这个过程并不完全像 delta patching,但很接近。

图 16.1:OSCAR 与集成器之间的數據交換

让我详细介绍一下集成器如何与一个示例一起工作:一家远程诊所查看另一家诊所的病人。当这家诊所想要访问该病人的记录时,这两家诊所必须首先连接到同一个集成器服务器。接待员可以在集成器上搜索远程病人(按姓名和可选的出生日期或性别),并在服务器上找到他们的记录。他们启动了一套有限的病人人口统计信息的复制,然后与病人再次核实,以确保他们同意通过填写同意书来检索他们的记录。完成后,集成器服务器将提供集成器知道的关于该病人的所有信息——笔记、处方、过敏症、疫苗接种、文件等。这些数据被缓存在本地,这样本地 OSCAR 不必每次想要查看这些数据时都向集成器发送请求,但是本地缓存每小时过期一次。

图 16.2:人口统计信息和相关数据在家庭诊所进行数据推送时被发送到集成器。集成器上的记录可能不代表家庭诊所的完整记录,因为 OSCAR 可以选择不发送所有病人数据。

在通过将病人的人口统计数据复制到本地 OSCAR 来初始设置远程病人后,该病人就被设置为系统中的其他病人。从集成器检索到的所有远程数据都被标记为远程数据(并记录了数据的来源诊所),但它只在本地 OSCAR 上临时缓存。任何记录的本地数据都与其他病人数据一样被记录——记录到病人记录中并发送到集成器——但不会永久存储在任何远程机器上。

图 16.3:一个远程 OSCAR 通过请求特定的病人记录来从集成器请求数据。集成器服务器只发送人口统计信息,这些信息永久存储在远程 OSCAR 上。

这具有非常重要的意义,特别是对于病人同意以及同意如何影响集成器的设计。假设一个病人去看一位远程医生,并且同意他们可以临时访问他们的记录。在他们访问结束后,他们可以撤回允许这家诊所查看该病人记录的同意,下次这家诊所打开该病人的图表时,那里将不会有任何数据(除了任何本地记录的数据)。最终,这使病人能够直接控制如何以及何时查看记录,这类似于走进诊所携带你的纸质图表副本。他们可以查看图表,同时与你互动,但当你离开时,你会把图表带回家。

图 16.4:一家远程诊所可以通过请求数据来查看病人图表的內容;如果存在适当的同意,则发送数据。数据永远不会永久存储在远程 OSCAR 上。

另一个非常重要的功能是,医生可以决定他们想通过集成器服务器与其他连接的诊所共享哪些类型的数据。诊所可以选择共享人口统计记录的所有内容或只共享部分内容,例如笔记但不包括文档、过敏症但不包括处方等等。最终,由建立集成器服务器的一组医生来决定他们愿意彼此共享哪些类型的数据。

正如我之前提到的,集成器只是一个临时的存储仓库,任何数据都不会被永久存储在那里。这是在开发过程中做出的另一个非常重要的决定;它允许诊所非常轻松地退出通过集成器共享任何和所有数据——实际上,如果需要,整个集成器数据库可以被清除。如果数据库被清除,客户端的用户将永远不会注意到,因为数据将从所有连接的客户端上的原始数据准确地重建。这意味着 OSCAR 提供者需要相信集成器提供者在他们说清除数据库时真的清除了数据库——因此最好将集成器部署到已经属于合法组织(例如家庭健康组织或家庭健康团队)的一组医生那里;集成器服务器将被安置在这些医生诊所中的一个。

数据格式

集成器的客户端库是通过 wsdl2java 构建的,该库创建一组类来表示 Web 服务通信的适当数据类型。每个数据类型都有类,以及表示每个数据类型密钥的类。

描述如何构建集成器的客户端库超出了本章的范围。重要的是,一旦构建了库,它必须与 OSCAR 中的其余 JAR 一起包含。这个 JAR 包含了设置集成器连接和访问集成器服务器将返回给 OSCAR 的所有数据类型所需的一切,例如 CachedDemographic、CachedDemographicNote 和 CachedProvider 等等。除了返回的数据类型外,还有一些“WS”类用于首先检索这些数据列表——最常用的类是 DemographicWs。

处理集成器数据有时可能有点棘手。OSCAR 没有真正内置的任何东西来处理这种数据,因此通常发生的情况是,当检索特定类型的病人数据(例如,病人图表的笔记)时,会要求集成器客户端从服务器检索数据。然后将这些数据手动转换为表示该数据的本地类(对于笔记来说,它是 CaseManagementNote)。一个布尔标志在类内部设置,表示它是远程内容,用于改变数据在屏幕上显示给用户的方式。在另一端,CaisiIntegratorUpdateTask 处理将本地 OSCAR 数据转换为集成器的格式,然后将这些数据发送到集成器服务器。

这种设计可能不是最有效率或最干净的,但它确实使系统的旧部分能够在不进行太多修改的情况下与集成器提供的数据“兼容”。此外,通过只引用一种类型的类来保持视图尽可能简单,这提高了 JSP 文件的可读性,并且在发生错误时更易于调试。

16.7. 教训

正如你可能想象的那样,OSCAR 在整体设计方面存在一些问题。但是,它提供了一个完整的功能集,大多数用户不会发现任何问题。这最终是该项目的目标:提供一个在大多数情况下都能工作的良好解决方案。

我不能代表整个 OSCAR 社区发言,因此本节将高度主观,来自我的观点。我觉得从关于该项目的架构讨论中有一些重要的收获。

首先,很明显,过去糟糕的源代码控制导致系统的架构在部分地方变得非常混乱,特别是在控制器和视图混合在一起的地方。过去项目运行的方式没有阻止这种情况发生,但这个过程已经改变,希望我们不再需要处理这样的问题。

其次,由于该项目非常老,很难升级(甚至更改)库,而不会对整个代码库造成重大破坏。这正是发生的事情。当我在库文件夹中查找时,我经常发现很难弄清楚什么必要,什么不必要。除此之外,有时当库进行重大升级时,它们会破坏向后兼容性(更改包名称是一种常见的错误)。OSCAR 中经常包含几个库,它们都完成相同的工作——这可以追溯到糟糕的源代码控制,但也与没有一个列表或文档来描述哪个组件需要哪个库有关。

此外,OSCAR 在为现有子系统添加新功能方面有点不灵活。例如,如果你想在电子图表中添加一个新框,你将不得不创建一个新的 JSP 页面和一个新的 servlet,修改电子图表的布局(在几个地方),并修改应用程序的配置文件,以便你的 servlet 可以加载。

接下来,由于缺乏文档,有时几乎不可能弄清楚系统的一部分是如何工作的——最初的贡献者可能不再参与这个项目了——而且通常你唯一可以用来弄清楚它的工具是调试器。作为这样一个年代久远的项目,这正在让社区失去新贡献者参与的潜力。但是,作为一个协作努力,社区正在努力解决这个问题。

最后,OSCAR 是一个医疗信息存储库,其安全性因包含 DBHandler 类(在上一节中讨论)而受到威胁。我个人认为,在 EMR 中,接受参数的自由格式数据库查询永远不应该被接受,因为很容易执行 SQL 注入攻击。虽然禁止使用该类的任何新代码,但开发团队应该优先考虑消除所有使用该类的情况。

所有这些听起来可能像是对该项目的严厉批评。在过去,所有这些问题都非常严重,正如我所说,由于进入门槛很高,阻止了社区的增长。这种情况正在改变,因此在未来,这些问题将不再是阻碍。

回顾项目的历史(尤其是过去几个版本),我们可以为应用程序的构建方式设计一个更好的方案。该系统仍然必须提供基本的功能级别(由安大略省政府为获得 EMR 认证而强制执行),因此所有这些功能都必须默认包含在内。但是,如果 OSCAR 要在今天重新设计,它应该采用真正的模块化设计,允许模块作为插件处理;如果你不喜欢默认的电子表格模块,你可以编写自己的模块(或者甚至完全使用另一个模块)。它应该能够与更多系统进行通信(或者更多系统应该能够与它进行通信),包括医疗硬件,这种硬件在整个行业中越来越多地使用,例如测量视力的设备。这也意味着可以轻松地将 OSCAR 适应世界各地地方和联邦政府关于存储医疗数据的要求。由于每个地区都有不同的法律和要求,这种设计对于确保 OSCAR 发展全球用户群至关重要。

我还认为,安全性应该是最重要的功能。EMR 的安全性仅与其最不安全的组件一样安全,因此应该重点关注尽可能从应用程序中抽象出数据访问,以便它通过经过第三方审核并被认为足以存储医疗信息的**主数据访问层 API** 在沙盒式环境中存储和检索数据。其他 EMR 可以通过模糊和专有代码作为安全措施(这实际上不是安全措施),但作为开源软件,OSCAR 应该引领潮流,提供更好的数据保护。

我坚定地相信 OSCAR 项目。我们知道有数百名用户(以及我们不知道的数百名用户),我们每天都会从与我们的项目互动的医生那里收到宝贵的反馈。通过开发新的流程和新功能,我们希望扩大安装基础并支持来自其他地区的用户。我们的目标是确保我们交付的东西能够改善使用 OSCAR 的医生的生活以及他们的患者的生活,通过创建更好的工具来帮助管理医疗保健。