martin fowler 老爷子的《企业应用架构模式》一书在江湖上流传已久, 在十几年前就企业应用中的典型场景及设计模式进行了思考和总结, 可以看到书中提及的常用模式在如今流行的企业应用框架中已经落地. 近日拜读, 受益不少, 将一些感悟和共鸣记录下来, 整理下, 不全面也不深入, 只便于后续乱翻书.
写在前面
行文知其思维, martin 老爷子的书写起来条理清晰, 层次分明, 易于理解, 非常值得称道, 本文借鉴 martin 先生的行文模式, 每一种模式均包含如下几部分: 模式概要, 我的理解, 项目实践. 希望通过后面两部分的介入, 尝试将对应要点落地.
知识概要
该书第一部分 "表述" 对书中提及的各种模式及知识要点做了概括性介绍. 主要从抽象层面介绍了企业应用中遇到的架构问题及常见的解决思路. 涉及到: 分层架构思想, 领域逻辑组织, ORM,web 表现层, 并发, 会话状态及管理, 分布式相关等.
企业应用
企业应用时将计算机技术这一生产力作用于现实世界的表现形式. 一个企业应用的设计需要考虑清楚该应用的业务目标, 受众人群等. 企业应用一般有如下特点:
需要持久化数据. 采用何种持久化介质? 如何持久化?
涉及大量数据. 数据存取的高效性? 数据容量? 存储介质如何支持数据的快速增长?
多人同时访问数据. 并发问题? 服务可用性? 用户会话管理?
大量用户交互. 交互方式? 服务可用性?
同其他企业应用之间的集成. 系统集成形式? 如何降低耦合? 快速集成?
斜线部分是我想到的一些关注点.
架构
关于 "架构" 的定义, 众说纷纭, martin 认为可以统一的两点有:
最高层次的系统分解;
系统中不易改变的决定.
在不同人看来, 在不同的上下文下, 在不同的系统生命周期时, 对于 "架构" 的理解是不一样的. 重要的是能够因时因地地选择合适的架构模式和设计.
分层
通常将系统分为多层, 层与层之间约定好契约, 下层对上层按照契约提供服务.
分层是最经典也是最常见的一种架构思想, 在网络协议的设计中, 在应用系统的架构设计中, 都使用到了分层的设计思想.
分层可以带来如下好处可以概括为: 层内部内聚, 层之间解耦. 层内部的内聚可以专注于本层的核心逻辑, 层之间解耦, 降低层与层之间的耦合, 可以替换其中某一层的实现而不对其他层产生副作用.
分层同样可能带来副作用: 人为引入的 "分层" 会给开发增加一定工作量, 同时可能带来一定的性能损耗.
我的理解:"分层" 及其他架构模式都是人为引入的, 目的是为了让人更好地理解和维护程序和系统. 对于计算机而言, 它进行资源分配和调度的单位是进程, 它只知道有多少个进程, 每个进程使用了哪些资源, 还需要哪些资源; 对于计算机来说, 它并不也不需要知道即将执行的二进制流涉及到多少分层, 为什么要这么分层, 对于机器来讲, 它只认二进制; 但是由于代码是人写的, 系统是人搭建的, 需要人来维护, 因此, 通过 "分层" 能够让人更好地理解程序, 更好地理解系统设计, 提高人与人的沟通效率, 提升系统可维护性.
模式
"模式" 一词大家都在用, 简单来讲就是我们通过实践发现的一些有价值的设计或者解决方案, 这些方案能复制到类似的问题域中很好地解决问题, 从而提升生产效率, 他是通过实践找到的捷径. 书中给出了 Chirstopher Alexander 给出的定义, 我觉得很好:
模式描述一个不断重复发生的问题以及该问题解决方案的核心, 这样我们能够使用该方案二不必做重复劳动.
所以, 一个模式包含如下关键部分: 问题上下文, 核心解决方案. martin 的书在讲后续的每个模式的时候, 也据此将模式分成了几部分来介绍: 模式名称, 意图和概要, 运行机制, 使用时机, 进一步阅读.
模式总结
事务脚本
模式概要: 事务脚本使用过程来组织业务逻辑, 每个过程处理来自表现层的单个请求, 可以适用于简单的业务场景, 可以加快开发速度, 去掉繁琐的分层.
我的思考: 事务脚本并不是指现阶段很多项目中出现的 "面条式" 代码, 而是根据简单业务场景简单处理, 不引入复杂分层, 一个过程走到底, 对于简单 / 临时性的业务应用, 可以快速开发 / 测试, 省去了繁琐的框架搭建.
项目实践: 实际项目中并没有遇到可以应用事务脚本的场景, 通常意义上的企业应用, 业务逻辑都不会太简单, 也不是临时性的项目.
领域建模
模式概要: 合并了行为和数据的领域对象模型, 核心在于将易变的业务逻辑内聚在领域模型中.
我的思考: 领域建模应当是开发人员 / 架构师需要加强的能力, 通过领域建模对涉及的业务领域有更深入的了解, 同时合理的建模, 确保业务逻辑内聚, 使企业应用更易于维护和迭代.
这方面的理论知识可以参考 Eric Evans 的《领域驱动干设计 - 软件核心复杂性应对之道》, 实践相关的内容可以参考 Vaughn Vernon 的《实现领域驱动设计》, 也可以参考我的系列博客[DDD] 使用领域驱动设计思想实现业务系统.
初学者在实践 DDD 的时候, 首先需要改变思维方式, 业务领域的分析和建模是关键, 通过不断的实践总结, 形成自己的一套完整的建模战术.
另外, DDD 对于复杂性较高的应用系统优势更加明显, 我们团队在用户系统和社区系统都进行了 DDD 的实践, 发现相比用户系统, DDD 在社区系统的优势发挥的更充分.
最后, DDD 需要不停地实践, 不要追求一步到位, 模型可以不断地迭代完善, DDD 的实践也是如此.
项目实践: 在用户体系和社区服务系统中均有实践, 可以参考我的系列博客[DDD] 使用领域驱动设计思想实现业务系统.
标识域
模式概要: 为了在内存对象和数据库之间维护标识而在对象内保存的一个数据库标识域. 标识域满足两个特性: 唯一性, 不可变性.
我的思考: 在数据库中通常存在两种类型的唯一且不可变键, 一个是业务主键, 一个物理主键, 那么应当使用哪个主键来作为标识域呢?
可以根据 DDD 中介绍的实体和值对象来做区分, 如果是实体, 那么建议是用业务主键, 比如 "User" 和 "Order", 分别可以使用 userNo 和 orderNo 来标识. 而对于值对象, 可以直接使用其物理主键作为标识域, 比如 "用户点赞信息", 可以使用物理主键 id 作为标识域, 当然也可以使用业务联合主键 (userNo 和 postNo) 作为标识域, 但是会增加复杂度, 不可取
另外, 通常情况下, 我们可以将物理主键命名为以 Id 结尾, 将业务主键命名为 No 结尾;
很多服务场景, 需要将实体的标识域暴露给调用方, 就要考虑安全性问题, 如果你的标识域是顺序递增的 long 型主键, 那么很可能会被攻击者遍历, 从而带来一些安全风险, 这时候可以做如下两种考虑: 标识域不再使用顺序递增的 long 型主键, 而是使用不可遍历的 uuid 等; 如果没法将标识域更改为 uuid, 那么考虑新建一个域, 存储专门供外部使用的 uuid 值. 比如: 我们在用户系统中便为 User 创建了一个使用 uuid 值的 UnionId 字段.
不建议前后端将标识域明文传递, 尤其是越权访问会带来数据泄露问题的场景, 比如: 查询用户信息, 这时候实体的标识域应当考虑从会话中获取, 避免越权访问带来的数据风险
项目实践: 订单系统中, 我们使用业务主键 orderNo 作为 Order 实体的标识域, 且由于 orderNo 形式为: yyyyMMddHHmmssSSS+sequence, 被遍历的成本非常高, 因此直接暴露在外使用.
外键映射
模式概要: 把对象间的关联映射到表间的外键引用.
我的思考: 外键映射适用于: 1:1 及 1:N 的关联关系, 通常让非 root 实体持有 root 实体的标识域, 比如唱片持有作者的标识域, 曲目持有唱片的标识域.
项目实践: 社区系统中的 "帖子" 实体持有 "用户" 实体的标识域, 在数据库中则表现为 Post 表持有一个 userNo 字段.
关联表映射
模式概要: 把关联保存为一个表, 带有指向表的外键.
我的思考: 外键映射适用于: N:N 的关联关系, 关联表通常对应一个值对象. 关联表通常存在两个方向的查询入口, 这两个入口跟关联表外键对应的实体表有关, 那个在 DDD 中, 该关联表就可以同时属于两个 "聚合" 中. 比如用户体系系统中 "用户账户关系表"(UserAccount), 作为值对象, 持有 userNo 和 accountNo; 存在根据 userNo 查询 accounts 的场景, 也存在 accountNo 查询 UserAccount 的场景; 可以看出 UserAccount 属于 User 和 Account 这两个 "聚合" 中.
项目实践: 用户体系系统中 "用户账户关系表", 作为值对象, 持有 userNo 和 accountNo.
单表继承
模式概要: 将类的继承层次表示为一个单表, 表中各列代表不同类中的所有域.
我的思考: 书上列出了如下优点: 只需关注一张表, 对象继承层次更改时无需更改存储层; 缺点也很明显: 数据库空间浪费, 维护成本增加, 可扩展性差. 实际的项目经验表明, 最好少用这种模式, 比如在社区系统中, 多个渠道发布的评论属性不一样, 开发人员将多个渠道的评论属性整合放入到一张表中, 且文档注释不全, 导致后续开发人员踩坑, 在写程序时不清楚每个字段的实际适用渠道, 维护起来非常麻烦. 而且后续某些渠道下线后, 该大表中多余字段仍然保留, 造成了极大的空间浪费.
项目实践: 社区系统中的评论表, 存储来自多个渠道的评论内容, 且缺乏注释, 业务代码混乱, 导致难于维护.
类表继承
模式概要: 将各个子类的公共属性放入一张父表中, 子类的非公共属性放入各自的子表中.
我的思考: 该模式最大的优点在于: 领域模型和数据库结构之间关系一致, 利于理解; 缺点在于: 读取一个对象需要多表访问, 性能上需要注意.
项目实践: 无.
具体表继承
模式概要: 每个子类对应一个具体表, 多个具体表之间可能存在相同的字段.
我的思考: 该模式最大的优点在于: 相比 "类表继承" 获取完整对象时, 无需表连接查询; 每个表都是自包含的, 无不相关域; 缺点在于: 领域内全局主键较难处理; 类继承层次该表对表结构影响较大. 全局主键的问题可以考虑使用: Id+type 这种联合主键的形式来解决, 每个子表使用独立的 seq, 每个子表定义一个类型字段, 映射到模型时, 使用 Id+type 作为全局唯一主键; 另外一种方案是: 使用全局的 seq, 所有子表共用一个 seq, 坏处在于每个子表 id 的不连续性; 公共 seq 可能成为资源瓶颈.
项目实践: 用户体系的 ** 账号表和理财账号表就是使用这种模式来建表的, 当然现在是对 ** 账号和理财账户进行独立建模, 所以影响倒不是很大, 但是此种模式的一个缺点较为明显: 每次需要查询某个用户下的所有账户列表时, 需要同时查询 ** 账号和理财账号表, 如果使用 "类表继承" 模式, 则可以避免多表查询, 因为在这个查询场景下, 仅仅需要查询账户的基本信息: 账号, 开户日期, 这些属性是所有账号共有的, 因此完全可以从父表中获取.
资源库
模式概要: 协调领域和数据映射层, 使用类似于集合的接口来访问领域对象.
我的思考: 资源库 repository 是一个很常见的术语, 它可以理解为一个存储层的 gateway, 也可以理解为一个存储层的 facade,repository 提供面向对象的查询原语, 可以参考 Elasticsearch 等内存数据库的 API 实现. 在存在多个存储介质的系统中个, 可以使用 repository 包装多个存储介质的实现, 比如整合 Redis 缓存和 oracle 主存配合使用的数据存取过程; 还可以有基础自 repository 的哑实现, 使用领域模型不必依赖于存储层也可以快速完成建模.
项目实践: 用户体系和社区系统中均有使用到, 使用场景见上面描述. 课参考:[DDD] 领域驱动设计实践 -- 一些问题及想法 中的 "2,repository 的实现" 一节.
Web 表现模式
模式概要: 经典的 MVC 模式, 将'视图(View)','模型(Model),'控制器(Controller)'分离.
我的思考: MVC 是非常经典的设计模式, 从 struts 到 springmvc, 研究 springmvc 足矣, 囊括了 filter,inteceptor 等基本组件.
项目实践: Web 类项目基本逃不掉 MVC, 不再赘述.
来源: https://www.cnblogs.com/daoqidelv/p/8540341.html