秋水逍遥

在咸酸外得真味,于无声处听惊雷

0%

在领域建模的时候,直接对两个实体间的关系进行显式建模,往往会得到更好的结果。

下面,我们通过三个例子详细说明这一点。

实例

实例1:航班客运

实例2:项目管理

实例3:人力资源管理

在很多应用程序中,我们都需要一个配置类Configuration,通常从一个文本文件中读入配置信息,根据配置调整应用的行为。通过这样的方式,我们可以用相同的代码来适应不同的环境,达到灵活性的目标。

本文探索如何设计好这样的配置类。我们的重点不在于设计的产物——配置类——本身,而是在设计中的权衡取舍,以及取舍的原则。

Read more »

“接口属于客户,不属于它所在的类型层次结构。”这是Robert C. Martin在他的名著《敏捷软件开发:原则、模式、实践》中提出来的观点。因此,接口应该表达客户领域的语义,由客户代码定义和控制,并和客户代码一起打包。

Read more »

在开发企业应用的时候,典型的开发范式基本上可以总结为三种:

  • 数据驱动:认为企业应用就是数据的存储和展示。其典型开发方式是“以数据库为中心的增删改查(CRUD)”,或者是Martin Fowler的《企业应用架构模式》中的“表模块”模式。
  • 特性驱动:认为企业应用是系统功能的集合,这些功能基本上是独立实现的。其典型的开发方式是Martin Fowler的《企业应用架构模式》中的“事务脚本”模式。
  • 领域驱动:认为企业应用像机器一样,由多个具有不同能力的零件(对象)组成,这些零件相互配合实现系统的功能。其典型的开发方式是Martin Fowler的《企业应用架构模式》中的“领域模型”模式。

下面先对它们作一个总结性的对比:

数据驱动 特性驱动 领域驱动
软件世界观 软件就是数据的存储和展示 软件是功能特性的集合 软件是由相互协作的智能零件组成的一台能动的机器
核心关注点 数据 功能特性 领域对象
核心模型 数据模型 /关系模型 用例模型 领域模型
业务逻辑组织典型模式 CRUD或表模块 事务脚本 领域模型
业务逻辑实现典型位置 数据库、表示层或缺失业务逻辑 应用层 领域层
重用价值 较低
扩展成本 极高
问题域/解决方案域语义距离 较大

先做一个总结陈词(我不会为这个论断道歉):

在三种开发范式中,数据驱动最差,领域驱动最好,特性驱动介于两者之间。

很不幸的是:在当前企业应用开发中,数据驱动的CRUD方式占了统治地位。

1. 问题域简介

在本文中,我以一个银行账户应用开发为例,说明三种开发范式的典型实现方式以及它们的优缺点。

个人和机构都可能在银行开设一个或多个银行账户。账户的类型都很多种,我们这里只关注储蓄账户和信用账户两种类型。

对每种账户,都可以进行存款、取款或转账等操作,也可以查看当前余额和获取一段时间的对账单。

每种账户在取款或转账时都会有限额。对储蓄账户来说,限额就是不得超过当前账户余额;对信用账户来说,限额就是当前信用额度减去已经刷卡消费的金额。

储蓄账户和信用账户还有很多方面的区别,例如信用卡在取现是需要支付手续费等,但为了减少复杂性,我们的范例中先忽略掉这方面的内容。

下面我们用三种范式分别实现该问题域的解决方案,然后分析各种范式的优劣得失。为了便于比较,三种范式都采用相同的架构——N层应用架构。

2. 数据驱动范式

数据驱动设计范式的软件世界观认为:软件是用于处理数据的虚拟机器。软件开发的核心关注点应该是数据,软件的设计和构建应该围绕数据的存储、检索和展现来开展。

数据驱动设计范式以数据模型为核心和出发点来进行开发。如果采用关系数据库为数据存储媒介,软件的核心就是关系模型,通常表示为E-R图(实体-关系图)的形式。

数据驱动设计范式认为,对数据只有四种可能的操作:增(Create)、删(Delete)、改(Update)、查(Retrieve),简称CRUD。

2.1 数据模型

问题域的表述中有这样一种说法:“账户的类型都很多种,我们这里只关注储蓄账户和信用账户两种类型。”这里暗示了一个类型层级的存在:储蓄账户和信用账户都是一种账户。在“账户”这个概念层级上,它们有相同的行为——存款、取款、转账、获取余额、获取对账单;但在具体细节上,两种账户实现相同的行为时遵循不同的业务规则。

这里的一个重大选择是:

  • 只创建一个统一的账户(Account)实体(对应到数据库中的一张表),用一个鉴别列(例如名为CATEGORY的字符串型列)来区分储蓄账户和信用账户,或者:
  • 将储蓄账户和信用账户分别建模为独立的实体SavingAccount和CreditAccount,不将它们看成是同类的东西。

同样地,银行账户的拥有人可能是个人,也可能是公司、政府部门或其他机构。如果是个人,我们要记录他/她的姓名、身份证号码等;如果是机构,我们要记录其名称、所属地区等等信息。

在数据建模的时候,也有两种选择:

  • 只创建一个统一的Party实体,统一代表个人和机构。用一个鉴别列区分该Party是一个自然人还是一家机构。该表中有些列(例如id和名称)是个人和机构共有的;而一些列是个人特有的(例如性别、身份证号码等),一些列是机构特有的(例如所属地区、组织机构代码等)。或者:
  • 将自然人和机构分别建模为独立的实体Person和Organization,不将两者看成是同类的东西。

每个账户都属于一个持有人(Owner),该持有人可能是个人,也可能是机构。如果采用第一种方式,在Account表(或者SavingAccount和CreditAccount两张表中)只需要定义一个owner_id字段,指向Party表的主键,根据对应的Party的鉴别列可以知道持有人是个人还是机构。如果采用第二种方式,在Account/SavingAccount/CreditAccount表中必须定义三个字段:owner_person_id(自然人id)、owner_organization_id(机构id)和owner_type(持有人类型,有个人/机构两种选择)。如果持有人是个人,owner_person_id的取值为某个人的id,owner_organization_id的取值为null,owner_type的取值为PERSON。;如果持有人是机构,owner_person_id的取值为null,owner_organization_id的取值为某个机构的id,owner_type的取值为ORG。

由于

3. 特性驱动范式

4. 领域驱动范式

本文以按颜色、大小、产地筛选一堆苹果这样一个情景为例,比较非参数化、数据参数化和通过函数式编程实现行为参数化,说明函数式编程的优势。

Read more »

世间的万事万物,不管它们之间的差异有多大,分解到一定的层次,其组成都是相同的。

美女与野兽,在器官层面的组成基本相同(都是由五官、心、肺、肠胃、皮肤等组成,当然形状等方面有一些差异),在细胞层面差异更小,到分子层面差异已经微不足道,到原子层面已经完全相同。

金刚石与氢气,在分子和原子的层面仍有差别,而到了亚原子的层面,其组成已经完全相同,都是由质子、中子和电子组成。

技艺高超的黑客写的代码和实习生写的代码,遵循的开发范式、采用的编程语言等等,看起来有巨大的不同,但是分解到字节的层面则别无二致,位的层面则都是0和1。

具有相同组成的两种事物,由于结构不同,在形态和行为上可能表现出巨大的差异。

金刚石和石墨,都是由碳原子组成的,由于碳原子之间的结合方式即结构方面的不同,两者的硬度不啻天壤之别——石墨非常软,可以制成铅笔笔芯来使用;而金刚石是世界上最硬的物质,比钢铁都要坚硬的多。

植物和人类的遗传物质DNA,都是由相同的四种脱氧核糖核苷酸核苷酸(腺嘌呤、鸟嘌呤、胞嘧啶、胸腺嘧啶脱氧核糖核苷酸)组成的。差别只在于这四种脱氧核糖核苷酸核苷酸的排列顺序和数量。不同的核苷酸顺序决定了植物成为植物,人成为人;决定了奥巴马成为黑人,而布兰妮成为白人;决定了苏东坡的随缘自适,也决定了曹操的心狠手辣(这一点没有定论)。

孙武的女兵通过阵法战胜男兵,全明星足球队往往输给俱乐部队,在在说明一个真理:与组成相比,结构更重要。

计算机程序也一样。采用哪种语言,选用哪种类库和框架,其重要性是有限的。决定你的程序的质量(适用性、扩展性、灵活性、可维护性、性能等等)和生命力的根本因素是它的组织结构的方式。有没有分隔业务代码和技术代码?有没有分离意图和实现?有没有把可变部分隔离于主体代码之外?有没有通过逐层分解识别出可重用组件,有没有通过泛化识别出共同概念与模式?模块之内是否足够高内聚,模块之间是否足够低耦合?对这些问题的回答不同,会造成软件结构上的巨大差异,从而在软件质量上也形成巨大的差异。

软件架构师最主要的工作就是定义软件的结构(在多个维度上分解系统,定义组件间的依赖关系和通信方式等),而技术选型(即软件的组成方面)相对没那么重要。

想起以往面试架构师,问起他们以往的软件架构是怎样的,居然有不少人回答是SSH(Spring + Struts + Hibernate),这些人真是捉错用神了。

DDD是一种设计思想,它本身不绑定到任何一种具体的架构风格,可以应用在多种不同的架构风格中。本文探讨在经典的分层架构中如何应用DDD,以及在DDD的语境下,分层结构每一层的具体职责。

分层架构是企业应用开发中采用率非常高的一种架构风格。它将软件系统的不同职责划分到不同的逻辑层中,并严格定义这些逻辑层的调用顺序。

在《领域驱动设计——软件核心复杂性的应对之道》一书中,DDD范式的创始人Evans提出下图所示的这样一种分层架构:

整个系统划分为基础设施层(Infrastructure)、领域层(Domain)、应用层(Application)和用户接口层(User Interface,也称为表示层)。下面从各个维度分别讨论之。

职责分配

四个逻辑层之间有着明确的职责划分。

领域层(Domain Layer)

领域层实现业务逻辑。

什么是业务逻辑?业务逻辑就是存在于问题域即业务领域中的实体、概念、规则和策略等,与具体的实现技术无关,主要包含下面的内容:

  • 业务实体(领域对象)。例如银行储蓄领域中的账户、信用卡等等业务实体。
  • 业务规则。例如借记卡取款数额不得超过账户余额,信用卡支付不得超过授信金额,转账时转出账户余额减少的数量等于转入账户余额增加的数量,取款、存款和转账必须留下记录,等等。
  • 业务策略。例如机票预订的超订策略(卖出的票的数量稍微超过航班座位的数量,以防有些旅客临时取消登机导致座位空置)等。
  • 完整性约束。例如账户的账号不得为空,借记卡余额不得为负数等等。本质上,完整性约束是业务规则的一部分。
  • 业务流程。例如,“在线订购”是一个业务流程,它包括“用户登录-选择商品-结算-下订单-付款-确认收货”这一系列流程。

对领域层的进一步说明如下:

  • 领域层映射到领域模型,是问题域的领域模型在软件中的反映。
  • 包含实体、值对象和领域服务等领域对象,通常这些领域对象和问题域中的概念实体一一对应,具有相同或相似的属性和行为。
  • 在实体、值对象和领域服务等领域对象的方法中封装实现业务规则和保证完整性约束(这一点是与CRUD模式相比最明显的差别,CRUD中的领域对象没有行为)。
  • 领域对象在实现业务逻辑上具备坚不可摧的完整性,意味着不管外界代码如何操作,都不可能创建不合法的领域对象(例如没有账户号码或余额为负数的借记卡对象),亦不可能打破任何业务规则(例如在多次转账之后,钱凭空丢失或凭空产生)。
  • 领域对象的功能是高度内聚的,具有单一的职责,任何不涉及业务逻辑的复杂的组合操作都不在领域层而在应用层中实现。
  • 领域层中的全部领域对象的总和在功能上是完备的,意味着系统的所有行为都可以由领域层中的领域对象组合实现。

应用层(Application Layer)

应用层定义系统的业务功能,并指挥领域层中的领域对象实现这些功能。

应用层是整个系统的功能外观,封装了领域层的复杂性并隐藏了其内部实现机制。

  • 应用层映射到系统用例模型,是系统用例模型在软件中的反映。
  • 应用层接口描述了系统的全部功能,意味着系统用例模型中的所有用例都可以在应用层接口中找到对应的方法。
  • 应用层实现类不实现业务逻辑,它通过排列组合领域层的领域对象来实现用例,它的职责可表示为“编排和转发”,即将它要实现的功能委托给一个或多个领域对象来实现,它本身只负责安排工作顺序和拼装操作结果。

基础设施层(Infrastructure Layer)

基础设施层为其余各层提供技术支持。

基础设施层是系统中的技术密集部分。它为领域层、应用层的业务服务(例如持久化、消息通信等等)提供具体的技术支持,用户接口层通常使用特定的表示层框架(例如SpringMVC、Struts或Tapestry)实现,但有需要时也可以申请技术设施层提供专门的技术支持。

一些例子:

  • 领域层需要持久化服务,在DDD中,领域层通过仓储(Repository)接口定义持久化需求,基础设施层通过采用JDBC、JPA、Hibernate、NoSQL等技术之一实现领域层的仓储接口,为领域层提供持久化服务。
  • 领域层需要消息通知服务,在领域层中定义了一个NotificationService领域服务接口,基础设施层通过采用手机短信、电子邮件、Jabber等技术实现NotificationService领域服务接口,为领域层提供消息通知服务。
  • 用户接口层需要一个对象序列化服务,将任何JavaBean序列化为JSON字符串,可以在用户接口层定义一个ObjectSerializer服务接口,基础设施层通过采用Gson实现这一接口,为用户接口层提供对象序列化服务。

以上例子都是满足依赖倒置原则,通过控制反转的方式为高层模块提供低层服务,在实践中,可以通过Spring等IoC容器将基础设施层的实现类实例进行依赖注入。

基础设施层的典型实现形式是提供一个一个的类,这些类使用某些专有的技术实现其余各层(主要是领域层)定义的接口,例如提供一个领域层的仓储接口的实现类,使用Hibernate实现持久化,以及提供领域层的通知接口的实现类,使用ActiveMQ广播领域层中发生的事件,等等。

基础设施层也被称为数据源层或数据访问层。这些名称的一个缺点是给读者一个强烈的暗示:基础设施层只负责数据库访问。虽然数据库访问是基础设施层的职责之一,但基础设施层的负责范围比单纯数据库访问宽广的多,它实现了系统的全部技术性需求,例如上面例子中的通知服务和对象序列化服务,等等。

用户接口层(User Interface)

用户接口层为外部用户访问底层系统提供交互界面和数据表示。

用户接口层在底层系统之上封装了一层可访问外壳,为特定类型的外部用户(人或计算机程序)访问底层系统提供访问入口,并将底层系统的状态数据以该类型客户需要的形式呈现给它们。

用户接口层有两个任务:(1)从用户处接收命令操作,改变底层系统状态;(2)从用户处接收查询操作,将底层系统状态以合适的形式呈现给用户。

说明:

  • 典型的用户是人类用户,但是也可能是别的计算机系统。例如如果ERP系统要访问我们的系统获取信息,它也是一种用户。
  • 不同类型的用户需要不同形式的用户接口,例如为人类用户提供Web界面和手机App,为ERP软件用户提供REST服务接口。
  • 不同类型的用户需要不同形式的数据表示,包括表现形式的不同(XML、JSON、HTML)和内容的不同(例如手机App中呈现的数据内容往往比Web页面中呈现的少)。
  • 用户接口层对应用层进行封装,用户接口层的操作与应用层上定义的操作通常是一一对应的关系。用户接口层从外部用户处接受输入,转换成应用层方法的参数形式,调用应用层方法将任务交由底层系统执行,并将返回结果转换成合适的形式返回给外部用户。
  • 用户界面层的典型任务是下面三个:校验——校验外部客户输入的数据是否合法;转换——将外部客户的输入转换成对底层系统的方法调用参数,以及将底层系统的调用结果转换成外部客户需要的形式;转发——将外部客户的请求转发给底层系统。

用户接口层也被称为用户界面层或表示层。

有时候,为了某些需要,我们可以从用户接口层中分离出一个亚层,可命名为门面层(Facade)。位于真正的用户接口层和应用层之间。

门面层(Facade Layer)

门面层隔离前台和后台系统,定义特定于用户接口层的数据结构,从后台获取数据内容并转化为用户接口层的数据形式。

从用户接口层中分离出专门的门面层,具有下面的优势:

  • 使得用户接口层能够独立于后台系统,与后台系统并行开发。

用户接口层通过门面层接口与应用层和领域层解耦,意味着用户接口层可以独立开发,不必等待后台系统的完成,亦不受后台系统重构的影响,在需求调研阶段系统原型出来并得到用户确认之后,就可以开始用户接口层的开发了。可以根据界面原型定义用户接口层需要的数据结构,该数据结构与底层数据结构解耦,不需要知道底层数据类型和数据之间的关联关系。将底层数据和界面数据连接起来并相互转换是门面层实现类的职责,这方面工作可以等待前后台系统分别完成之后进行。

  • 使得分布式部署成为可能。

如果没有门面层的隔离,用户接口层只能直接使用领域层的领域对象作为自己的数据展现结构。这样我们就不能将系统进行分布式部署,将用户接口层和后台系统(领域层、应用层等)分别部署到不同的服务器上。因为在JPA和Hibernate等技术实现中,领域实体绑定到当前服务器的持久化上下文中,必须脱管之后才能够跨越JVM进行传输。更大的问题是事务问题,事务要跨越服务器的边界,复杂性增加,性能严重下降。门面层的存在使得实体和事务都限制在后台系统,不需要扩展到前台服务器。

  • 避免Hibernate中“会话已关闭”的问题,消除成本巨大的“Open Session in View”模式的需要。

在采用JPA或Hibernate作为持久化手段的系统中存在臭名昭著的“会话已关闭”问题,对付这一问题的主要手段是Open Session in View这一存在潜在性能问题的方案。如果不采用门面层隔离后台数据结构,在前端展现数据需要访问实体的延迟初始化属性时就会遇到“会话已关闭”问题,而采用Open Session in View模式处理这个问题就意味着事务不是在后端完成而是扩展到前端用户接口层,在大访问量的网站上会遭遇严重的性能问题并降低吞吐量。采用门面模式的话,有关联关系的数据在后台拼装完毕再一次性返回给前端,事务局限在后端范围,不再有“会话已关闭”和性能问题。

门面层说明:

  • 门面层特定于用户接口层,由用户接口层定义和控制(包括操作和数据的形式和内容),这意味着需要为不同类型的用户接口层开发专门的门面层。
  • 查询结果通常以数据传输对象(DTO)的形式表示。DTO的结构由用户接口层而不是后端决定,代表前端需要的数据形式,与底层数据结构脱耦。一个DTO对象往往由来自领域层中多个相关实体类的数据聚合而成。
  • 通过门面层实现类访问后端的应用层。实现类将后端数据拼装为DTO并返回给前端,它可以将数据装配职责委托给专门的Assembler工具类去执行。
  • 在分布式系统中,可以在前端和后端分别部署门面层。前后端的门面层接口相同,但后端的门面层实现类负责数据装配和发布,前端的门面层实现类负责通过某种通信机制(Web Service等)与后端门面层通讯,获取后者装配好的数据。传输过程中DTO可能序列化为JSON或XML等形式。

综合说明

  • 在四层架构中,领域层和应用层纯粹表达业务意图和机制,不包含任何技术逻辑;而基础设施层和用户接口层纯粹提供技术实现,不包含任何业务逻辑。在业务和技术之间存在清晰的关注点分离。
  • 应用层定义系统的全部业务功能,领域层具体实现这些功能。领域层“动于内”,应用层“形诸外”。
  • 应用层和领域层合在一起代表了整个业务系统,具备概念上的完整性(包含了全部领域概念,实现了全部的业务行为),但不具备实现上的完整性(没有基础设施层的技术支持,系统不具备可运行性;没有用户接口层支持,系统不具备可访问性)。
  • 所有业务逻辑都在领域层实现,业务逻辑泄漏到应用层是一个错误,泄露到基础设施层或用户接口层是严重错误(在用户接口层中实现业务逻辑是采用CRUD模式的常犯的典型错误)。
  • 领域层在履行职责的过程中如果需要技术支持,则在领域层中定义一个表达业务意图的领域服务接口,交由基础设施层采用各种具体技术去实现这一接口。保证领域层(和应用层)不被各种具体技术污染是逻辑分层的第一要务。
  • 判断业务层(领域层和应用层)是否被具体技术污染一个方便的方式是检查它们是否有对具体技术框架(例如Spring和Hibernate)的编译时依赖。业务层代码应该只依赖于JDK(java.*)、Java规范(javax.*),以及一些被广泛使用的类库如commons-lang、Guava、SLF4J、JodaTime等,这些类库本质上可视为对JDK的补充,不是一种具体技术框架。
  • 应用层和门面层的区别:应用层属于后端,门面层属于前端。应用层方法的参数和返回值可以包含领域对象,门面层方法的参数通常是字符串和数字等简单值,返回值是简单值或DTO。以转账操作为例子,应用层中的方法签名是这样的:void transferFund(Account from, Account to, Money amount, Date transferTime),门面层中的方法签名是这样的:void transferFund(String fromAccountNumber, String toAccountNumber, BigDecimal amount, String currency, Date transferTime)。在门面层的实现类中,负责根据账户号码从仓储中获取Account对象,将amount和currency拼装成Money对象,然后以这些对象和transferTime为参数访问应用层中的相应方法。
  • 领域层中的领域对象具有领域通用性或行业通用性,意味着可以在基本相同的领域层上建立不同的应用层(就像三极管、二极管、电容、电阻等在电子工业领域具有通用性,可以用来组装收音机、录音机、电视机等不同应用),应用层是应用特定或客户特定的,只为特定的应用或客户定制。相比应用层,领域层对象具备高度的可重用性。例如一套完备的用户管理领域层模块可以被OA、ERP、CRM、HRM、MES等多个应用重用。因为领域对象中封装了业务逻辑,这种重用是非常有价值的。
  • 可以基于相同的应用层建立不同的用户接口层,例如Web页面,手机App、BI报表、RESTful Web Service等等。

各层间的调用关系与依赖关系

各层在运行时对象调用关系如下图所示:

各层在编译时的依赖关系如下图所示:

图中infrastructure是基础设施层,domain是领域层,application是应用层,facade和facade-impl是门面层(前者是门面接口层,后者是门面实现层),webapp是用户接口层(采用web形式)。

这里有几个关键点:

  • 基础设施层和其他各层的编译时依赖关系和运行时调用关系是相反的:在运行时,其他各层中的对象调用基础设施层中的对象实例,使用后者提供的服务;而在编译时,基础设施层中的类依赖于其他各层(主要是领域层)中的类。这是通过运用面向对象原则中的依赖倒置原则达到的,在领域层中定义服务接口,而在基础设施层中实现领域层定义的接口。在运行时,通过Spring等IoC容器向领域层中服务接口的客户类注入基础设施层中定义的实现类的实例。依赖倒置原则保证了领域层是主,基础设施层是仆。正如同建筑工程建设中的甲方和乙方,虽然甲方使用乙方提供的产品和服务,但乙方要完成的内容是由甲方指定和控制的,是乙方依赖于甲方,而不是甲方依赖于乙方。
  • 代表业务的层(领域层和应用层)不依赖于代表技术的层(基础设施层和用户接口层),代表技术的层依赖于代表业务的层。这是天经地义的——业务提出需求,技术实现业务提出的需求。在保持业务层不变的前提下,可以变更技术层,实现可替换性——例如原来的基础设施层采用MyBatis实现持久化,可以自由替换为用JPA实现持久化;又例如原来的用户接口层基于Swing,可以自由替换为Web界面——在两种情况下,都不需要修改业务层。
  • 领域层处于整个系统的核心位置,它在概念上不依赖于其他各层,其他各层都直接或间接依赖于它。领域层是整个系统的核心引擎,直接实现业务目标,攸关业务正确性、可靠性、灵活性和扩展性。应该把最优秀的人才和最大部分的精力放在领域建模和领域层开发上,这才是真正的“领域驱动设计”。
  • 领域层应该是整个系统中最“胖”的一层,因为它实现了全部业务逻辑并且通过各种校验手段保证业务正确性,其余各层相对都较“瘦”。如果你的代码中不是如此,你肯定走错了路。胖用户接口层是“以数据库为中心的增删改查”模式的典型症状,胖应用层是事务脚本模式的典型症状。
  • 通过定义门面接口(图中的facade),前端和后端概念上互相独立,互不依赖,可以并行开发实现。等到前后端系统都开发完毕,再开发门面实现(图中的facade-impl),将前端和后端系统粘合起来,成为一体。门面层接口的任务是“分”:将前台和后台在概念上分隔开来;门面层实现类的任务是“合”,将前台和后台粘合在一起,成为一个有机统一的完整系统。

软件开发,尤其是大型软件开发是非常复杂的,涉及非常多的组件元素,需要作出大大小小的各种决策。软件开发的根本矛盾是不断增长的系统规模和复杂性与开发人员有限的大脑容量和处理速度之间的矛盾。:)

为了了解软件开发的全貌,让我们上升到3万米的高空,对软件开发内容做一个全景式的鸟瞰,对我们要了解和实现的东西进行一个高层的切分。

两个维度

范围维度

软件开发的主要目的是为其他行业的业务问题提供技术解决方案,例如为航空公司的飞机实现自动导航,为汽车公司的汽车生产进行产销平衡,等等。从范围上看,软件开发所覆盖的范围可以划分为两个领域:我们要开发的软件系统属于解决方案域,而软件系统来自于和最终服务于的业务领域(例如飞机导航、汽车生产等等)属于问题域。问题域存在问题,软件系统为问题域的问题提供技术上的解决方案。

这就是我们看待软件开发的第一个维度:范围维度。我们把软件开发的范围划分为问题域和解决方案域。处于问题域中的时候,我们的任务是认识世界,即了解在没有软件系统存在的情况下,现实业务领域是如何运作的;而处于解决方案域中的时候,我们是在改造世界,即通过设计和实现软件系统帮助业务领域运作得更好(更大、更快、更赚钱、更省钱、更透明、更灵活……)。

在软件开发的术语中,“业务”、“领域”是问题域的同义词,“系统”是解决方案域的同义词(“系统”是“软件系统”的简称)。因此,“业务”分析师、”业务”规则、“业务”用例模型、“领域”模型、“领域”专家聚焦于问题域,而“系统”分析师、“系统”用例模型聚焦于解决方案域。

内/外维度

除了范围维度之外,我们还可以从另外的维度看待软件开发,其中一个非常有用的维度是内/外维度。从内/外视角看,系统在我们面前可以划分为外部视图和内部视图。外部视图是系统的黑盒视图,我们看不到它的内部组成和运作机制,只能够看到它呈现给外界的功能;而内部视图是系统的白盒视图,揭示了系统的内部组成和运作机制。

四个象限

范围维度和内外维度两者是正交的,你可以把它们看成两条相互垂直相交的数轴——X轴和Y轴,它们把软件开发问题空间分割为四个区域——用数学的术语表示就是四个象限。

分别说明如下:

问题域的外部视图——业务用例模型

这个象限从外部视角看待问题域,分析探究业务系统实现了哪些功能,对它的用户提供了哪些业务价值?每一项系统功能体现为一个业务用例。例如银行储蓄业务为储户提供存款、取款、转账和提供对账单等等服务,每一项服务就是一个业务用例。

开发业务用例模型不是软件开发的必须步骤,但本人强烈建议不要忽略这一方面的工作。一方面,开发软件系统的目标就是实现和改进这些业务用例(或业务用例中的某些环节),业务用例为软件开发设定了目标和验收标准;另一方面,通过业务用例分析可以更有效地支持领域建模和系统用例建模,有助于发现领域对象和机制,防止系统用例遗漏或疏失。

问题域的内部视图——领域模型

这个象限从内部视角看待问题域,研究业务领域中固有的组成、结构、行为、机制和规则等等。这方面的工作称为“领域建模”,其产物称为“领域模型”。我们要重点分析研究的是:业务领域中有哪些关键的概念实体(例如银行储蓄业务中的账户、对账单、信用卡、借记卡等等)、各种概念实体之间的关系(关联、泛化、组合、聚合等等),以及它们之间如何交互以实现业务用例模型中规定的各项功能等等。

领域建模是软件开发中最核心的活动,领域模型是软件开发中最有价值的工件。领域建模的质量在很大程度上决定了软件的成败。如果由于对问题域分析理解的偏差,产出错误的领域模型,那么,无论多么先进的技术工具、多么高超的技术手段,都无法挽救软件的失败命运。

解决方案域的外部视图——系统用例模型

这个象限从外部视角看待要构建的软件系统,定义了系统所要实现的所有业务功能,即软件系统能够向它的用户提供的业务价值。这方面的工作称为需求分析(通常只包含功能需求),其产物称为“系统用例模型”,简称为“用例模型”(另一个名字是“功能规格说明书”),每一个系统用例描述一项对用户有价值的系统功能。

系统用例模型定义了软件系统的功能外观,即软件系统能够“干什么”的方面。系统用例模型沟通了两个领域,它定义了要开发的软件系统(解决方案域)必须实现的业务功能(问题域)。

解决方案域的内部视图——设计模型

这个象限从内部视角看待要构建的软件系统,即通过哪些技术组件和怎样的交互手段实现系统用例模型中定义的业务功能?

领域模型中的对象代表问题域中的概念(如信用卡、账户等等),而设计模型中的对象代表软件实体(如记录集、DAO等等)。正如同业务用例模型会映射到系统用例模型一样(通常一个系统用例实现一个业务用例,或者业务用例中的一个步骤),领域模型中的对象也可以而且应该映射到设计模型中的相应软件对象之上,这意味着设计模型中也应该有信用卡、账户这样的对象,它们和领域模型中的同名对象一样,拥有相同或类似的属性和行为,这就是《领域驱动设计》一书的主张——使用同一种语言(业务语言)和基于同一个模型(领域模型)进行分析和设计,开发软件应用。

关系

解决方案域以问题域为出发点和依归,即问题决定解决方案(这是理所当然的,不是吗?如果我想求解3乘2这个问题,你却提供3加2这个解决方案,当然是错误的)。开发人员的一个典型的错误是把关注点集中在解决方案域的实现技术上,却忽视了问题分析和问题定义,因此在实现过程中失焦,刻鹄成鹜,画虎类犬。

内部视图“动于内”,外部视图“形诸外”。系统的外部视图定义了外部可见的行为,而内部视图揭示了系统内部的组织结构和运行机制,系统通过内部视图中的结构和机制实现外部视图中向外界呈现的行为。外部视图表达What,即“做什么”,内部视图表达How,即“怎么做”。

设计模型从两方面获得指导:从系统用例模型中获得“What”方面的指导,确定需要实现哪些功能;以及从领域模型中获得“How”方面的指导,确定如何实现那些功能。在分层架构中,用例模型和领域模型分别映射到系统的应用层和领域层;在Clean架构(Robert C. Martin,Clean架构)中,用例模型和领域模型分别映射到系统的用例层和实体层。

时刻记住你当前所处的位置

在软件开发过程中,请时刻记住你当前处于哪一个象限。我们应该严格划分业务(问题域)和技术(解决方案域),意图(外部视角)和实现(内部视角),要防止业务逻辑渗漏到技术组件中(例如在表示层或持久层中维护取款业务规则),也要防止技术逻辑渗漏到业务逻辑中(例如领域层代码依赖Spring或Hibernate,或使用RabbitMQ进行消息通信),还要防止在只应该表示意图的地方直接提供实现方案(例如在应用层中实现业务逻辑)。

当处于问题域中的时候,我们的任务是“发现”,是“认识世界”;当处于解决方案域的时候,我们的任务是“发明”,是“改造世界”。当处于外部视图中的时候,我们是在描述系统的行为和功能;当处于内部视图中的时候,我们是在描述系统的内部组成、结构和实现机制。具体来说:

  • 在业务用例模型中,我们识别和建模业务意图和业务行为;
  • 在领域模型中,我们识别和建模业务实体和业务实现机制;
  • 在系统用例模型中,我们定义待开发的软件系统要实现的行为和功能;
  • 在设计模型中,我们定义待开发的软件系统的内部结构和行为实现机制。