软件系统设计方法和工具介绍
在构建系统时,尤其是一些大项目实施的过程中,可以接触和学习一些高阶层面分析问题和系统架构的方法论, 如麦肯锡的解决问题7步法:定义问题、分解问题、排定优先级、制定工作计划、分析问题、综合分析、阐明观点;TOGAF的关键是架构开发方法(Architecture Development Method: ADM),业务流程设计方法、PMP项目管理流程等最佳实践和科学管理方法。
在完成项目规划和高阶论证之后就是系统具体实现的设计的,本文分享和探讨理解需求建立系统模型的部分方法和工具的初步介绍,因个人能力有限, 敬请指正错误。
系统设计的工作通常是业务需求分析师或系统工程师负责,业务需求分析师偏向于业务需求,系统工程师在要求业务分析和系统设计同时具备。当项目中存在系统工程师,通常就代表着有系统方案设计和评审环节,如果是BA负责,就可能会出现将业务方案设计和系统方案混淆并忽略系统设计方案的设计和评审,从业务需求模型直接进入功能详细设计。在这种场景下,开发人员也应提升系统设计的意识和能力,尽可能的从技术和非功能性需求(如扩展性、可维护性)方面补充系统设计。
KEY:系统思维
要进行系统设计,首先要进行系统思维。
系统思维简单地说就是把某个疑问、某种状况或某个难题明确地视为一个系统,也就是视为一组相互关联的实体,将问题具象化为系统实体及其之间关系,建立相应的系统模型。
其它思维模式:批判思维(衡量某个说法的有效性)、分析思维(根据一套规律或原则进行分析)、创新思维等。
整体性原则是系统思维方式的核心。整体原则:每个系统都作为某一个或某些大系统的一部分运作,同时,每个系统中也都包含着更小的一些系统。整体原则要求从整体与部分、整体与环境的相互作用过程来认识和把握整体。
系统思维具体实施过程可以按照以下步骤进行:
- 确定系统及其形式及功能。
- 确定系统中的实体、实体的形式与功能,以及系统边界和系统所处的环境。
- 找出系统内及系统边界处的实体之间所具备的关系,以及那些关系的形式与功能。
- 基于实体的功能以及实体之间的功能互动,来确定系统的涌现属性。涌现:系统在运作时所表现、呈现或浮现出来的东西。包括如功能,漏洞、性能、可靠性、可维护性、可操作性、安全性和健壮性的非功能性需求对应的属性。
关于系统架构及思维方面的介绍推荐阅读《系统架构:复杂产品的设计与开发》。
工具
确定实体方法
确定实体可以下面的顺序进行:
- 确认显性,清晰的实体。
- 用整体思维找出潜在的实体。
- 通过对重点分析,把注意力集中到重要的实体。
- 为实体创建抽象。
- 定义系统的边界,并将其与外界环境隔开。
下面我们举例描述系统设计中实体确认初略过程。
采购寻源功能: 采购寻源是供应商管理系统中的子模块,供应商和采购方通过该流程对可能产生的交易进行前期的交易物的内容规格沟通澄清,达成意向价格。为物品采购提供供应来源。
采购寻源属于交易的范畴,应该交易系统的角度去思考。
从交易或合同的角度去理解最基本元素或者说实体包括:交易物、交易团体、交易类型、价格、数量。交易类型可以分为多头(买入)或空头(卖出)等。
寻源所指价格和具体交易的不同,类似于期货交易,约定的是某个时间和地点的价格, 不同的是寻源所得价格并不等同于交易,仅仅是一个意向。 两者都表达的是在一个特定场景下的价格(比如时间、地点、交易数量、 付款方式等等)。
价格的最终确定需要一个协商过程,需要报价对象来表示形成最终价格的中间价格。报价包含以下价格:采购方提出的价格-出价,供应方提供的价格-索价,两者的中间价、差价,价格协商的轮次等信息。
整体性思考,可以从以下角度考虑考虑:入口、出口、识别系统变化点、非功能性需求等角度考虑。场景的变化多样性需要按照规则和需要设定一些实际业务场景模板-寻源模板,最终生成价格后,价格会被后续的订单模块所使用,需要记录价格使用记录,对于产生价格数据需要有价格管理器负责管理价格生命周期,价格数据需要按照特定的分析场景-分析场景对价格进行分类形成价格目录(或分类)的概念, 对同一目录下的价格进行分析、比较和汇总,应该需要一个价格选择器接口负责实施价格分类的逻辑。
实体名称 | 含义功能 | 可能的设计模式 |
交易物 | 寻源过程协商价格对应的物品,可能是实物或者无实物的服务 | |
交易团体 | 参与寻源过程的组织或团体,包括买方和卖方 | |
场景/场景元素 | 场景代表一个上下文的,场景元素则是形成独立上下文的因素,通常以键值对存在。 | |
价格/价格场景 | 寻源最终输出、指定价格场景下生效的价格。 价格自身通常包括数字、币种、汇率。价格是价格服务的聚合根。 | |
寻源过程/寻源场景 | 对产生价格的流程的抽象, 定义了整个过程的完整步骤、规则等信息。寻源过程是寻源服务的聚合根。 | 工厂模式 |
报价 | 报价是产生最终价格的中间数据、报价通常包括出价、索价。 | |
报价处理器 | 从多分报价中确定最终的价格是业务核心规则、是变化点和扩展点,应提取单独的对象来进行封装并提供扩展性, | 策略模式 |
寻源模板 | 预先定义的场景。 | |
价格使用记录 | 价格的使用应得到记录和校验,保证价格的使用符合价格场景的限定。 | |
价格管理器/价格生命周期 | 价格从产生到失效的完整生命周期应进行清晰定义, 并由专门的管理器对象负责处理影响生命周期的事件和价格生命周期的改变。 | 发布/订阅模式;状态及模式等 |
价格目录/价格分析场景/价格选择器 | 在后续的价格使用或分析过程中需要将价格进行组合分类使用或分析,可以通过价格选择器对象 | 过滤器模式/组合模式 |
表1: 采购寻源实体清单
备注:面向对象的设计方法认为应该将对象的行为封装到对象类中,通过继承/组合等方式扩展。 对于复杂和易变的业务规则应抽象为单独的规则处理器类以隔离稳定部分和变化部分、规则处理器类的设计应使用扩展性设计,如事件监听器、过滤器、策略模式、状态机模式等来应对后续业务规则的变化。
模型:用模型来描述问题和设计、形成各种领域的模型库。
图1: 采购寻源示例模型
使用模型图,来表示功能是设计关键方式,有以下意义:
- 有利于在需求分析阶段和领域专家进行沟通,方便识别系统设计的错误和遗漏点。
- 使用模型图可以在需求、开发、测试保持一致描述语言,避免过多的实现细节或方案的疏漏,保证实现和设计的一致性。
- 使用模型图来描述问题和设计、容易形成共享的、各种领域的模型知识库。
系统需求通常可以抽象为以下的某种通用需求后再进行扩展:
- 团体组织/人员管理
- 观察与测量
- 库存与账务
- 计划
- 交易
应基于通用需求的领域模型进行完善并再次抽象为合适的模型形成模型库为后续使用。
领域模型和领域驱动设计
领域模型是对领域(业务)内的概念类或现实世界中对象进行可视化表示,专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
领域驱动设计是以领域模型为核心的软件开发设计方法。主要内容包括:领域驱动设计的设计语言和领域驱动设计架构
领域驱动设计的设计语言
定义了领域驱动设计所涉及的概念。
领域: 一个组织所做的事情以及其中所包含的一切。当为某个组织开发软件是, 面对的就是这个组织的领域。
子域: 领域包含多个子域。
限界上下文: 语言层面的上下文边界。 一个限界上下文中的子域拥有共同的语言。(这是一种拆分系统、服务的思想,也是DDD的关键),一个限界上下文不一定只包含一个子域。
聚合: 聚合是一些实体为了某项业务而聚类在一起形成的集合,可以认为一个子域对应一个聚合, 一个聚合有一个聚合根。
聚合根: 聚合根:如果把聚合比作组织,聚合根则是组织的负责人,聚合根也叫做根实体,它不仅仅是实体,还是实体的管理者;聚合根是某个子域的唯一入口。
实体\值: 聚合内对象
领域驱动设计以业务领域为核心,建立得到领域专家高度认可和共识的领域模型,保证系统设计能充分理解和实现业务领域的需求。聚合的内容是领域业务。
领域驱动设计任务系统的核心和难点是业务,应以域的概念对业务进行充分理解和聚合,相关业务的代码和数据应高度聚合在整体内(子域)。相比较的就是以功能聚合的系统设计。 以订单为例, 传统功能性设计会存在一个订单团队去支持各种类型如低耗、固定资产、服务的订单,领域驱动设计的理念则是由具体业务的实施团队将订单放置于自身业务的场景下去负责订单方案的设计和功能实施。
现实项目中往往需要综合模块化和领域驱动设计的理念去进行设计,由模块化团队从功能角度思考功能框架的设计,建立通用框架。而那个功能的业务变化点转移到业务领域去思考和实施。要求模块负责人或系统整体负责人要正确的识别两者的边界。
领域驱动设计架构
推荐了领域驱动设计的一些架构实践。
- 系统层次结构中引入领域层
图2: 领域层示意图
- 领域模型核心+适配层提供对外服务的六边形模型
图3: 六边形模型
- 命令和查询分离-CQRS
图4: 命令和查询分离设计模式
- 事件驱动设计
图5: 事件驱动设计系统模型
模块化、服务化、领域驱动设计和微服务
模块化、服务化、领域驱动设计三者都是将系统整体划分成部分的方法。
- 模块化从系统功能对系统进行划分和管理,比如登录模块、用户管理模块、订单模块等,模块化是早期针对独立个体系统的分析方法。
- 领域驱动设计是在面对系统日益增长的复杂度时所产生的软件开发方法,将系统核心从功能的实现转到领域业务后形成的软件设计思想。类似各种业务事业部的组织思维模式。
- 服务化概念是建立在互联网技术的发展上, 软件可以以服务的形式进行集成、进而打破系统是一个独立存在的形式,是整体思维在系统设计上的体现。比如各种中间件服务。
- 微服务是针对系统拆分后的软件架构和具体实施的方法,比如微服务的基础框架(SpringCloud),开发管理方法(敏捷/devops/自动化测试),过程工具(持续集成/持续部署)等。
模块化和领域驱动设计都可以当做进行系统/服务拆分的一种方法。
设计模式和设计原则
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式是设计在编码阶段的语言,蕴涵的设计理念同样可以在系统设计阶段可以去使用。
分类 | 名称 | 使用频率 |
创建型 | 单例模式 | 常用 |
原型模式 | 常用 | |
工厂模式 | 常用 | |
建造者模式 | 低 | |
结构性 | 代理模式 | 常用 |
桥接模式 | 常用 | |
装饰者模式 | 常用 | |
适配器模式 | 常用 | |
门面模式 | 低 | |
组合模式 | 低 | |
享元模式 | 低 | |
行为型 | 观察者模式 | 是 |
模板模式 | 常用 | |
策略模式 | 常用 | |
职责链模式 | 常用 | |
迭代器模式 | 常用 | |
状态模式 | 常用 | |
访问者模式 | 低 | |
备忘录模式 | 低 | |
命令模式 | 低 | |
解释器模式 | 低 | |
中介模式 | 低 |
表2 设计模式列表
设计原则:
- SRP 单一职责原则:一个类对应一个原则。
- OCP 开闭原则:对修改拒绝,对扩展开放‘’
- LSP 里式替换原则:子类可以替换父类的实现,但不能破坏父类定义的协议。
- ISP 接口隔离原则:客户端不应该强迫依赖它不需要的接口。
- DIP 依赖倒置原则:包括控制反转和依赖注入。控制翻转的意思是流程的控制从程序员转为框架,而依赖注入是不用手动创建依赖对象而由框架进行注入。
- KISS(尽量保持简单)、YAGNI (不要做过度设计)原则。
- DRY 原则:不要写重复的代码。
- LOD 原则: 高内聚,松耦合。 迪米特法则:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。
以上原则的说明是从编码角度取阐释, 在系统实体和模块的关系设计上同样适用。
有关设计原则和设计模式相关内容参考《设计模式-可复用的面向对象软件元素》及相关书籍。