软件设计要素初探:组件思想
组件思想的核心是可复用和灵活组合。
在 “软件设计要素初探” 一文,尝试从软件设计的整体角度,综合讨论了软件设计的各种要素。本文讨论组件思想。
组件思想
将整个系统分解为若干紧密关联的高内聚低耦合的小而美的组件(微服务或模块),理清组件的职责、交互与边界。划分的基本原则是“识别、分离和组合关注点”。
每个组件必定有其核心关注点和基础关注点,而基础关注点中交叠的部分,即是组件交互定义的基础。
设计优良的系统,通常有一个清晰的组件化骨架。这个组件化骨架,是系统可复用、可扩展、可定制、可配置化的基础。
组件思想是可复用、可灵活组合和可定制的前提,是构建大型软件应用的核心思想之一。
组件基本原则
组件思想通常遵循如下基本设计原则:
- 接口与实现分离
- 二进制兼容
- 语言独立
- 位置透明
- 版本控制
- 组件安全
接口与实现分离
组件设计与编程的基本原则。客户端无需关心服务端的具体实现。服务端可以变更实现而不影响客户端代码。接口比实现更易于复用。
策略模式就是接口与实现分离的典型例子。事实上,几乎所有的设计模式,都遵循这个基本准则。
二进制兼容
服务端变更之后,客户端无需重新编译和部署。
语言独立
客户端与服务端可以选择不同的语言分别开发,互不影响。
比如同一个系统中的不同微服务可以用不同语言实现。java 可以调用 Go 工程里的 http 接口,反之亦然,客户端可以使用 C, lua 等语言实现,给服务端上报数据。
位置透明
服务端的部署对客户端是透明的。换句话说,无论服务端部署在单机器、多节点、集群环境、分布式环境,客户端的访问方式都是统一的。
比如微服务架构有一个服务注册和服务发现的技术。当服务在整个架构中注册后,客户端只需要引用 jar 就可以引用对应的服务,而无需知道服务放在哪里。
位置透明涉及性能、可伸缩性、健壮性、吞吐量、安全性等关键质量需求。
并发管理
组件设计实现者必须假定组件会在并发环境里使用。要确保这一点,需要提供一个并发管理服务,能够让组件在应用里紧密协作,同时给性能优化留下空间。
比如各种中间件 mongo, mysql, redis, kafka,都需要考虑提供并发能力。
版本支持
保证客户端与服务端的变更独立。服务端的变更不会影响现有的客户端代码。
安全策略
由于无法预知组件如何被使用,组件必须提供某种安全策略,避免组件被恶意利用,对客户端造成不利。
组件设计
组件识别与隔离
将业务逻辑切割和分离成适当粒度的组件,可以说是组件编程的最基本又最重要的技能了。 组件编程思想建基于此。 一般来说,组件能够做到细粒度,那么可组合性更强。如何识别组件呢?
粗粒度
有明显意图和分割线的独立业务子流程或业务实体可以作为粗粒度的组件。比如检测白名单、发送事件消息给大数据、保存业务对象等。再比如读写 MySQL, Mongodb, ES, HBase, Kafka 等。这些(业务和技术)意图和分割线明显,能够容易地识别,将其做成组件再合适不过。
关键架构特征
通过分析关键架构特征,也能识别出新的组件。
比如《Fundamentals of Software Architecture》第八章 Component-Based Thinking 讲到,对于一个拍卖系统来说,由于与竞拍者相关的部分的可靠性和可用性必须高于系统的其它部分,需要把抽离出一个竞拍者的组件。
迭代
好的组件设计不是一蹴而就的,而是反复迭代完成的。
反复审查现有组件的角色与职责,与需求的匹配度,从关键质量需求上去分析,识别和分离新组件,才能完成一次好的组件设计。
隔离
复杂的业务系统中,总有些意图不太明显或者分割线不太明显的鞣在一团的逻辑,尤其是遗留逻辑。 这种怎么办呢? 重构会花费大量的开发、测试成本,得不偿失。
此时,也可以抽离出组件,把这些逻辑放在这个组件里,隔离起来, 尽量不允许再在这里面添加代码。 这样,遗留逻辑就可以通过组件的方式隔离起来,只在局部影响。
组件粒度
组件粒度可大可小。组件粒度可以是:
- 领域服务
- 子系统
- 微服务
- 模块
- 技术组件
- 业务组件
- 工具类
- 模式
- 类与对象
- 函数与方法
- 数据结构与算法
组件设计可以从不同粒度上进行。
(1) 单个应用。
单个应用内的组件设计,主要是将单一关注点的逻辑功能组件化,能够灵活组合和配置。亦可将紧密关联的小组件串联成更大粒度的更实用的组件。
比如一个订单导出应用,其流程是:订单搜索 -> 订单详情获取与拼接 -> 订单过滤 -> 详情数据格式化 -> 结合报表模板生成报表 -> 上传报表文件。将每个步骤定义成一个组件接口(订单搜索组件接口、获取详情组件接口、订单过滤组件接口、订单详情格式化组件接口、报表生成器组件接口、上传文件组件接口),再定义一个全局控制器,将组件接口串联成导出流程,而具体的导出实现只要传入指定组件接口的具体实现即可。订单搜索可以从DB查询,亦可以从 ES 查询;订单详情可以从API接口获取,亦可以从Hbase获取。为保证大批量数据导出的性能,减少业务数据库和业务接口的压力,推荐使用 ES + Hbase 组合。
组件化之后,亦容易实现可定制化。 比如有的导出只需要导出 ES 表里的记录;有的导出只需要导出 Hbase 表里的记录;而略微复杂的导出则需要从 ES 和 Hbase 同时获取数据。这就需要根据参数配置对组件进行灵活组合。
(2) 多个应用构成了更大粒度的领域服务组件。
一些互联网企业开始推行“服务化架构”,每个服务化工程就是一个组件。比如订单管理组件可由订单搜索/订单详情/数据同步/发货组件/订单导出组件等组成;交易服务组件可由下单服务组件、订单管理组件、逆向交易组件以及辅助组件(比如交易消息推送组件、核销组件)组成。
(3) 多个特定领域服务组件构成更大粒度的行业服务。
比如交易、营销、商品、会员等服务组件可构成电商SAAS的云服务能力。比如拥有完整微商城能力并致力于移动零售领域的有赞云。
组件协作
尝试从更大范围的系统领域来思考和设计组件以及组件之间的协作结构。
比如订单导出需要从 ES 和 Hbase 搜索订单和获取订单详情,而 ES 和 Hbase 的订单数据则依赖于从消息、DB 或 API 接口进行数据获取和存储的数据同步组件。因此,大数据存储设计、数据同步对于订单导出的整体设计尤为重要。
订单导出的比较棘手的一个问题是,数据源通常来自于多个分散的业务表,这样导致同步设计重而且不灵活。如果制定一种标准,需要导出的字段必须落在下单表的字段或扩展字段里,那么就可以有效地解决数据源分散的问题,而集中精力于解决导出可扩展可配置的问题。此时,下单、同步、导出成为密切关联的一体化设计。当从单系统上难以寻求解决方案时,不妨从更大系统范围去发现。
创建组件,定制组件,设计和复用组件协作结构,组合出更大结构的组件,从而能够创建更大型更综合的大规模软件系统。
组件编排框架
当组件化做得足够好时,就可以实现组件编程框架。
定义好组件的接口和实现,通过配置将组件连接起来。配置可以采用文件或数据库记录的形式。
- 配置采用 yaml 格式。
- 配置采用数据库记录格式。 定义组件编排所需字段,比如组件ID,组件名,组件阶段、前置条件及下一个待执行组件、父组件等。
组件编程思想
任何一种新的编程思想和编程范式(命令式、对象式、函数式、逻辑式、声明式、元编程、并发编程、响应式编程、组件编程等),都需要用一种新的视角去看待日常见惯的逻辑,会对编程能力提出更高的要求;而随着工具和框架的完善,这种新的编程能力逐渐固化成日常编程模式。
组件编程有助于实现软件的“高内聚低耦合”的模块特性,提升软件的可复用性和可扩展性。从组件编程的角度来设计程序,有助于提升程序员的设计能力。
组件编程的思想很简单:从构建软件的高层视角来看,软件开发应当像堆积木一样,将各种设计良好的软件构件组装起来成为完整的业务实现。当然,设计良好的组件“积木”,以及将这些积木合理地组织集成,都不是件容易的事情,涉及到对业务逻辑的理解和和对设计的较高层次的要求,也需要大量的练习和实践。
设计先行与基于接口编程
组件编程遵循“设计先行”原则,是基于接口编程的具体体现和实践。看国外优秀源代码,在实现层之下,总有一层设计良好的接口层,而实现只是采用何种策略的问题。
做设计,需要先从概念设计和行为定义上去建立整个业务逻辑框架,而不是直接从实现上着手。这种方式与日常写代码的直觉方式是有点相悖的,但有利于构建更稳固、可复用性、可扩展性更好的软件。
接口包括两部分:接口名和接口里的方法。接口名说明了组件的意图,而方法则定义了组件的行为规范。
组件编程的一个基本技巧是“识别和分离关注点”。
心里没有“关注点”概念的开发者,写代码时比较随意,在实现功能时无意识将多个关注点掺杂在一起,当关注点发生变化需要重组时,“for-ififelseif-for-ifelseif-switch”的噩梦开始诞生了。而善于“识别和分离关注点”的开发者则会想办法把这些关注点解耦开,实现成简洁可组合的短小函数、方法和类,并置于该归属的地方。
组件编程的另一个基本技巧是“面向接口编程”。
先创建所需要的组件接口,然后创建基于组件接口的应用骨架,最后根据需求和场景创建和注入具体实现。
- 公共组件与业务的交互,公共组件应当仅依赖业务接口,而不依赖具体业务实现;
- 跨模块交互,模块应该仅依赖外部模块的接口,而不依赖具体的模块实现。
小结
本文探讨了组件化设计与组件编程思想。
应用组件化设计和组件编程思想,能够让应用更加高内聚低耦合,实现更加优雅而可扩展可配置。