什么是模型

为了创建真正能为用户活动所用的软件,开发团队必须运用一整套与这些活动有关的知识体系。所需知识的广度可能令人望而生畏,庞大而复杂的信息也可能超乎想象。模型正是解决此类信息超载问题的工具。模型这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题。
—— Erics Evans
利用抽象化繁为简,通过标准的结构来组织和传递信息,形成一致的可以进行推演的解决方案,这就是“模型”。
从问题域到解决方案域,或许有多种途径或手段,然而针对复杂问题域,通过建立抽象的模型来映射现实世界的多样性,就好似通过数学公式来求解一般,是实践证明可行的道路:
image.png
建模过程与软件开发生命周期的各种不同的活动(Activity)息息相关,它们之间的关系大体如下图所示:
image.png
每一次建模活动都是对知识的一次提炼和转换,产出的成果就是各个建模活动的模型。

  • 分析活动:观察现实世界的业务需求,依据设计者的建模观点对业务知识进行提炼与转换,形成表达了业务规则、业务流程或业务关系的逻辑概念,建立分析模型
  • 设计活动:运用软件设计方法进一步提炼与转换分析模型中的逻辑概念,建立设计模型,使得模型在满足需求功能的同时满足更高的设计质量。
  • 实现活动:通过编码对设计模型中的概念进行提炼与转换,建立实现模型,构建可以运行的高质量软件,同时满足未来的需求变更与产品维护。

整个建模过程如下图所示:
image.png
一个完整的建模过程,就是模型驱动设计(Model-Driven-Design)

表达方式

模型驱动设计可以分为两个不同的维度来表现模型,即建模视角与建模活动。不同的建模视角驱动出不同的抽象模型,而不同的建模活动,也会获得不同抽象层次的模型。这两个维度表达的模型驱动设计如下图所示:
image.png

  • 如果我们是以数据为核心,关注数据实体的样式和它们之间的关系,由此建立的模型就是“数据模型”。
  • 如果我们需要为系统外部的客户端提供服务,关注的是客户端发起的请求以及服务返回的响应,由此建立的模型就是“服务模型”。
  • 领域驱动设计则强调以领域为中心,通过识别领域对象来表达业务系统的领域知识包括业务流程、业务规则和约束关系,由此建立的模型就是“领域模型”。

Eric Evans —— 模型驱动设计是领域驱动设计中的一种模式。
张逸 —— 领域驱动设计不过是模型驱动设计中的一种罢了。
我 —— 模型驱动设计是领域驱动设计的一种实现模型,领域驱动设计是模型驱动设计的一种表现形式。

数据分析模型

数据建模过程中的分析活动会通过理解客户需求寻找业务概念建立实体(Entity)。在数据模型中,一个实体就是客户希望建立和存储的信息。这是一个抽象的逻辑概念,位于数据模型的最高抽象层。
一个实体不一定恰好对应一个数据表,它甚至可能代表一个主题域。在识别实体的同时,我们还需要初步确定实体之间的关系。由于这个过程与数据库细节完全无关,因而称之为对数据的“概念建模”,建立的模型称之为实体关系模型
在分析阶段对实体做进一步细化,形成具体的数据表,并定义表属性,确定数据表之间的真实关系。这时获得的分析模型称之为“数据项模型”。实体关系模型数据项模型之间的关系如下图所示:
image.png
在数据建模过程中,越早确定数据库的细节越有利于数据模型的稳定。

关系数据库的数据项模型

关系数据库体现了关系模型,形成了一种扁平的结构化数据,这就要求进一步规范数据表的粒度,将实体或主题域拆分为多个遵循数据库范式的数据表,明确一个数据表的主要属性。
数据库范式是面向数据的分析建模活动的一个关键约束。这些范式包括一范式(1NF)、二范式(2NF)、三范式(3NF)、BC 范式(BCNF)和四范式(4NF)。遵循这些范式可以保证数据表属性的原子性、避免数据冗余和传递依赖等。
建立数据项模型需要考虑:

  • 访问关系数据库的性能特性,从而决定数据的粒度与分割。
  • 数据访问的频率。

    NoSQL 的数据项模型

    由于 NoSQL 数据库是一种无样式的数据结构(Schemaless Data Structures),这使得它对数据项模型的约束是最少的。诸如 MongoDB、Elasticsearch 这样的 NoSQL 数据库,它所存储的 JSON 文档,可以在属性中进行任意嵌套,形成一种能够自由存取的文档结构。所以 Martin Fowler 又将这样的 NoSQL 数据库称之为“文档型数据库”。

    总结

    通过分析活动获得的数据项模型,可以认为是数据分析模型,它确定了系统的主要数据表、关系及表的主要属性。

    数据设计模型

    到了建模的设计活动,就可以继续细化数据项模型这个分析模型,例如丰富每个表的列属性,或者确定数据表的主键与外键,确定主键的唯一性策略,最后将数据表映射为类对象。

    数据模型与对象模型

    虽然员工与客户都定义了诸如 country、city 等地址信息,但它们是分散的,并被定义为数据表提供的基本类型,无法实现两个表对地址概念的重用。对象模型就完全不同了,它可以引入细粒度的类型定义来体现丰富的领域概念,封装归属于自己的业务逻辑,同时还提供了恰如其分的重用粒度:
    image.png

    NoSQL数据设计模型

    一个订单对象在关系数据库中需要被分解为多张数据表,但对于诸如 MongoDB、Elasticsearch 这样的数据库,则可以认为是一个聚合。因此,在设计 NoSQL 的数据模型时,可以运用领域驱动设计中聚合的设计原则。
    image.png

    数据实现模型

    SQL与存储过程

    数据量小的时候,为了优化性能使用存储过程;数据量大的时候,存储过程的性能优化已经解决不了问题,带来的系统复杂度也是得不偿失。
    根据 AKF 的立方体模型,若要实现系统的可伸缩性(Scalability),可以从三个维度对系统的对应层次进行分割:
    image.png
    评估立方体的三个维度:

  • X 轴的可伸缩性代表了数据或服务的复制与负载均衡:数据层采用数据库集群以及读写分离来保证。此时,存储过程不会受到 X 轴分割的影响。

  • Y 轴的可伸缩性代表了服务的切分:相当于采用微服务架构。为了避免数据库出现性能瓶颈,应遵循微服务的设计原则,保证每个微服务有专有的数据库。若拆分前的单体架构使用了存储过程,可能包含大量多表关联,当迁移到微服务架构时,可能因为分库的原因,需要对存储过程做出修改和调整。
  • Z 轴的可伸缩性代表了数据的分片:这意味着需要针对同类型的数据进行分库,如果 SQL 封装在服务或数据访问对象中,可以通过类似 ShardingSphere 这样的框架对 SQL 进行自动解析,满足分库分表的能力;但如果是存储过程,就会受到影响。

软件系统一旦上线投入到生产环境,最难以更改的其实是数据库,包括数据库样式和已有的数据。无论是升级还是迁移,数据库都是最复杂的一环。为了解决这一问题,就需要对数据库脚本进行版本管理。无论变更是大还是小,是影响数据库样式还是数据,每次变更都应该放在单独的脚本文件中,并进行版本控制。这就相当于为数据库标记了检查点(Checkpoint),使得我们既可以从头开始部署数据库环境,也可以从当前检查点开始升级或迁移。像 FlywayDB 框架就规定每次数据库变更都定义在一个单独的 SQL 文件中,文件名前缀为:V{N}__,N 代表版本号。例如:
image.png

对象关系映射

数据表与对象之间的关系:建立与数据表完全一一对应的类模型。
根据模型驱动设计的核心原则,即它是按照数据表与关系来建模的
业务 —— 服务、数据访问对象、持久化对象协作完成,三者关系:

  • 持久化对象就是数据表的映射,并合理处理数据表之间的关系;
  • 数据访问对象负责访问数据库,并完成返回结果集如 ResultSet 或 DataSet 到持久化对象的映射;
  • 服务利用事务脚本或者存储过程来组织业务过程。

    数据模型驱动设计的过程

    整个数据模型驱动设计的重头戏都压在了分析模型上。通过对业务知识的提炼,识别出实体和数据表,并正确地建立数据表之间的关系成为了数据建模最重要的工作。
    image.png
    对于简单的业务系统而言,设局模型驱动设计无疑是简单高效的设计方法。
    除了数据库性能优化与数据库设计范式之外,它对团队开发人员几乎没有技术门槛,只需掌握三个技能:一门语言的基本语法和编程技巧、一个 ORM 框架的使用方法及基本的 SQL 编写能力——就这三板斧,足矣!
    这也正是为何数据模型驱动设计能够大行其道的主要原因,无他,门槛低而已。

    案例

    数据分析模型

    确定实体关系模型:
    image.png
    深化实体关系模型,确定数据表以及数据表之间的关系和属性 —— 数据项模型:
    image.png

    数据设计模型

    业务功能 —— 服务、数据访问对象、持久化对象 —— 设计模型:
    image.png

    数据实现模型

    持久化对象(贫血对象):
    import lombok.Data;
    import java.sql.Timestamp;
    import java.util.List;
    import java.util.UUID;

    @Data
    public class Order {
    private String id;
    private Student student;
    private OrderStatus status;
    private Timestamp placedTime;
    private Timestamp createdAt;
    private Timestamp updatedAt;
    private List orderItems;
    public Order() {
    this.id = UUID.randomUUID().toString();
    }
    public Order(String orderId) {
    this.id = orderId;
    }
    }

    import java.sql.Timestamp;

    @Data
    public class OrderItem {
    private String id;
    private String orderId;
    private Training training;
    private Timestamp createdAt;
    private Timestamp updatedAt;
    }
    数据访问对象:
    import org.apache.ibatis.annotations.Param;
    import xyz.zhangyi.practicejava.framework.mybatis.model.Order;
    public interface OrderMapper {
    Order getOrder(@Param(“orderId”) String orderId);
    }
    服务对象:
    @Component
    @Transactional
    @EnableTransactionManagement
    public class OrderService {
    private OrderMapper orderMapper;

    @Autowired
    public void setOrderMapper(OrderMapper orderMapper) {
    this.orderMapper = orderMapper;
    }

    public Order getOrder(String orderId) {
    Order order = orderMapper.getOrder(orderId);
    if (order == null) {
    throw new ApplicationException(String.format(“Order by id %s is not found”, orderId));
    }
    return order;
    }
    }
    只要数据访问对象实现了各种丰富的数据表访问功能,服务的实现就会变得相对容易