本文共 6767 字,大约阅读时间需要 22 分钟。
\\\本文要点
\\
- 重构有三个层次:代码层次微重构,模式重构,以及更深层的模型重构。\\t
- 无论是对系统还是你的理解来说,做许多小的变更可以形成复杂的大的变更。\\t
- 这里提出的案例是Nexia Home Automation的摄像机整合案例。重构之后,开发人员可以更方便地了解领域模型,以及系统中的Java和Ruby代码。\\t
- 重构加强了好的DDD实践,比如说强边界的上下文,以及跨边界的显式转换。\\t
- 使用功能开关和阶段性发布提供了一个推迟做出决策的选择,可以让你在掌握了足够的信息之后再做出明智的选择。\
本文改编自 2017的一个。
\\在化学的世界中,你可以提取不同的物质,每个物质本身都处在稳定的状态,然后你可以将它们组合起来,它们互相发生反应,成为比反应物加起来还要好的物质。相同的,在软件行业,也会有不同的重构反应物,每个都具有不同的工作量、频率和能力。当它们与领域驱动的探索发现过程催化剂相互碰撞的时候,这些重构反应物就会产生代码的“化学”反应,将代码转换为丰富的领域模型。
\\本文讲述了Nexia Home Intelligence中一个持续很久的摄像机支持系统的重构故事。Nexia是一个大规模Ruby on Rails应用程序,需要支持使用成千上万个摄像机的客户集群需求。
\\我将从三个层次介绍重构。Martin Fowler的《重构》一书中谈到了微重构,就是在代码级别不断进行小的变更,以实现增量提升。好的开发人员会花时间去记忆并养成如何使用重构工具的习惯,所以这些微重构就成为了第二天性。
\\Joshua Kerievsky在《重构与模式》一书中谈到了更高阶的模型,比如说策略模式。他在书中还定义了各种“坏味道”,比如霰弹式修改,进行一个小的变更也需要很多额外的其他变更。这让我很放心,因为我的设计没有必要一开始就完全正确,我随时可以开始开发,当碰到一种“坏味道”的时候,我就有了可以重构的工具,但也只在必要的时候进行重构。
\\我将介绍第三层次的重构,即重构到更深层次的模型,Eric Evans已经在《领域驱动设计》一书中为我们介绍过这一层次。当我第一次阅读这本书的时候,第三部分吸引了我的注意力。在第三部分中,他谈到了一个项目,在项目中模型并不适用,他们就提出了一个新的重构方式模型,它彻底改变了项目。
\\看一下这三个层次,如果你可以在你的模型中引入新的概念,这就是一次有用的重构。你需要精通于微重构,充分使用模式来重构到更深层次的模型中去。
\\Nexia Home Automation系统是Ruby语言编写的,可以帮助你完成各种家庭自动化工作,比如了解窗户是打开还是关上的,需要系统与运动传感器集成,并连接摄像机。Dan Sharp和我负责摄像机系统,这也是我将介绍的内容。
\\家庭自动化不像银行业或保险业等其他领域,它是一个高科技领域,需要处理硬件和固件。这就意味着客户并不了解很多技术的问题,你不能直接问他们固件相关的问题。
\\我们的目标是不断研究新功能,同时提升对新的摄像机的支持。当新的摄像机面世后,通常需要几周或几个月的时间,通过大量霰弹式修改,才能添加Nexia对它的支持。我们希望能大大缩短这个时间。
\\如果你要完成向Nexia添加摄像机的过程设置,你会注意到它使用的一些术语。比如说,并不是添加摄像机,而是需要注册一个新的摄像机,之后进行激活步骤。注册步骤是想让Nexia知道相机的存在,需要在连接到Nexia之前完成。
\\安装在客户家庭的几千台摄像机和许多相机管理器组件通信。相机管理器是由Java编写的,通信是通过HTTP和SSL实现的。当消息从摄像机进入管理器之后,我们将这些消息放到Redis作业队列中。这些消息被Portal Workers从队列中去除,在后台中运行。Portal Workers是由Ruby编写的。Nexia需要回复摄像机,所以我们在RabbitMQ消息总线上将这些消息排队,这些消息会通过相机管理器处理。图1展示了这个架构很高层次的一个视图。
\\ 图1:Nexia架构\\这个应用程序本身是Rails应用程序,代码库的部分内容如图2所示。如果你不熟悉Rails开发,models文件夹通常不是控制器所在的地方,所以不要认为它是富领域模型。我特别展示了一些我提到的workers,以及和自动化相关的camera文件。比如在日落时执行一些工作,或是在指定时间调暗灯光,都是Nexia的自动化例子。
\\ 图2:Rails应用程序架构\\我们遇到的第一个挑战是代码很难推断。Java 相机管理器过度架构,它们使用了有许多抽象的元架构,让它可以和任何与Nexia连接的任何类型摄像机一起工作。实际上大多数摄像机都非常类似,比如说都使用SSL和HTTP,我们不需要额外的抽象层。
\\举一个系统性问题的例子,图3展示的是handleRequest()
方法的一部分。任何DDD从业者都会对这段代码的语言表达产生疑问。91行引入了Zombie一词,什么是Zombie?93行提到了“如果没有进行授权(isAuthorized
)”,但是94行的注释提到的行的注释提到的认证(authenticated
)和它并不是同一个东西。更糟糕的是,98行将一个变量声明为auth
,这既能代表前者,也能代表后者。虽然这仅仅是个小例子,但是这段代码也能代表我们相机管理器代码库中遇到的一些问题了。
handleRequest()
代码示例\\ 在Ruby端,网站工作人员对于摄像机的支持随着时间推移而增长。由于大多数工作都是由不同的开发人员(主要是外包)按照需要完成的,所以过多地倾向于职责实现了,而未进行有目的地建模。
\\Ruby代码的优点是十分简洁,可以在几行内表达很多内容。但是,CameraWorker
情况不同,它负责验证并关闭摄像机的链接。先声明一下,由于不能本文中展示超过130多行代码,因此图4中展示了部分代码。在多个地方,worker需要对摄像机对象进行状态修改,而不是声明所需的行为。我们还碰到了一些不太好的命名,比如89行的start_motion
调用,看上去像是开始行动的命令,但其实并不是。
CameraWorker
代码示例\\ 和那段Java代码类似,这只是其中的一个小片段,但它也可以代表系统性问题了。这些都造成了代码很难推算。
\\遇到的第二个挑战是相机管理器与设备管理器过于耦合了。想要理解这个问题,就得先了解一些架构的历史了。相机管理器(CM)是从通用设备管理器(DM)发展而来的,后者可以管理各种类型的设备。这就导致需要和其他Nexia的部分共享内核。这成为了一个重大的部署问题,这意味着基本上我们就不能升级Java了。最终,我们认识到这种耦合是没有必要的。尽管摄像机是个设备,但是它和其他设备没有很多类似之处,比如门锁等等。
\\第三个挑战真正涉及到了DDD,领域知识出现在错误的位置。大多数领域逻辑是在Java相机管理器代码之中,这就代表着增加新的功能会很复杂、耗时、容易发生错误以及很难测试。同时,修改代码代表着要对所有东西进行霰弹式修改。
\\我列出所有问题,不仅仅是吐槽糟糕的代码,而是要明确它们并不是不可克服的挑战。此外,DDD提供了可以在很大程度上改善这种情况的技术。首先回顾一下DDD的四大关注点。
\\首先,我们希望在代码中演进并表达深层的领域模型。第二,我们希望将代码重构为通用的语言,内容一致易于理解,代码意图清晰。第三,我们要清楚地描述模型和模块的边界和职责。很难在没有明确边界的情况下实现高内聚和松耦合。最后,我们需要严格遵守模型边界(即有界的上下文),跨边界时进行显式转换。
\\当你遇到这样的代码的时候你会怎么做?现在有一些选择,我知道现在一些人已经试过某些选择了,但并没有成功。一个选择是“清除积水”,尝试删除所有旧的代码,重新开始。人们可能需要空出几个礼拜进行重大重构,直到“修复”的时候再解脱出来。第二种选择是,自己不做任何处理。
\\我喜欢选择进行试验,看看你能做什么。我喜欢。这个团队进行了许多试验,找了很多方法,从小的地方开始改进,并做了许多改变。英国自行车队负责人Sir Dave Brailsford说:“我觉得我们应该从小处进行改变,通过小收益的累积,采取不断提升的哲学思想。抛弃完美,关注事情的进展,尝试各种改进。”对于敏捷软件开发人员来说,持续改进的思想并不陌生。
\\在我们的项目里,我们尝试了各种不同的事情,但大多数都没有用。在2014年3月,我们尝试了“一小步”,我们意识到摄像机的概念实际上需要完成两个不同的任务。它充当了物理设备,也叫实体。但同时它还需要作为命令处理程序,提供向物理设备发送命令和查询的接口。
\\首先看一下Ruby代码,我们发现这两个任务都在摄像机对象中。它是设备的子类,有许多问题。由于没有进一步的子类,所有的逻辑都在巨大的“上帝”对象摄像机中。
\\我们首先决定添加新的领域服务,而不是改变摄像机对象,这遵循了开放和修改的原则。这个新的Camera::CommandService
存放所有的摄像机指令和查询。由于我们将它作为扩展来写,我们可以用好的测试驱动开发和结对编程实践来实现它,在不破坏其他东西的情况下创造更高质量的设计工作。我们有一个很好的测试组件,它覆盖了controllers、 workers和collaborators,我们可以更放心地进行更新。这一小步实现了虽然小,但是不可忽视的改进。
Martin Fowler在其《修改代码的艺术》一书中聊到了寻找代码的接缝,也就是你可以加入新东西。我们召开了事件风暴会,了解设备注册Nexia的不同方法,这有助于可视化这些工作流中的相似之处。通过查看Java代码,我们发现相机管理器组件太过“智能”,它仅需要管理摄像机会话就可以了,但却做了太多其他事情。我们希望让相机管理器成为通用的http代理,由于所有指令都是HTTP调用,并整合Ruby的所有摄像机逻辑。
\\我们在接缝相机管理器加入了新的通用send_url()
指令。我们将http代理模型运用在连接管理、验证、摄像机到入口消息传递和日志记录上(来帮助故障排除以及未来计划)。在Ruby端,我们可以在前一年的进展上,使用Camera:CommandService
向摄像机发送任何指令。
在推行了一小步并发现新的接缝一年之后,我们可以将摄像机的领域逻辑从Java端迁移到Ruby端。我们将Camera::CommandService
指令(例如Pan-Tilt)迁移到使用通用的相机管理器接口。这个方法最棒的地方是不需要修改Java代码。作为Ruby端的内部重构,我们可以只使用一个指令进行测试,并迭代它直到可以运行。
我聊到这个故事的时候,通常会被问一个问题:“你怎么验证这些重构?”我指出,我们正在继续交付应用程序的功能,这是我们随时可以进行的工作。同时,这些小的步骤也获得了一些进展。由于我们为摄像机提供通用的URLs,可以从Ruby发送指令,因此我们可以在所有安装的相机上批量升级固件。此外,我们可以在Ruby端简单、快速地进行变更,这代表着我们可以进行试验,发现其他的边界改进。在这之前,需要Java和Ruby合作才能完成变更。
\\我想再次强调小进展的重要性。非技术利益相关者并不关心你是否重构代码。我相信一般来说,他们相信你是专业人士,会竭尽最大努力写可维护的代码。这代表着你必须建立信任和信誉,可以通过实现小的进展来完成这一点。
\\我们还发现,重构可以帮助清理代码。在Camera::CameraWorker
中就有三个验证。首先,在重新连接的时候验证已存在的摄像机。其次,处理新的摄像机的创建和验证。第三,去掉“僵尸”摄像机,就是已经连接但没有验证的摄像机。通过重构到更深层的模型,代码可以更容易地推断,正如图5中的8-14行所示。
在我们引入了更多的领域逻辑之后,Ruby代码占据了Nexia普遍用的语言的比重更高了。我们不需要给摄像机对象进行许多修改,并设置许多属性,我们发现工厂模式更加适合。普遍使用的语言包括心跳的概念,对于这个系统来说就是摄像机连接到Nexia,就像它们或者一样。之后我们创造了名为update_from_heartbeat
和create_from_heartbeat
的工厂方法,来分别处理现有的摄像机和新的摄像机。
Java端也得益于重构。较之前在图3中部分展示的handleRequest()
方法,变成了图6中的5行代码。对一些提取的方法进行重构,功能变得更加容易理解。
摄像机类很大,因此你经常会跑到这段代码里。小贴士,处理这种情况并不需要通过代码重构使它更加清晰。当代码杂乱不堪时,简单地重新排列一下代码会很有效,虽然它只是个简单的设计技巧。看一看模式,把类似的方法放在一起,这将缓解你在处理庞大代码域时的认知负担。
\\处理一个庞大、混乱的代码库就像雾中漫步,你不能看到周围的一切,你可能看到的只是一棵树,或是一座山。当你做了一些小的变更之后(如重组织代码,提取方法),这些小的收益会累积,雾开始消散。实现微重构和Fowler和Kerievsky提到的模式能产生累积的效果,因此可以对模型有更深层次的了解。
\\比如说,在Ruby端,我们发现我们正在向摄像机发送指令。所以我们按照Kerievsky的建议,使用命令模式,使之大大简化了。我们为指令设置了基类,以及标准的execute()
方法。之后我们创建了camera/command文件夹,开始写每个指令,实现这个基类。此外,我们还引入了功能开关,帮助旧代码继续执行,直到相应的指令已经转换。我强烈推荐使用功能开关来帮助你安全地进行重构。
我推荐的另一个方法是阶段性发布。我们希望避免每台摄像机突然断开连接的情况,大多数现有的客户群的产品都有共同的目标,就是不要影响到所有客户。在第一个月我们仅仅部署到Nexia IP地址,让QA、开发人员和支持人员在部署到客户之前先尝试新系统。第二个月我们加大部署,部署到一部分客户,但只使用一个生产服务器。直到第三个月我们才会部署到所有的摄像机、所有的客户和所有的生产服务器上。这比我职业生涯中参与的其他生产环境发布都要顺利。
\\功能开关和阶段性发布的另一个好处是它们提供了有价值的选择。我推荐一书,介绍了实际选择权的概念。通常,当某人在会议中说“我们需要作出决定”的时候,就会有两个选择,一个是根据有限的知识作出选择,要么不做选择。实际选择权指出还有第三个选项,在我们更好地理解之前,战略性地推迟决策。功能开关和阶段性发布都可以帮助你推迟做出决定,直到你可以做出更好的决定为止,这非常关键。
\\回看一开始的时候,我们已经获得了很大的成就。之前,添加新的摄像机需要几周甚至几个月。现在,我们可以在几小时内添加新的摄像机。我们不再需要在Java和Ruby中都作出变更,并保持代码的同步,我们只需要在Ruby中进行修改。尽管旧代码在一些方面不一致,但新的代码更加内聚,也很容易推断,因为上下文很清晰。我们移除了相机管理器和设备管理器之间粗糙的依赖,所以我们可以更新Java了。
\\根据这些经验,有一些通用的重构技巧。不要只选择一个变更的实现方法,比如说命名新的东西,尝试至少三种语言和/或模型选项。在你的日常工作中,同样要注意一些小的收益。我们往往太过于高估大变更的效果,却低估了小的累积的变化的力量。
\\Paul Rayner 是开发者、教练、导师、培训师以及国际流行会议讲师。他在各种行业拥有超过25年的软件开发经验,他是经验丰富的软件设计教练和领导力导师,帮助团队点亮他们的设计技巧。他的咨询公司为敏捷团队提供软件设计的指导和培训。Rayner生在澳大利亚珀斯,他在科罗拉多丹佛和妻子及两个孩子生活、工作和玩耍。他在推特上用澳大利亚英语发表推文,并在发布博客。
\\查看英文原文:
\\感谢对本文的审校。
转载地址:http://bjbvx.baihongyu.com/