应对复杂软件的思考

由于自己身处SAAS行业,在经历了几轮复杂需求的蹂躏之后,我一直试图寻找一种解法,可以尽量cover住复杂多变的需求。在过去的一年中,通过反复阅读和实践,似乎让我对此有了一些清晰的思路,所以我想写一点东西总结一下自己的这一年里的思考。

在我们的项目初期,项目的规模可能比较小,代码量很少,我们的代码或许还能整理的比较干净,就像这几组交换机的网线一样,比较有条理。

整齐的网线

但是随着功能复杂之后,项目也随之变得庞大,整个代码就可能会和这个机房一样,非常的混乱。在经历几次这种状况之后,于是我便在想,究竟是什么问题导致了这种混乱。

凌乱的机房

首先来看一段代码(Kotlin Code):


fun executeRequest(request: Request) : String {
    // 校验身份
    val isValidate = validateRequest(request)
    if( isValidate ) {
        return "Request is not valid"
    }
    // 处理业务
   dealBiz(request)
    try {
        // 存储数据
        saveToDB(request)
    } catch (exception:Exception) {
        return "Occur error when save results to DB"
    }
    // 发送消息
    val isSendSuccess = sendMessage(request)
    if (isSendSuccess == false) {
         return "message send unsuccessfully."
    }
    return "success"
}

这是我们比较常见的一些代码结构,其实看起来问题也不算很大,但是随着业务复杂,业务逻辑的控制和控制逻辑耦合的很厉害,阅读这种"面条代码"的成本越来越高。每一位新进入项目的伙伴犹如进入了一个“代码迷宫”。来来回回去寻找自己需要的那一段代码,实际上这个时候已经形成了:

只有上帝和我能看得懂的“上帝代码”了。

这显然是我们不愿见到的代码,在左耳听风专栏里有一篇《编程的本质》里讲到:

有效地分离 Logic、Control 和 Data 是写出好程序的关键所在。

那什么又是Logic,Control,Data 呢?

  • Logic : 就是一般的业务代码,类似上面代码中的dealBiz(),sendMessage()等等
  • Control : 对业务逻辑的流程控制,比如遍历数据、查找数据、多线程、并发、异步等等
  • Data :函数和程序之间传递的这部分信息

如果有效地将这几种代码分离,代码可读性将会大大提升。通过这种拆分,我们也降低除了自己之外的维护者阅读代码翻译业务内容的成本。通过分离,我们可以将代码写成这样:

fun executeRequest(request: Request) : String {
    return Result
            .of(request)
            .flatMap { validateRequest(request) }
            .flatMap { dealBiz(request) }
            .flatMap { saveToDB(request) }
            .flatMap { sendMessage(request) }
            .fold(
                success = { return "success" },
                failure = { return it.message }
            )
}

当然这里说了如何写细节的代码,那么代码架构又如何去做才可能保证可以应对这么多的变化?

一般的项目中我们把一个软件系统进行分层,这是我们目前做工程项目的一个共识,我们最初学习的分层架构就是经典的三层架构了。它自顶向下分成三层:

  • 用户界面层(User Interface Layer)
  • 业务逻辑层(Business Logic Layer)
  • 数据访问层(Data Access Layer)

数据访问层这块,其实很多系统已经变成了面向数据编程,最终做成了“数据库管理系统”。按照传统的三层模型,用户界面的开发依赖Service层,而Service层又依赖着DAO,DAO对应着数据库。大家相互依赖,业务逻辑一旦修改,就意味着要从DAO层开始修改,数据库也跟着被修改,而往往随着我们开发的深入,业务的模型会被不断调整,这样数据库可能就要频繁的变动。代码也开始变得复杂... ...

而在领域驱动设计中提出了另外一种四层架构,在此之前,我想先分享《实现领域驱动设计》一书讲的六边形模型。

六边形架构

我们在设计系统的时候,往往过于关注数据库,Http接口等基础设施的设计,而忽略了我们需要关注的业务。在复杂系统中,最容易变化的也是业务形态,产品经常会要求改来改去,因为业务本身就在不断地演进,如果我们一开始就基于数据库作所有的设计,那么势必一旦遇上业务的修改,库表肯定也需要对应先进行变化。假如我们融入六边形架构,将数据库和暴露的Controller都视为是基础设施,先去关注业务的模型和代码,Class的修改比要数据库改起来要简单的多。另外一方面,也大大提高了程序的可测试性:在没有准备一堆基础设施(数据库,接口,异步通知等等)情况下,可以先测试逻辑的完整性。

另外,有时候随着业务增长有的基础设施是会需要进行替换的,采用六边形架构之后,这种更换的成本就会降低。另外如果出现需要使用Web Service的客户,我们也不必纠结于之前的HTTP接口,直接开出一套新的协议代码供客户使用,而不会纠结领域部分代码有逻辑上的缺失。

采用六边形架构之后,我们的领域模型也会更加独立,更精简,在适应新的需求时修改也会更容易。在《架构整洁之道》之中提到的“整洁架构”也与“六边形架构”大同小异。

Bob大叔的Clean Architecture

其实这两种架构也是依赖倒置原则很好的实现:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  2. 抽象不应该依赖细节
  3. 细节应该依赖抽象

此时再回顾原来定义的四层架构:

  • 用户展示层
  • 应用服务层
  • 领域层
  • 基础设施层

他们的依赖关系如图:

四层架构

这样我们可以将业务核心代码放入领域层之中,要应对的各个场景代码放入应用服务层中。将协议转换、中间件和数据库的适配都放入基础设施层里。在应用层与Controller之间的那些VO作为用户展示层,以做出整洁架构。

当我们开始学习Java的时候,都知道Java是一门面向对象的语言,我们本可以将现实世界翻译到代码的世界之中,但实际上我们往往在项目中只会将对象定义成贫血模型,最终写成面向过程的代码。如何做才能让这个复杂的世界反应到代码里呢?

让我们再从需求说起,对于一个复杂的软件,任何一个项目的参与者(包括初创的成员),都很难靠自己就看清整个项目的全貌,我们犹如图中的盲人,大家可能最后对项目的理解都是不一致的。此时每一位参与者都犹如“盲人摸象”中的“盲人”,对需求(大象)只有片面的理解,于是乎,有的人觉得大象是水管的形状,有的人觉得大象是扇子一样的形状,有的人说大象长得跟柱子一样... ...

盲人摸象

通过讨论,我们会对自我的认知进行一些修正,最终大致得出一个需求的全貌。

讨论的价值

比如我们要去识别一些系统边界,在DDD的战略设计中非常强调划分界限上下文。比如我作为一个个体,在不同的场景中,我的身份、角色都不大相同。

对自我身份的识别

犹如在上图中,在“地铁”、“家庭”和“公司”中,我的身份是不一样的,但是我依旧是我,找出业务的场景,也就意味着我找到了系统的边界。通过分析场景识别边界来找出系统的核心领域和支撑领域,以此来最终确定系统的数量,降低系统的耦合。

我们还可以用四色建模方法来识别出我们系统中发生的整个流程,发现究竟是谁通过什么方式触发了什么事情,最终又影响了哪些对象。

四色建模(图片来自infoq.cn)

最终通过我们找出的事件,整理出一个能够让我们进行沟通的模型。在我们的模型被构建得相对完善之时,其实代码也差不多已经被构建出来了,因为这个时候再去回想面向对象的设计,我们发现模型即代码,代码即模型。

四色建模的要点

一直以来我都希望通过一些好的“工程实践”来提高团队的效率以及我们的代码质量,我想这也是思考这些架构的意义吧。我想用《架构整洁之道》中的一句话来做本文的总结:

软件架构的最终目标是,用最小的人力成本来满足构建和维护系统的需求。

正如《人月神话》里说的一样,软件工程里没有“银弹”,即使做了整洁架构也无法避免需求的变化和延期,只是希望当我们身处需求的困境中时,仍能给自己以更多的选择。

参考资料&活动 :
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容