抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

使用 MDSD 开发安全可靠的软件

2010 年 6 月 12 日

我们何时可以认为软件产品被真正地完成了呢?通常情况下,当我们不再为其提供后续支持,或者该产品已经被其它产品替代的时候,它的生命就终结了,几乎所有软件产品都会经历从开始到结束的演化过程。但存在了很长时间的大型企业级系统却会随着时间的推移,向不可维护、变僵硬的趋势发展。这导致了软件开发的停滞,使得响应客户需求的时间变长。

本文说明了如何使用 MDSD 方法来决这些问题。文章首先介绍了我们要解决的问题——向后兼容性问题和升级问题,说明了为什么这些问题很难解决,然后描述了在当前的软件体系结构中容易隐藏非功能性关注面的地方。

文章中举了三个例子,用来说明 MDSD 技术如何帮助我们在不损失灵活性的前提下,解决软件生命周期问题。这些例子都是来自于电子保健行业中的真实敏捷项目,作者还说明了其中的最佳实践可以怎样应用于其他情况下。

项目中得到的经验教训被总结为拇指规则(在面对复杂环境时候采用的一种简化或经验性的处理方式,因为理性的处理能力是有限的),在文章的最后,引用了一些有用的框架与工具,它们是目前解决方案的基础。

介绍

在软件工程领域,经过几年的实践,模型驱动的软件开发(MDSD)已经证明了它并非是昙花一现。如今,MDSD 的很多承诺 [1] 已经变成现实,大量成功的例子也持续涌现出来。

本文说明了可以如何使用模型驱动方法来解决当前软件系统中的一些问题。牢记 MDSD 最佳实践 [2],我们将关注软件生命周期本身这个非功能性需求。

软件生命周期

几乎所有的现代软件系统都面临着生命周期问题。当产品发布或是部署时,你不得不解决向后兼容以及迁移策略等问题。我们通常会低估产品兼容性问题,或是在开发中完全忽略这个问题。当我们不得不花费很多资源来解决产品兼容性问题时,才会后悔莫及。严肃地看待这个问题会影响到软件产品的演进,正如我们将在下面这个例子中所看到的。

案例:java.lang.Cloneable

Java 中的 _java.lang.Cloneable_ 接口说明了向后兼容性是如何限制软件演化的。该接口在 JDK 1.0 时被引入,仅作为一个标记,并不提供任何接口方法,想要支持克隆的类型都需要实现这个接口。

此外,它还需要提供 _java.lang.Object.clone()_ 方法合适的实现。其实 _clone()_ 方法如果定义在 _java.lang.Cloneable_ 接口中似乎更有说服力,并且更自然。但事实上该接口从未改变过,因为那将会破坏向后兼容性。引入克隆接口方法会导致对所有已实现该接口的类的编译错误。关于这个 bug 的完整历史,请参看 [3],对于 _java.lang.Cloneable_ 更详细的讨论,请参看 [4]。

在分析我们系统中已经存在的生存周期问题细节以前,非常有必要建立一个公共术语表。

向后兼容性

如果出现以下情况,那么可以认为该系统是向后兼容的:

  • 它能处理以前系统的接口
  • 它能处理以前系统的数据
  • 该系统的使用者不必关心系统版本改变

系统的修改可能引发向后不兼容的问题。通过引入兼容性层可以解决这个问题,该层负责提供以前系统接口的视图。在这样的环境中,权衡并设计进 - 出良好的接口更加重要。

关于二进制与源代码的兼容性在本文中不做讨论,[5] 中描述了它们和 Java 语言之间的差异。

更新

更新描述的是对已有软件的改进。通过一次更新,应用的特性集是不会被扩展的。更新的主要目的在于提供额外的健壮性(例如修复安全隐患)。更新不会涉及到接口与数据描述的改变,它不会引发向后兼容性问题。OSGi 版本控制体系 [6] 定义了版本分类模式 _major.minor.micro_。一次更新只增加第三位,即 micro 位的版本号。

升级

我们认为一个新版本,或对已有软件增加功能都是一次升级。与更新不同的是,升级提供了新的特性集。在这个过程中兼容性问题或多或少都会暴露出来,这取决于新老版本之间的差异大小。让我们再来看一下 OSGi 版本控制体系,其中的 minor 版本号的增加描述了一次向后兼容的升级。Major 发布号的增加则意味着一次向后不兼容的升级。

迁移

迁移是将软件系统从一个操作环境移动到另一个操作环境的过程。对于软件系统而言,迁移一般是将软件系统移动到一个更新的版本(忽略了将现有软件产品替换为竞争产品的迁移场景)。如果这个新版本承诺了对老版本是向后兼容的,那么对于消费者而言迁移过程是不需要任何额外操作的。另一种情况就是消费者需要为迁移做出附加操作以适应新版本。对于此种情况,向后不兼容将影响该产品的所有客户。越多客户使用该产品,对于迁移带来的兼容变更过程就越痛苦。随着产品相关客户的增加,向后不兼容变更的成本与后续迁移过程代价会变得非常高。因此,对于向后兼容性问题的正确考虑对软件系统演化起着至关重要的作用。

MSSD,盛装的骑士

如果您已经在使用模型驱动的方法进行产品开发,那么在这里你会找到很多能够减少我们前面提到的冲突的方法。我们的主要目的就是为了让客户满意,并且不受系统变更的影响,同时还可以让产品继续发展,而不会感到在你的开发瓶颈处有里程碑的存在。

在每个现代应用中,我们都可以识别出一些潜在的向后不兼容的情况。有了 MDSD 方法,我们就拥有了消除这些阻力的工具。以下的三个示例说明了在当前的软件体系结构中,生存周期方面的问题隐藏在何处,以及如何使用模型驱动方法来让软件系统的兼容性处于控制之下。

API 的不兼容性

每个暴露给外部的 API 都声明了提供者与其编程接口的潜在消费者之间的契约。使用新的不兼容的 API 替换现存的 API 将会导致正在使用旧 API 的客户端死锁。这不仅只是针对类似 Java 的普通编程语言,对于所有暴露接口的形式(例如 Web 服务)都一样。

API 的不兼容性可以通过一个专门的向后兼容层来解决。当软件系统无法支持以前版本 API,而只是暴露最新版本的 API 时,我们可以在目标系统(客户端系统)前部署向后兼容层,它会提供当前软件系统中所有版本的 API。对于这个向后兼容层到底是以中间件 ESB(企业服务总线)的形式部署还是直接部署于目标系统中我们不做深究。向后兼容层在此扮演了一个 API 门面的角色,它在各 API 版本间进行消息转换 [7]。有了这个架构,我们就需要制定消息转换的规则。

对于每个不兼容的 API 都必须定义对应的消息转换规则,这是 MDSD 的关键所在。在重要项目中,我们应该将定义消息转换规则视作正常处理而非异常处理。因此,编写转换代码是经常性的并且是非常关键的任务。如果遗漏了一项转换,那么就会导致无法使用以前对应版本的 API 接口。如果遵循模型驱动方法,API 接口应该定义为一个模型。对于软件系统的每个版本都对应一个模型。当比较不同版本的 API 接口时,可以通过对比它们对应的模型实例看出差异。由此您可以得到所谓的系统差异模型,其包含了两个系统版本间的所有差异。以该差异模型作为输入,可以生成一份可读的变更报告,该报告描述了两个系统版本的细节差异。以该报告作为检查清单,您可以控制系统发生的所有改变。而不会再忽略了任何一个变更或是遗漏了编写该变更对应的转换代码。对于一组接口的变更处理和上面是类似的。有了差异模型,您可以更顺利地处理后续项目任务。另外,对于转换通常是给转换数据添加一个必须的类型字段,以标识出该消息需要转换成的类型。对于同一类消息的转换而言,这项工作是非常乏味、重复的,最好将其自动化。在没有模型驱动支持时,开发者不得不手工编写所有转换代码,但当我们拥有整个变更信息时,通过模型驱动的方式,可以以某个确定的标识模式生成消息转换代码。这可以将开发人员从重复、易出错的任务中解放出来,将他们的时间投在其他更值得去做、更有挑战性的任务上。当然,模型驱动的方法也是有局限性的。100% 生成转换代码只是一种乌托邦式的期待。在模型中总有一些复杂变更不能标识到模式目录。对于这样的情况,转换只能手工编写。因此,我们需要时刻谨记区分自动生成与手工编写变换代码的最佳实践 [2]。说到这里,您可能会问“为什么我们要付出如此多的努力在向后兼容上?”,这个问题问到点上了,而且必须针对每一种不同产品的具体情况来回答。对于历史遗留系统,您没有选择,必须保持向后兼容。这点在电子保健领域印证无疑。有很多医疗机构需要与中心外部系统保持通信,而这些中心系统一般都非常古老,并且希望有稳定的接口与其通信。改变目标接口,那么每个医疗机构都必须做出相应的修改。投入一些时间来解决目标端的不兼容性冲突比想办法移除所有异构遗留环境更为合理。

数据库模式出轨

通过使用数据关系映射工具(例如 Hibernate)对数据库进行抽象既是一种赐福也是一种诅咒。一方面,它为开发者提供了想要的数据库技术抽象;另一方面也隐藏了对不同版本间数据库模式的出轨(不匹配)。对于每一次持久化模型的变更都将重新处理已部署应用的数据库模式。例如给领域对象添加一个新的持久化字段就需要给对应的数据库表添加一个新的列。如果持久化模型与数据库模式不同步,那我们就认为数据库模式出轨了。

> 而 MDSD 方法保证了数据库模式不会出轨。如果我们以持久化模型作为主模型,根据该模型生成需要的数据库模式,那么我们就能够重用模型信息以处理兼容性问题。假设我们有不同的模型版本,我们就可以生成差异模型,差异模型会为我们提供非常有用的不同版本差异的信息。就像前面那样,我们可以生成一份变更报告,来查看变更影响。另外,我们还可以基于差异模型生成 SQL 升级脚本。我们不打算 100% 生成脚本,那不现实。对于复杂数据库模式情况,生成的脚本总是需要手动添加一些代码的。自动生成部分与手工编写部分的组合定义了数据库模式从一个旧的版本升级到一个新的版本。由于存在各式各样的 SQL 方言,所以提供已支持数据库的对应升级脚本是很重要的。如果系统支持多个数据库,那么这些脚本必须遵从对应方言。对于 Ruby 迁移 [8] 而言,Ruby 社区提供了不依赖数据库(database-agnostic)的 DSL 来描述数据库模式变更。这些数据库模式变更描述将被翻译成不同方言的 SQL 语句。与直接生成 SQL 相比,模型驱动方法能够将差异模型变换成 Ruby 迁移表达。这样一来,生成 SQL 语句的责任就转移到了 Ruby 迁移。随着 MDSD 提供的支持,对于领域模型变更带来的恐惧消失殆尽。开发者可以不再犹豫对领域模型的演进,项目重获敏捷。

语言变更

当使用模型驱动的技术时,我们经常使用一些领域特定语言(DSLs)来描述我们的模型。这些领域特定语言非常适合描述目标领域,因为它们为我们提供了对所需要的领域特定的表达与抽象。与一些通用意图语言(general purpose languages,GPLs)相比,DSLs 不会像 GPLs 那样死板,并且不受语言变更约束。随着 DSL 描述的领域与元模型的演化,一些新增的概念与结构体(constructs)将被添加或替换,语言本身也在演化。不幸的是,领域特定语言本身对于不兼容性是没有任何免疫力的。

而对于潜在的冲突还是有办法解决的。我们可以使用文献 [9] 中提到的反腐化层(Anticorruption Layer)模式处理这类冲突。就像其名字一样,该模式可以解决腐化问题。在一个语言变更场景中,腐化发生于我们变更语言元模型的地方。对于新版本元模型而言,所有满足旧的元模型的模型都腐化了。遗留的元模型不再被系统支持,并且客户端将被强制迁移到基于新元模型的语言上。通过特定的反腐化层,我们既能保护旧的元模型,也能让元模型朝着更合理的方向演化。反腐化层是一系列门面、适配器以及描述不同模型变换的变换器的组合。客户端对于元模型可见的同时,我们也维护这一个内部元模型。该内部元模型作为主元模型将被用于未来的模型处理。所有的外部模型实例将被变换成对应内部元模型的模型实例。外部元模型提供了目标领域的特定视图。通过反腐化层,我们可以将多个视图正确映射到一个通用的元模型上。此时,不同版本变换可以在反腐化层内部被清晰地分离出来,元模型过时策略也可以被更容易地实现。

这里有个简单的例子展示了反腐化层所带来的好处:假设您用自己的 DSL 管理您全世界的豪华汽车车库容量。在版本 1 中,您为每一辆车指定了宽度、高度以及长度。几个月后,你发现您的车库里只停了三种尺寸的车辆:S、M 以及 L,因此,新版本的仓库尺寸描述替代了三维描述。您没有将新版本尺寸描述马上上线,而是实现了一个反腐化层,该层仍然理解原来版本 1 的三维元模型,只是在内部将其变换为版本 2 的尺寸元模型。

拇指规则

就像您在示例中看到那样,我们经常面对各种不兼容性问题。当结合 MDSD 处理兼容性问题时,本文得出如下规则:

向后兼容性是用户友好的

流行的顶尖应用将带来更多的客户。委托客户增加时,您必须应对随之而来的更多的需求。因此,成功的应用将进一步开发后续版本。以往已经有足够多的失望之极的客户与不兼容性作斗争的反面示例,所以解决兼容性问题将得到客户非常高的满意度评价,可以将客户牢牢拉拢在您的解决方案上。满意的客户是长期系统运维的关键动力。

向后兼容性是昂贵的

兼容性不是免费的。生存周期关注面犹如一个沉睡的巨人,架构师通常不会去唤醒它们。一旦它们被唤醒,特别是在开发后期,我们需要花费巨大的代价来控制它。通过模型驱动技术,兼容性关注面可以在一开始就被评估出并可以进行正确的整合处理。因此,兼容性问题不再限制了成功产品的演化。使用增量式迁移能够保持低成本、可管理的复杂度:只提供直线迁移路径替代提供所有支持版本迁移路径。迁移链完成后,您一样可以达到同样的迁移目标。

不兼容性不可避免

兼容性问题在所有的软件产品中都需要考虑。对于内部产品而言,使用范围与兼容性可以被控制,而其他产品则需要避免发生兼容性问题。项目需要有着保持向后兼容的能力以及可以朝着向需要方向进行合并的生命力。只有这样才能让产品长期存活。因此,不要等兼容性问题发生时才后悔,对于公开开发的产品更是如此。事实上,开源开发者从不知道他们的产品将被如何使用,在何地使用。

系统一旦发布,就成了遗留系统

一次发布版本号描述了系统生命中的一个快照。一旦发布了确定的版本,开发其实早已进行多时。因此,该发布描述了过去的开发状态。当产品发布后,您不得不为了赢得客户满意而为其提供支持。

模型差异也是模型

前两个示例中强调,模型差异描述了一些有价值的、可在后续开发中利用的信息。在运用模型驱动方法后,获取不同模型版本的差异模型是非常廉价的。已有工具链需要被扩展,以产生并处理差异模型。一些环境(例如版本控制系统)对于维护已有模型版本是非常有用的。

反腐化层是一种最佳实践

反腐化层给我们带来一种处理兼容性与弹性的有力模式。当内部与外部联系非常紧密时,它清晰地分离了内部与外部表示。它所引入到已有工具链中的部分是非常小的,并且可以在稍晚时候引入。一旦整合完毕,它允许所有终端独立演化。

有用的框架与工具

Java 中的 MDSD 框架有:openArchitectureWare(oAW)[10],它是一个“构建 MDSD/MDA 工具的工具”。它是 Eclipse 模型项目(EMP)的一个子项目,提供了健壮的模型驱动开发解决方案。在 EMP 中,Eclipse 建模框架(EMF)[11] 作为主要的构建工具提供了顶尖的结构化数据模型处理。因为 oAW 非常多地使用了 EMF 技术,所以我们建议两者结合使用。EMF Compare[12] 是一个用于比较 EMF 模型的工具,看上去它非常适合我们创建有价值的差异模型。

关于作者

Andreas Kaltenbach 是一名来自 InterComponentWare AG(ICW)[13] 的软件开发者与培训者,他是德国保健相关应用的专家。他围绕 ICW 的电子保健框架(eHF),关注 MDSD 方法以及安全学。另外,他还是 ICW 的开放电子保健基金会 [14](关注于建立保健相关行业的开源社区)的开发者代表。

链接与文献

[1] VÖlter, Stahl: Model-Driven Software Development (2006)

[2] Efftinge, Friese, K??hnlein: Best Practices for Model-Driven Software Development (2008) http://www.infoq.com/articles/model-driven-dev-best-practices

[3] Cloneable doesn’t define .clone, http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4098033

[4] Bloch: Effective Java: A Programming Language Guide (2008), item 11

[5] Gosling et al.: The Java Language Specification, Third Edition (2005), chapter 13

[6] OSGi Alliance: OSGi Service Platform Core Specification (2007), chapter 3.6.2

[7] Hohpe: Enterprise Integration Patterns (2003), chapter 8

[8] Understanding Migrations, http://wiki.rubyonrails.org/rails/pages/understandingmigrations

[9] Eric Evans: Domain-Driven Design: Tackling Complexity in the Heart of Software (2003), chapter 14

[10] openArchitectureWare, http://openarchitectureware.com/

[11] Eclipse Modeling Framework, http://www.eclipse.org/modeling/emf/

[12] EMF Compare, http://wiki.eclipse.org/index.php/EMF_Compare

[13] InterComponentWare AG, http://www.icw-global.com/

[14] Open eHealth Foundation, http://www.openehealth.org

查看英文原文: Staying Safe and Sound Thanks to MDSD


感谢侯伯薇对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010 年 6 月 12 日 21:391883

评论

发布
暂无评论
发现更多内容

ARTS 挑战打卡第九周(200706-200712)

老胡爱分享

ARTS 打卡计划

融云 X- Meetup 技术沙龙广州站:全球通信云技术实践分享

InfoQ_967a83c6d0d7

LeetCode题解:26. 删除排序数组中的重复项,双指针,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

week 11学习总结

Geek_2e7dd7

完了,这个硬件成精了,它竟然绕过了 CPU

简爱W

小米的护城河

石云升

小米 护城河

马方业:区块链就是新未来 区块链就是新财富

CECBC区块链专委会

区块链 新未来 新财富

介绍一款API敏捷开发工具

棒锤🐮

敏捷开发 Rocket API API敏捷开发

Redis 之父关于 CRC64 的神秘往事!

yes

redis CRC

十一周作业

olderwei

极客大学架构师训练营

战斗还是逃避,或许可以考虑一下合作?

escray

学习 面试 面试现场

ARTS挑战打卡第五周(200608-200614)

老胡爱分享

ARTS 打卡计划

JeecgBoot手记

卧石漾溪

ARTS 挑战打卡第七周(200622-200628)

老胡爱分享

ARTS 打卡计划

前端分页组件实现逻辑

书旅

php 前端 分页

图解javascript——基础篇(以思维导图总结js中关键技术点,为面试及工作助力)

执鸢者

Java 前端

week 11

Geek_2e7dd7

Linux系统监控工具推荐

王坤祥

监控 工具软件

一个快捷方便的油煎鸡胸肉,懒人标配香喷喷好吃看得见

小霸王其乐无穷

美食 鸡胸肉 懒人

请不要随便修改基类

架构师修行之路

ARTS挑战打卡第八周(200629-200705)

老胡爱分享

ARTS 打卡计划

SQL查询语句执行顺序详解

书旅

MySQL SQL语法 sql查询

ARTS 打卡第四周(200601-200607)

老胡爱分享

ARTS 打卡计划

数据库是咋工作的?

简爱W

【解Bug之路】——Nginx 502 Bad Gateway

简爱W

ARTS挑战打卡第六周(200615-200621)

老胡爱分享

ARTS 打卡计划

用科学的方法理解每日优鲜

石云升

新零售 每日优鲜 多快好省 科学分析

视频码控:CBR、VBR和ABR

潇湘落木

直播 SRS 视频编码 码控

Spring Boot 集成 Sharding-JDBC + Mybatis-Plus 实现分库分表

简爱W

动态修改logback的日志级别

华宇法律科技

Java springboot logback

Docker搭建项目环境实战

书旅

Docker Dockerfile Docker-compose

Study Go: From Zero to Hero

Study Go: From Zero to Hero

使用MDSD开发安全可靠的软件-InfoQ