细数软件架构中的解耦

2020 年 11 月 11 日

细数软件架构中的解耦

架构的定义

架构是软件方法学的范畴,它解决的是软件组织的问题,不解决软件算法的问题。两者的区别可用下图的积木做个类比:



算法就像一个个的积木块,比如绿色的圆柱,蓝色的三角,红色的方块等。而架构则是把各种积木块,组装成一个城堡,一辆小火车。为搭建这个城堡或小火车,架构师脑子里得有张图纸,图纸里既要定义需要哪些形形色色的积木块,又要考虑如何将它们组装起来。这工作很像建筑师,英文也的确叫 architect。


这样类比,很容易让不太理解技术的企业家们陷入误区,会觉得架构师要比算法工程师更厉害?其实不然,这是两个细分领域的才能。不知道您注意到小火车车头上的烟囱没?它是一个像鸡腿菇一样的弧线造型,浇灌出这种造型的模子,要比三角形和方块形要难很多,它需要更深奥的几何学的支撑,这可以形象的看做是算法工程师解决的问题。

架构的意义


架构解决软件组织的问题,它能给企业创造什么价值?换句话说,好的软件组织,跟差的软件组织,从商业价值创造的角度,有什么不同?笔者以为架构的价值体现在可用性和敏捷性两个角度,但今天要讲的是敏捷性。敏捷性指的是快速、低成本、高质量地应对扩张市场的差异化需求。企业在初创期积累了不少软件资产,这些资产在当初的市场环境下,已被论证取得了市场业绩。但是伴随着企业扩张,市场会更加精细化、场景化,这些都会给我们的软件提出新的需求,企业需要借助前些年在这个领域积累的先发优势,一方面快速占领细分的市场;另一方面复用曾今积累的资产,发挥资产的规模经济效应。


比如京东电商,从高价值、标准化的 3C 数码起家,建立起自营电商模式;紧接着开始扩品,做低价值、但高频次、依然标准化的日用百货圈用户粘性;再做相对非标的服装发展女性用户和生态模式等,直指行业竞争的关键区;除了扩品还伴随着场景扩张,诸如 2B 企业业务、下沉市场拼购业务、泰国印尼国际业务等。供给角度的品类扩张,需求角度的场景扩张,构成了京东矩阵式垂直业务线。它们正是复用了零售中台的软件基础设施,才在一定程度上做到了快速扩张。


架构的灵魂


既然软件组织的价值如此重要,那么好的软件组织的标准是什么呢?又该如何做到呢?好坏的标准在解耦。解耦的对立面是耦合,耦合是指阻碍变化的依赖;解耦是要在依赖的基础上,做到应对可能的变化。依赖是必不可少的,依赖的本质是分工,正如亚当斯密的《国富论》论述的那样,分工有助于专业化、有助于提高效率。太抽象了!说了这么多,没讲清楚解耦是什么。的确,笔者也认为这样的解释只能让已经理解了的人再表示一次赞同,无法让原本不理解的人变得理解,这样毫无意义!我该如何诠释?事实上,很多真理是建立在归纳法基础上的。归纳法的好处是见得多了自然就会(归纳似乎是人脑的一种本能),比如诗词,只要熟读唐诗三百首,不会吟诗也会吟。不信你看,先来一篇叫“大漠孤烟直”的,没啥概念;再读一篇叫“空山新雨后”的,有点感觉了;最后“小桥流水人家”你自己就会了。如何写出点有意境的诗,你张口就来“床前明月光”,还不是自己写的?如果你去到草原晚上触景生情,即兴来上一句“明月篝火烤肥羊”,就能媲美“日照香炉生紫烟”了。所以笔者觉得,最好的方式就是细数那些软件架构中的解耦,让读者从铺陈式的实例中,自己找感觉。


笔者分 3 类 6 组(每类分进程内的应用层和进程间的架构层)给大家举例:


外加中间的 Naming 解析与 Proxy 代理融合的 CNAME 别名,总共 7 个案例。

中间层映射

中间层映射的设计理念是当 A 对 B 有依赖时,A 不要直接依赖 B,而是抽象一个中间层,让 A 依赖中间层,再由中间层映射到 B,从而当 B 变成 C 时,不用修改 A,只用调整中间层的映射关系。中间层映射,在应用层表现为面向接口动态绑定,在架构层表现为 Naming 解析动态绑定。

应用层-面向接口动态绑定


面向接口编程的核心思想是“先想清楚做什么,再想让谁来做”。什么叫想清楚了做什么?就是用接口的形式,描述输入什么,输出什么;但接口更多描述的是语法层面,语义层面的刻画还需配合单元测试及其断言(技术上叫 Test Driven),还有文档。这跟企业家们常读的《高效能人士的 7 个习惯》里面讲的“以终为始”,思想上如出一辙。让谁来做?就涉及到运行时动态绑定。比如下图:


在 Java 面向对象的语言里,使用方通过 Provider 接口 Response doService(Request r)来对外刻画它的招标文件。然后三个供应方,LocalProvider、RemoteProvider 和 AsyncProvider 来应标。使用方只使用 Provider 接口,至于它跟哪个具体的 Provider 绑定,完全可以在“采购”时刻动态替换。


面向接口动态绑定的解耦,体现在使用方把依赖的服务抽象为一个接口,依赖这个抽象的接口,而不依赖于具体的服务提供者,以便应对服务提供者变化的可能性。

架构层-Naming 解析动态绑定


上图是域名服务 DNS 的示意流程。客户端并不直接通过 IP 地址来访问 Provider#A 或 B,而是先询问 Naming 服务,并依据返回的服务列表,再访问 Provider#A 或 B。如果某个 Provider 故障了,可以替换转移到其他的 Provider。出于性能考虑,也可以在客户端把 Naming 的结果缓存起来,并配个缓存更新机制。


基于 ZooKeeper 的应用层名字服务,思想上类似 DNS。不同的是,它基于 TCP 长链接来实现 Server Push,可及时刷新服务列表。


Naming 解析动态绑定的解耦,体现在使用方把依赖的对象或网络进程,抽象为一个名字,名字代表的具体服务提供者则通过 Lookup 机制返回,进而做到如果提供者有变化,只要改变 Lookup 的结果,无需改变使用方代码。

前后节植入


前后节植入的设计理念是服务器是流程的集合,流程是环节的序列。改变一个流程的行为,可以通过在其前后植入一个新环节来实现。前后节植入,在应用层表现为 Chain 拦截模式,在架构层表现为 Proxy 代理模式。

应用层-Chain 拦截模式



上图是 Strtus2 的架构,每个 Action 的执行,都会被包裹在一系列 Interceptor 里面,形成一条处理链 Chain,每个 Interceptor 会进行 PreHandler 和 PostHandler 处理。这里的 Interceptor 可以增加、删除或替换,以此实现可拓展性。比如可以在 Interceptor 里做鉴权、日志、性能统计、限流等。


Chain 插拔的动态绑定,通过增删替 Interceptor,把过去 URL 与 Action 的 1:1 的处理关系,转变成了 M:N 的处理链。一类请求(某个 URL),可以被多个 Interceptor 处理;一个 Interceptor 也可以处理多类请求。


顺便说一下,Strtus2 这里说的“动态绑定”,是配置相对硬编码而言的。严格意义上,这里的绑定是编译期的,不是运行期的,是静态的绑定。类似的架构还有 Spring AOP 和 Servlet Filters 机制。

架构层-Proxy 代理模式



上图是一个 Proxy 架构模式,这个应用极其广泛。比如 HTTP 的 Nginx,SQL 的 Apache Calcite,memcached 和 redis 的 twitter/twemproxy。为什么?因为 Proxy 对于 Backend 而言就是流量入口,是中间人,能扮演架构层面的 AOP 机制,可拓展性非常强。


当一个请求过来后,刚开始 Proxy 转发给 Backend#A。但是业务发展了,Proxy 也可以转发给 Backend#B 以实现负载均衡,更重要的是 A 和 B 还可以不同的版本,以实现灰度发布。还可以植入 PrePlugin 和 PostPlugin:

  • 在PrePlugin里可以做权限控制、流量控制、请求改写、缓存加速、恶意流量拦截、PV统计、性能Profile、ChaosMonkey混沌事件植入等等。

  • 在PostPlugin里,还可以做响应报文改写,安全加密(后端不用考虑数据安全,对外时统一加密处理)、压缩加速等等。

两者融合的实例-CNAME 别名



上图是一种混合模式:既有 Naming 解析,又有 Proxy 代理。而且 Naming 服务,为了支持可拓展,还引入了父子层级,末端的 Naming 服务,完全可以委托给上一层级的 Naming 服务。


在 DNS 里面,我们经常会看到 www.example.org 的域名解析,CNAME 别名到 www.example.org.cdnprovider.com (它是 cdnprovider.com 的子域名),这样客户端不用修改,依然访问的是 www.example.org ,但是对应的后端服务,却不再是直接访问 Provider#A 或 B,而是中间植入了 CNAME Proxy,再由 Proxy 依据 Plugin 的决策,是否转发给问 Provider#A 或 B。


这个设计太棒了!它使得商业公司 cdnprovider.com 给 www.example.org 提供 CDN 服务时,完全是零侵入,不需要修改任何一段代码,只需要在域名服务商那修改 www.example.org 的域名解析,这个操作代表 www.example.org 同意 cdnprovider.com 为他们提供 CDN 服务,代表授权。这一切,都源于基于 Naming 解析的动态绑定实现的解耦。同样的,除了 CDN,我们的恶意流量清洗、灰度发布、性能分析等都可以采用这种方式,实现零侵入插拔。

事件流订阅


事件流订阅的设计理念是将瞬间的过程化调用转变成可回放的指令,对指令的响应可以不用再预定义。事件流订阅,在应用层表现为 Mediator 中介模式,在架构层表现为 Broker 消息模式。

应用层-Mediator 中介模式


A 直接调用 B,意味着 A 对 B 产生了强依赖。当然我们可以通过面向接口编程,把这个依赖降低,降低到只依赖接口,不依赖实现。简单说,我们只依赖对事情的处理结果,不依赖于如何实现这个处理结果。


但是这还不够,因为我们还依赖了接口,接口意味着对处理语义的刻画。现实中有些情况,连语义的描述都要发生变化,也就是接口都要发生变化,如何进一步解耦呢?如下图:


A 不直接调用 B,而通过中介 Mediator,解耦两步:

  • 先由A调用Mediator: A持有Mediator的引用,执行Mediator的方法,即mediator.publish(e)。

  • 再由Mediator调用B:  为了解耦Mediator对外界的依赖,我们用面向接口EventHandler来实现依赖反转。让B来实现EventHandler,当然如果B已经存在,或更有话语权,依然应该遵循依赖反转的原则,只不过Mediator模式的推进方可以再实现一个Adaptor,来帮助既有的B适配到EventHandler。


有了上述的设计模式后,具体的执行分三步:


  1. 订阅: 通过 mediator.subscribe(b)把未来的事件处理提前注册到Mediator。

  2. 发布: A向Mediator发布自己的事件。注意这个理念特别重要,A仅仅发布发生了什么事情,A并没有直接调用B声明对事情的处理。也就是A连对B的接口都不再依赖了!举个例子,比如新员工入职,刚开始要为员工办理磁条卡,只是办理磁条卡的供应商可能是甲,也可能是乙。这叫面向接口编程,但这还不够,因为随着公司的发展,现在新员工入职,有人脸识别了,不用再办磁条卡了,而是要登记人脸识别,另外员工福利更好了,对异地公干的新员工入职还会发放一笔安家费,这些都是之前的“接口”没有描述的。

  3. 执行: 当mediator收到A的事件后(A调用了mediator.publish(e)),mediator会通过EventHandler来回调预先通过mediator.subscribe(b)注册的处理类。


上述 Mediator,有些局限性,对所有的 Event,只能有一种 EventHandler。如果我们把 Mediator 升级为一种通用的处理机制,一种平台,自然会有各种各样的 Event,自然会我们会对 Event 做个分类或分组。我们把 Event 的分类或分组,叫做 Topic;而把 Event 理解为 Topic 这个类里面的具体实例。并在 Mediator 里面维护,从 Topic 到 EventHandler 的一组处理器。如下图所示:



可以看到上述架构通过 Map<Topic, List<EventHandler>> resolver 来维护从 Topic 到 EventHandler 的一组处理。为什么是 List<EventHandler>,而不是 EventHandler 呢?为了更加灵活,比如上文提到的「现在新员工入职,有人脸识别了,不用再办磁条卡了,而是要登记人脸识别,另外员工福利更好了,对异地公干的新员工入职还会发放一笔安家费」。

架构层-Broker 消息模式


上图的 Broker 模式,跟 Mediator 模式其实没有本质的不同,只不过 Broker 更加突出了借助消息中间件 MQ 实现异步。客户端提交一个委托,Broker 持久化完成,并回复 ACK,表示委托已收到。接着委托的消费处理,可以是离线的。通常需要支持 Group 机制:Group 内部多个 Instance 是负载均衡的,它们共同瓜分委托消息的处理;而 Group 间是冗余复制的,它们各自消费各自的,相互之间隔离,有助于实现业务可拓展性。


比如一个新员工入职,它产生一个“新人入职”事件,然后行政部门会为其准备工卡、财务部门会为其准备工资卡、HR 部门会为其缴纳社保。当然,随着公司业务发展,可能还会增加,比如业务部门的业务培训,风控部门的合规性培训等。


跟前面说的 Proxy 模式,相同点在于它们都是在架构层面实现可拓展性。不同点是,Proxy 模式支持的是 PreHandler 和 PostHandler;而 Broker 模式支持的是 MidHandler。

关于作者

作者李伟,研究微服务与大数据方向,擅长中台架构敏捷性课题。就职于京东零售,担任资深架构师,加入京东前曾就职于搜狐和百度。

2020 年 11 月 11 日 10:215845

评论 11 条评论

发布
用户头像
晦涩难懂的术语用简单明了的比喻解释,做的不错。说清楚了解耦合的理论又加入落地方法,学习了。作者辛苦啦
2020 年 11 月 20 日 07:25
回复
感谢读者
2020 年 11 月 24 日 10:22
回复
用户头像
非常不错的架构类文章,期待后续更多的文章!
2020 年 11 月 19 日 17:18
回复
用户头像
“应用层-Mediator 中介模式”两张图是一样的。
写得挺好的,就是对于应用层和架构层的分类感觉比较奇怪,我理解主要就是单个应用进程内和多个应用进程间(“进程内的应用层和进程间的架构层”),相对来说进程内和进程间比较好理解,应用层和架构层歧义会比较多。
2020 年 11 月 17 日 14:28
回复
感谢发现BUG,拖图的时候拖错了,我请InfoQ更新下。如您理解,这里应用层是进程内、架构层是进程间。笔者以为,设计的思想精髓在 Desigin Pattern里已经描述的差不多了,但那时候的时代背景分布式(跨进程)没有今天这样,刻意分3类6组其实也是为了让读者可以体会设计思想精髓与场景表现的区别与联系。
2020 年 11 月 17 日 18:17
回复
用户头像
架构的贯彻性和延续性其实是很多企业IT遇到的问题,就好比足球流派,大家经常是今天拉丁技术流、明天欧洲力量流,企业每个部门的诉求和时效性不同,故架构多半会越来越乱。
2020 年 11 月 16 日 16:51
回复
用户头像
没有标注来源啊 来源是 http://downgoon.com/2018/06/arc-decoupling/
2020 年 11 月 16 日 13:36
回复
同一个作者哈 downgoon
2020 年 11 月 16 日 19:28
回复
用户头像
不错,期待后续
2020 年 11 月 12 日 11:28
回复
用户头像
这个排版看的真是难受。
2020 年 11 月 10 日 22:30
回复
抱歉,刚开始排版时出问题了,现在已修正哈~
2020 年 11 月 11 日 10:26
回复
没有更多评论了
发现更多内容

mPaaS x Menxlab | 1024程序员节:Talk is cheap,Show me the AppID

蚂蚁集团移动开发平台 mPaaS

程序员 开发者 mPaaS 1024

现成区块链交易所开发app,币币撮合交易平台搭建

WX13823153201

现成区块链交易所开发

架构师训练营 1 期 -- 第五周作业

曾彪彪

「架构师训练营第 1 期」

DDIA 读书笔记(2)数据模型的存储与检索

莫黎

读书笔记

趣味科普丨一文读懂云服务器的那些事儿

华为云开发者社区

镜像 服务器 服务

java week1练习

闷骚程序员

架构必修:领域边界划分方法--职责驱动设计(RDD)

马迪奥

架构 领域 架构师 RDD

typora增强-mac

老菜鸟

Typora

解析 CloudQuery 审计分析功能

CloudQuery社区

数据库 sql 安全 工具软件

Microsoft Azure机器学习采用NVIDIA AI为Word编辑器提供语法建议

Geek_459987

千万不要往 Shell 里粘贴命令!

大道至简

命令行

iOS性能优化 — 一、crash监控及防崩溃处理

iOSer

性能优化 ios开发 Crash 监控及防崩溃处理

二十、深入Python迭代器和生成器

刘润森

Python

甲方日常 35

大橘子

工作 随笔杂谈 日常

年纪轻轻怎么就卵巢早衰了?试管可帮忙!

Geek_65d32f

试管 三代试管

Go语言内存管理三部曲(三)图解GC算法和垃圾回收原理

网管

go 内存管理 垃圾回收 GC GC算法

学了那么多 NoSQL 数据库 NoSQL 究竟是啥

哈喽沃德先生

数据库 nosql 非关系型数据库

GitLab用户切换引发的某程序员“暴动”,怒而开源项目源码

小Q

Java git 学习 开发 代码仓库

数据结构与算法系列之链表操作全集(一)(GO)

书旅

go 数据结构 数据结构和算法

vivo 商城前端架构升级—前后端分离篇

vivo互联网技术

JavaScript 前端 前后端分离

攻克金融系统开发难点,借助SpreadJS实现在线导入Excel自定义报表

Geek_Willie

SpreadJS 在线导入excel

AI让远程交流“更清晰”:GAN消除视频通话中的抖动

Geek_459987

吃透阿里大佬整理的Java面试要点手册,成功五面进阿里(二本学历)

Java架构追梦

Java 学习 架构 面试 核心知识点整理

LeetCode题解:98. 验证二叉搜索树,递归中序遍历过程中判断,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

JavaScript 类型 — 重学 JavaScript

三钻

JavaScript 前端 前端进阶

自动化测试框架类型,你知道几种?此处介绍5种比较常见的

软测小生

软件测试 自动化测试框架 软件自动化测试

【线上排查实战】AOP切面执行顺序你真的了解吗

Zhendong

spring aop

Flink窗口算子-6-8

小知识点

scala 大数据 flink

机器学习是什么?

马同学

机器学习

ArCall功能介绍手册

anyRTC开发者

ios 音视频 WebRTC RTC 安卓

架构师训练营第五周学习总结

尹斌

细数软件架构中的解耦-InfoQ