Domain-Driven Design(领域驱动设计,或 DDD)
基础概念
Domain Primitive
是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object 。
1、DP 是一个传统意义上的 Value Object,拥有 Immutable 的特性
2、DP 是一个完整的概念整体,拥有精准定义
3、DP 使用业务域中的原生语言
4、DP 可以是业务域的最小组成部分、也可以构建复杂组合
分析维度:接口的清晰度(可阅读性)、数据验证和错误处理、业务逻辑代码的清晰度、和可测试性。
使用 Domain Primitive 的三原则
1、Make Implicit Concepts Explicit 将隐性的概念显性化
2、Make Implicit Context Explicit 将 隐性的 上下文 显性化
3、Encapsulate Multi-Object Behavior 封装 多对象 行为
什么情况下应该用 Domain Primitive
1、有格式限制的 String:比如 Name,PhoneNumber,OrderNumber,ZipCode, Address 等。
2、有限制的 Integer:比如 OrderId(>0),Percentage(0-100%),Quantity(>=0) 等。
3、可枚举的 int :比如 Status(一般不用 Enum 因为反序列化问题)。
4、Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有业务含义的,比如 T emperature、Money、Amount、ExchangeRate、Rating 等。 5、复杂的数据结构:比如 Map<String, List<Integer>> 等,尽量能把 Map 的所有操作 包装掉,仅暴露必要行为。
实战 - 老应用重构的流程
第一步 - 创建 Domain Primitive,收集所有 DP 行为
第二步 - 替换数据校验和无状态逻辑
第三步 - 创建新接口
第四步 - 修改外部调用
在做架构设计时,一个好的架构应该需要实现以下几个目标:
1、独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚。
2、独立于 UI:前台展示的样式可能会随时发生变化(今天可能是网页、明天可能变成 console、后天是独立 app),但是底层架构不应该随之而变化。
3、独立于底层数据源:无论今天你用 MySQL、Oracle 还是MongoDB、CouchDB,甚至使用文件系统,软件架构不应该因为不同的底层数据储存方式而产生巨大改变。
4、独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变 化。
5、可测试:无论外部依赖了什么数据库、硬件、UI 或者服务,业务的逻辑应该都能够快 速被验证正确性。
事务脚本类的代码很难维护因为以下几点:
问题 1-可维护性能差
可维护性 = 当依赖变化时,有多少代码需要随之改变
1、数据结构的不稳定性:AccountDO 类是一个纯数据结构,映射了数据库中的一个表。 这里的问题是数据库的表结构和设计是应用的外部依赖,长远来看都有可能会改变,比 如数据库要做 Sharding,或者换一个表设计,或者改变字段名。
2、依赖库的升级:AccountMapper 依赖 MyBatis 的实现,如果 MyBatis 未来升级版本, 可能会造成用法的不同(可以参考 iBatis升级到基于注解的MyBatis 的迁移成本)。同 样的,如果未来换一个 ORM 体系,迁移成本也是巨大的。
3、第三方服务依赖的不确定性:第三方服务,比如Yahoo的汇率服务未来很有可能会有变化:轻则API签名变化,重则服务不可用需要寻找其他可替代的服务。在这些情况下改造和迁移成本都是巨大的。同时,外部依赖的兜底、限流、熔断等方案都需要随之改 变。
4、第三方服务 API 的接口变化:YahooForexService.getExchangeRate 返回的结果是小数 点还是百分比?入参是(source, target)还是(target, source)?谁能保证未来接口 不会改变?如果改变了,核心的金额计算逻辑必须跟着改,否则会造成资损。
5、中间件更换:今天我们用 Kafka 发消息,明天如果要上阿里云用 RocketMQ 该怎么办? 后天如果消息的序列化方式从 String 改为 Binary 该怎么办?如果需要消息分片该怎么 改?
问题 2-可拓展性差
可扩展性 = 做新需求或改逻辑时,需要新增/修改多少代码
1、数据来源被固定、数据格式不兼容
2、业务逻辑无法复用
3、逻辑和数据存储的相互依赖
问题 3-可测试性能差
可测试性 = 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量
1、设施搭建困难:当代码中强依赖了数据库、第三方服务、中间件等外部依赖之后,想要 完整跑通一个测试用例需要确保所有依赖都能跑起来,这个在项目早期是及其困难的。 在项目后期也会由于各种系统的不稳定性而导致测试无法通过。
2、运行耗时长:大多数的外部依赖调用都是 I/O 密集型,如跨网络调用、磁盘调用等,而 这种 I/O 调用在测试时需要耗时很久。另一个经常依赖的是笨重的框架如 Spring,启 动 Spring 容器通常需要很久。当一个测试用例需要花超过 10 秒钟才能跑通时,绝大部 分开发都不会很频繁的测试。
3、耦合度高:假如一段脚本中有 A、B、C 三个子步骤,而每个步骤有 N 个可能的状态,当多个子步骤耦合度高时,为了完整覆盖所有用例,最多需要有 N * N * N 个测试用 例。当耦合的子步骤越多时,需要的测试用例呈指数级增长。
防腐层(ACL)Anti-Corruption Layer
1、适配器:很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式, 可以将数据转化逻辑封装到 ACL 内部,降低对业务代码的侵入。 2、缓存:对于频繁调用且数据变更不频繁的外部依赖,通过在ACL里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的, 通过将缓存逻辑嵌入 ACL,能够降低业务代码的复杂度。
3、兜底:如果外部依赖的稳定性较差,一个能够有效提升我们系统稳定性的策略是通过ACL起到兜底的作用,比如当外部依赖出问题后,返回最近一次成功的缓存或业务兜 底数据。这种兜底逻辑一般都比较复杂,如果散落在核心业务代码中会很难维护,通过 集中在 ACL 中,更加容易被测试和修改。
4、易于测试:类似于之前的 Repository,ACL 的接口类能够很容易的实现 Mock 或 Stub,以便于单元测试。
5、功能开关:有些时候我们希望能在某些场景下开放或关闭某个接口的功能,或者让某个接口返回一个特定的值,我们可以在ACL配置功能开关来实现,而不会对真实业务代 码造成影响。同时,使用功能开关也能让我们容易的实现 Monkey 测试,而不需要真正物理性的关闭外部依赖。
网友评论