如何应对软件复杂度

1/22/2020

缘起

至今也写了四年多代码,但如何在功能不断增多的同时写出可读、可维护、可扩展的或者说优雅的代码一直令我感到非常困惑。

最近读了《Clean Architecture》以及 Domain Driven Design(简称 DDD) 相关软件架构的书,对这个问题有了进一步的思考。

战术的成功不代表战略的成功

虽然读过《代码大全》、《Clean Code》、《重构》、《Design Patterns》等书,也在编程中不断实践,但逐渐发现书中的技巧更着重细节的打磨,关注点在于如何把一个函数、类、模块设计好。不过大量模块集成后的系统质量却难以保证。

没有设计导向糟糕设计

为什么不是“没有设计就是糟糕设计”。因为当软件的复杂度很低时,无论如何都可以达到不错的质量。对于一个小模块,只需几个类就可以实现预期的功能。

但需求永无止境,功能堆叠难以避免。新功能可能是基于原模块修改,添加一些新类和新函数,也可能是通过新模块实现。此时模块、类、函数之间的交互逐渐复杂,虽然整个系统还可以理解,但是混乱的种子已经埋下。

cYlmsh

长此以往,人员的来来往往伴随着千人千面的设计,整个系统难以避免的走向了大泥球。

n5AUMn

失控的软件复杂度让“软件”变为“硬件”

大泥球的代码依旧可以运行,依旧可以为用户提供功能,依旧可以在上面修修补补,为什么这会成为一个问题呢?

因为现实中的需求和业务总是在不断的变化中,这也要求软件不断的发生变化以适应需求的变化,而失控的代码恰恰导致变化的成本非常高昂。

复杂的依赖关系意味着每一行代码修改的结果都会如同涟漪一般扩散到依赖的代码,导致难以预料的后果。

边界含糊的模块、类、函数互相交织,再没人能理解整个系统。缺失的上下文导致变更时,程序员只能从代码中重建对应的逻辑,往往落入盲人摸象的困境,而基于不完整理解的产物通常是四不像的怪物。

长此以往,变更、维护的成本会越来越高,软件易修改、可扩展的优点丧失殆尽,退化为“硬件”。

知道大泥球是避免大泥球的第一步

在大泥球中工作绝对不是一件愉快的事情,接手过旧有代码的人都可以理解,因为大泥球还有另外一个俗称“屎山”。从个人来说,为了愉快的编码,肯定不希望自己的代码变成大泥球。对公司来讲,代码的可扩展性、稳定性、可维护性是公司竞争力的来源,如果你可以比对手花更低的成本,更好更快的响应用户需求,自然能步步争先。

为我们的问题构建正确的心智模型

软件经过几十年的发展,能做的事情越来越多,复杂度越来越高。在这个过程中,大家花了很多的时间去思考怎么样才能更好的管理复杂度,因为软件的极限在于我们能多大程度上理解我们所创建的系统

面向对象、垃圾回收、函数式编程、设计模式等都是思考的结晶,但它们更着重于技术的实现,随着软件不断整合复杂的现实世界,我们需要从更广阔的角度、用更好的方式来建模业务逻辑,DDD 即是其中流行的架构范式之一。

以技术为中心的架构设计

以技术为中心的思考方式通常能以最快的速度搞定需求。当开发一个功能时,首先考虑的是数据库怎么设计,接口怎么定义,需不需要缓存,如何利用框架已有的功能等,有了一个大概轮廓后,编码、测试、上线,不断循环。但从架构设计的角度,让代码跑起来只是第一步,如果新代码不能跟旧代码在整体设计上进行集成,最后很可能变成大泥球一般的四不像怪物。不过就算我们以良好的服务设计、模块化设计、类设计、接口设计将新功能优雅的整合进现有模块,以技术为中心的开发方式依旧有其最大的弊病。

技术为骨,业务为皮:研发为什么不说人话

在工作中,我常常听到客户经理、销售、产品对研发说:你为什么不说人话?因为通常来说,需求的发起方是来自销售、产品这些业务专家,他们对业务的建模(心智模型)通常跟我们以技术为中心的思考方式不同。他们并不了解微服务、关系型数据库、Mongodb、Redis、Route、Graphql、RESTful、gRPC 等技术术语,他们了解的是业务逻辑、业务对象及其交互。不懂技术的业务专家,不懂业务的技术,他们的沟通成果可想而知。

在实际的开发中,研发通常会将业务逻辑简称为 crud(增删改查),认为技术是骨,业务是皮,业务逻辑不过是一些简单、琐碎的编码过程,笑称专注于此的程序员为码农。长此以往,每个功能内含的底层业务逻辑被割裂,零碎的分散在整个代码库中。而新需求通常是基于业务专家的建模,此时谁还有能力从整个代码库中还原出整个业务蓝图并予以修改呢?

建模应当基于用户的心智模型

为什么要发明高级编程语言而不使用汇编?因为编程时通常不需要关心底层的寄存器、指令集。

为什么要发明 ORM 而不是用 SQL ?因为 SQL 作为关系型数据库,不是以面向对象的思考方式设计。

为什么要发明图形化界面而不使用命令行?因为大部分用户习惯通过图形化的方式来认知世界。

设计应当以终端用户的体验为中心,而架构说到底不过是设计的别名。当心智模型和底层逻辑匹配时,变更、维护、扩展的成本因此降低。而业务软件的用户是谁呢?业务专家以及熟悉该业务的用户。

t5V5eq

业务为骨,技术为皮: 领域驱动设计(DDD)

基于以上的思考,一些人在 20 年前提出了领域驱动设计(Domain Driven Design),简称 DDD,试图扭转这一趋势,提倡以业务为中心的架构设计。

以 DDD 的进行架构设计,首先不是关注语言、数据库、框架、RESTful、gRPC 等,而是从梳理业务逻辑出发,通过与业务专家的深入讨论,构建符合业务逻辑的模型,再通过各种各样的技术实现支撑业务逻辑的建模。在这个过程中要严格分离业务逻辑和技术实现,因为通常业务逻辑和技术实现有不同的变化速率以及变化方向。

分而治之:识别核心领域并精炼通用语言

一个产品有人使用,必然是有其核心优势,优势所在即其核心领域。比如今日头条的核心优势不是资讯,不是爬虫,而是其推荐引擎。资讯、爬虫、甚至抖音的短视频都是基于其推荐引擎的次级领域。对于一个影评网站来说,核心领域则是其评分、评论机制。而用户系统,包括登录、注册、重置密码、头像等功能,只能算是一些通用领域。

这样的话,我们可以基于业务领域的重要性进行划分:

  • 核心域(Core Domain):产品竞争力的来源和优势所在,例如头条的推荐引擎。
  • 支撑域(Support Domain):实现核心域的辅助领域。例如资讯、短视频的管理,以及如何依赖推荐引擎进行分发。
  • 通用领域(Generic Domain):例如用户管理。这里是各种框架、库、第三方服务大放异彩的地方。

每个领域应该保证其中的核心概念具有精确的定义,否则“名不正,则言不顺”。正如用户管理领域中的用户跟推荐系统中的用户是不同的概念,具有不同的属性和行为,各有其偏重。

下图以保单为例,我们在承保时,关注的是保单的价格和理赔条件,在审核时,可能关注的是承保人的健康状况,保单是否为本人签名,而理赔时则关注对应的赔偿金额。如果强行把不同阶段的逻辑都整合到一个保单对象,必然会导致逻辑繁杂、主次不分。

M4ZeA8

领域的建模需要业务专家和技术人员的深入沟通

DDD 专注业务复杂度,致力于将业务专家的心智模型建模为技术模型。这个过程需要业务专家和技术人员深入沟通、并肩协作,通过大量的信息传递,在不断的讨论、质疑、磨合中提炼最终的模型。建模是 DDD 最重要的实践过程,也是 DDD 最难以落地的困难之一:对业务感兴趣并喜欢深入钻研的程序员并不多

这个问题随着团队人数的增长,会越来越严峻。对个人开发者来说,设计和开发都在一人之手,自然也不存在沟通的问题。对于小型团队,如果不能保证团队成员对产品感兴趣,只能通过文档、培训等来普及领域知识。随着领域知识的不断增长和复杂化,超过个人的承载能力后,划分不可避免。

B3RwDM

领域的建模是动态的

领域建模需要深入设计,有人会疑惑:“那是不是需要整体设计完后才能开始编码,这不是退回到瀑布流的开发模式?” 并不是。很多 DDD 的倡导者本身也是敏捷和极限编程的支持者。他们认为一开始就认为设计出完美、精确的领域模型是不现实的。随着业务的扩张收缩、产品开发方向的变化,我们会不断获得新的领域知识,领域建模在开发、沟通、实践过程中会被不断精炼、完善。旧概念的理解会越来越透彻,新概念会不断涌现,一些不相关的概念会被剥离,因此代码的渐进式重构不可避免。领域驱动设计(DDD)也意味着对业务领域的深入理解驱动着设计的不断变化

后记

DDD 提倡我们要尊重现实世界的业务逻辑,尊重已有业务专家的经验,以业务为中心驱动架构设计,更好的为业务服务。

对我来说真有当头棒喝的效果,毕业这几年已经习惯从技术的角度看问题,相信技术万能论。“当你手上有一把锤子的时候,看所有的东西都是钉子”,有时候效果很好,有时候却一叶障目,但长此以往,目光越来越狭窄是必然的。

DDD 落地还有很多技术细节,有机会的话下次在谈。这里也欢迎喜欢编程、架构设计、DDD 的小伙伴一起来探讨。