内容简介:本文改编自在化学的世界中,你可以提取不同的物质,每个物质本身都处在稳定的状态,然后你可以将它们组合起来,它们互相发生反应,成为比反应物加起来还要好的物质。相同的,在软件行业,也会有不同的重构反应物,每个都具有不同的工作量、频率和能力。当它们与领域驱动的探索发现过程催化剂相互碰撞的时候,这些重构反应物就会产生代码的“化学”反应,将代码转换为丰富的领域模型。本文讲述了Nexia Home Intelligence中一个持续很久的摄像机支持系统的重构故事。Nexia是一个大规模Ruby on Rails应用程序
本文要点
本文改编自 Explore DDD 2017的一个 演说 。
在化学的世界中,你可以提取不同的物质,每个物质本身都处在稳定的状态,然后你可以将它们组合起来,它们互相发生反应,成为比反应物加起来还要好的物质。相同的,在软件行业,也会有不同的重构反应物,每个都具有不同的工作量、频率和能力。当它们与领域驱动的探索发现过程催化剂相互碰撞的时候,这些重构反应物就会产生代码的“化学”反应,将代码转换为丰富的领域模型。
本文讲述了Nexia Home Intelligence中一个持续很久的摄像机支持系统的重构故事。Nexia是一个大规模Ruby on Rails应用程序,需要支持使用成千上万个摄像机的客户集群需求。
我将从三个层次介绍重构。Martin Fowler的《重构》一书中谈到了微重构,就是在代码级别不断进行小的变更,以实现增量提升。好的开发人员会花时间去记忆并养成如何使用重构 工具 的习惯,所以这些微重构就成为了第二天性。
Joshua Kerievsky在《重构与模式》一书中谈到了更高阶的模型,比如说策略模式。他在书中还定义了各种“坏味道”,比如霰弹式修改,进行一个小的变更也需要很多额外的其他变更。这让我很放心,因为我的设计没有必要一开始就完全正确,我随时可以开始开发,当碰到一种“坏味道”的时候,我就有了可以重构的工具,但也只在必要的时候进行重构。
我将介绍第三层次的重构,即重构到更深层次的模型,Eric Evans已经在《领域驱动设计》一书中为我们介绍过这一层次。当我第一次阅读这本书的时候,第三部分吸引了我的注意力。在第三部分中,他谈到了一个项目,在项目中模型并不适用,他们就提出了一个新的重构方式模型,它彻底改变了项目。
看一下这三个层次,如果你可以在你的模型中引入新的概念,这就是一次有用的重构。你需要精通于微重构,充分使用模式来重构到更深层次的模型中去。
有关Nexia Home Automation
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
,这既能代表前者,也能代表后者。虽然这仅仅是个小例子,但是这段代码也能代表我们相机管理器代码库中遇到的一些问题了。
图3:相机管理器 handleRequest()
代码示例
在Ruby端,网站工作人员对于摄像机的支持随着时间推移而增长。由于大多数工作都是由不同的开发人员(主要是外包)按照需要完成的,所以过多地倾向于职责实现了,而未进行有目的地建模。
Ruby代码的优点是十分简洁,可以在几行内表达很多内容。但是, CameraWorker
情况不同,它负责验证并关闭摄像机的链接。先声明一下,由于不能本文中展示超过130多行代码,因此图4中展示了部分代码。在多个地方,worker需要对摄像机对象进行状态修改,而不是声明所需的行为。我们还碰到了一些不太好的命名,比如89行的 start_motion
调用,看上去像是开始行动的命令,但其实并不是。
图4: CameraWorker
代码示例
和那段Java代码类似,这只是其中的一个小片段,但它也可以代表系统性问题了。这些都造成了代码很难推算。
遇到的第二个挑战是相机管理器与设备管理器过于耦合了。想要理解这个问题,就得先了解一些架构的历史了。相机管理器(CM)是从通用设备管理器(DM)发展而来的,后者可以管理各种类型的设备。这就导致需要和其他Nexia的部分共享内核。这成为了一个重大的部署问题,这意味着基本上我们就不能升级Java了。最终,我们认识到这种耦合是没有必要的。尽管摄像机是个设备,但是它和其他设备没有很多类似之处,比如门锁等等。
第三个挑战真正涉及到了DDD,领域知识出现在错误的位置。大多数领域逻辑是在Java相机管理器代码之中,这就代表着增加新的功能会很复杂、耗时、容易发生错误以及很难测试。同时,修改代码代表着要对所有东西进行霰弹式修改。
DDD关注点
我列出所有问题,不仅仅是吐槽糟糕的代码,而是要明确它们并不是不可克服的挑战。此外,DDD提供了可以在很大程度上改善这种情况的技术。首先回顾一下DDD的四大关注点。
首先,我们希望在代码中演进并表达深层的领域模型。第二,我们希望将代码重构为通用的语言,内容一致易于理解,代码意图清晰。第三,我们要清楚地描述模型和模块的边界和职责。很难在没有明确边界的情况下实现高内聚和松耦合。最后,我们需要严格遵守模型边界(即有界的上下文),跨边界时进行显式转换。
从何开始?
当你遇到这样的代码的时候你会怎么做?现在有一些选择,我知道现在一些人已经试过某些选择了,但并没有成功。一个选择是“清除积水”,尝试删除所有旧的代码,重新开始。人们可能需要空出几个礼拜进行重大重构,直到“修复”的时候再解脱出来。第二种选择是 将问题甩给别人 ,自己不做任何处理。
我喜欢选择进行试验,看看你能做什么。我喜欢 2012年夏季奥运会英国自行车队获得金牌的故事 。这个团队进行了许多试验,找了很多方法,从小的地方开始改进,并做了许多改变。英国自行车队负责人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行所示。
图5:验证的三个不同方向
在我们引入了更多的领域逻辑之后,Ruby代码占据了Nexia普遍用的语言的比重更高了。我们不需要给摄像机对象进行许多修改,并设置许多属性,我们发现工厂模式更加适合。普遍使用的语言包括心跳的概念,对于这个系统来说就是摄像机连接到Nexia,就像它们或者一样。之后我们创造了名为 update_from_heartbeat
和 create_from_heartbeat
的工厂方法,来分别处理现有的摄像机和新的摄像机。
Java端也得益于重构。较之前在图3中部分展示的 handleRequest()
方法,变成了图6中的5行代码。对一些提取的方法进行重构,功能变得更加容易理解。
图6:新Java代码示例(与图3相比较)
摄像机类很大,因此你经常会跑到这段代码里。小贴士,处理这种情况并不需要通过代码重构使它更加清晰。当代码杂乱不堪时,简单地重新排列一下代码会很有效,虽然它只是个简单的设计技巧。看一看模式,把类似的方法放在一起,这将缓解你在处理庞大代码域时的认知负担。
重构到更深层次
处理一个庞大、混乱的代码库就像雾中漫步,你不能看到周围的一切,你可能看到的只是一棵树,或是一座山。当你做了一些小的变更之后(如重组织代码,提取方法),这些小的收益会累积,雾开始消散。实现微重构和Fowler和Kerievsky提到的模式能产生累积的效果,因此可以对模型有更深层次的了解。
比如说,在Ruby端,我们发现我们正在向摄像机发送指令。所以我们按照Kerievsky的建议,使用命令模式,使之大大简化了。我们为指令设置了基类,以及标准的 execute()
方法。之后我们创建了camera/command文件夹,开始写每个指令,实现这个基类。此外,我们还引入了功能开关,帮助旧代码继续执行,直到相应的指令已经转换。我强烈推荐使用功能开关来帮助你安全地进行重构。
我推荐的另一个方法是阶段性发布。我们希望避免每台摄像机突然断开连接的情况,大多数现有的客户群的产品都有共同的目标,就是不要影响到所有客户。在第一个月我们仅仅部署到Nexia IP地址,让QA、开发人员和支持人员在部署到客户之前先尝试新系统。第二个月我们加大部署,部署到一部分客户,但只使用一个生产服务器。直到第三个月我们才会部署到所有的摄像机、所有的客户和所有的生产服务器上。这比我职业生涯中参与的其他生产环境发布都要顺利。
功能开关和阶段性发布的另一个好处是它们提供了有价值的选择。我推荐 《Commitment: Novel about Managing Project Risk》 一书,介绍了实际选择权的概念。通常,当某人在会议中说“我们需要作出决定”的时候,就会有两个选择,一个是根据有限的知识作出选择,要么不做选择。实际选择权指出还有第三个选项,在我们更好地理解之前,战略性地推迟决策。功能开关和阶段性发布都可以帮助你推迟做出决定,直到你可以做出更好的决定为止,这非常关键。
回顾
回看一开始的时候,我们已经获得了很大的成就。之前,添加新的摄像机需要几周甚至几个月。现在,我们可以在几小时内添加新的摄像机。我们不再需要在Java和Ruby中都作出变更,并保持代码的同步,我们只需要在Ruby中进行修改。尽管旧代码在一些方面不一致,但新的代码更加内聚,也很容易推断,因为上下文很清晰。我们移除了 相机管理器 和 设备管理器 之间粗糙的依赖,所以我们可以更新Java了。
根据这些经验,有一些通用的重构技巧。不要只选择一个变更的实现方法,比如说命名新的东西,尝试至少三种语言和/或模型选项。在你的日常工作中,同样要注意一些小的收益。我们往往太过于高估大变更的效果,却低估了小的累积的变化的力量。
有关作者
Paul Rayner 是开发者、教练、导师、培训师以及国际流行会议讲师。他在各种行业拥有超过25年的软件开发经验,他是经验丰富的软件设计教练和领导力导师,帮助团队点亮他们的设计技巧。他的咨询公司 Virtual Genius LLC
为敏捷团队提供软件设计的指导和培训。Rayner生在澳大利亚珀斯,他在科罗拉多丹佛和妻子及两个孩子生活、工作和玩耍。他在推特 @ThePaulRayner
上用澳大利亚英语发表推文,并在 thepaulrayner.com
发布博客。
查看英文原文: Refactoring to a Deeper Model
感谢冬雨对本文的审校。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- K8S架构自动化深层解析
- 开源 | Service Mesh 数据平面 SOFAMosn 深层揭秘
- 语言结构的深层处理是NLP绕不开的坎
- Google AI提出「透明注意力」机制,实现更深层NMT模型
- 开源人工智能算法一种新颖的超像素采样,网络深层特征估计超像素
- Airbnb 的前端重构
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
An Introduction to Probability Theory and Its Applications
William Feller / Wiley / 1991-1-1 / USD 120.00
Major changes in this edition include the substitution of probabilistic arguments for combinatorial artifices, and the addition of new sections on branching processes, Markov chains, and the De Moivre......一起来看看 《An Introduction to Probability Theory and Its Applications》 这本书的介绍吧!