浙江省住房和城乡建设厅网站打不开,食品网站建设风格,北京各大网站推广服务公司,天津做app和网站的公司作用:
通过规定一个固定的架构设计#xff0c;可以让团队内有一个统一的开发规范#xff0c;降低沟通成本#xff0c;提升效率和代码质量。
目标#xff1a;
在做架构设计时#xff0c;一个好的架构应该需要实现以下几个目标#xff1a;
独立于UI#xff1a;前…作用:
通过规定一个固定的架构设计可以让团队内有一个统一的开发规范降低沟通成本提升效率和代码质量。
目标
在做架构设计时一个好的架构应该需要实现以下几个目标
独立于UI前台展示的样式可能会随时发生变化今天可能是网页、明天可能变成console、后天是独立app但是底层架构不应该随之而变化。独立于底层数据源无论今天你用MySQL、Oracle还是MongoDB、CouchDB甚至使用文件系统软件架构不应该因为不同的底层数据储存方式而产生巨大改变。独立于外部依赖无论外部依赖如何变更、升级业务的核心逻辑不应该随之而大幅变化。可测试无论外部依赖了什么数据库、硬件、UI或者服务业务的逻辑应该都能够快速被验证正确性。
贫血模型架构下的问题
贫血模型下的跨币种转账流程图
一个应用最大的成本一般都不是来自于开发阶段而是应用整个生命周期的总维护成本所以代码的可维护性代表了最终成本。
贫血模型下通常会在业务处理层中将流程按方法模块进行拼接组成一个完整的业务动作。这种写法在功能上没有什么问题但是长久来看有以下几个很大的问题可维护性差、可扩展性差、可测试性差。
可维护性当依赖变化时有多少代码需要随之改变
提高数据结构的稳定性Service 层不能直接使用 DO避免 DO 变化影响到 Service 层。DO类是一个纯数据结构映射了数据库中的一个表表结构和设计是应用的外部依赖长远来看都有可能会改变比如数据库要做 Sharding或者换一个表设计或者改变字段名等。依赖库的升级Service 层不能直接使用 Data Access LayerDAL层。例如 xxxMapper 依赖 MyBatis 的实现如果MyBatis未来升级版本可能会造成用法的不同可以参考iBatis升级到基于注解的MyBatis的迁移成本。同样的如果未来换一个ORM体系迁移成本也是巨大的。第三方服务依赖的不确定性轻则API签名变化重则服务不可用需要寻找其他可替代的服务。在这些情况下改造和迁移成本都是巨大的。同时外部依赖的兜底、限流、熔断等方案都需要随之改变。中间件更换中间件的升级、变更、能力拓展等问题。
上述几点都表达的一点需要将依赖解耦。
可扩展性做新需求或改逻辑时需要新增/修改多少代码
数据来源、格式的变化业务逻辑无法复用贫血模型下数据格式变更带来的不兼容问题会导致核心业务逻辑无法复用。每个用例都是特殊逻辑的后果是最终会造成大量的if-else语句而这种分支多的逻辑会让分析代码非常困难容易错过边界情况造成bug。逻辑和数据存储的相互依赖当业务逻辑增加变得越来越复杂时新加入的逻辑很有可能需要对数据库schema或消息格式做变更。而变更了数据格式后会导致原有的其他逻辑需要一起跟着动。
可测试性每个需求需要增加的测试用例数 与 编写测试用例的耗时
设施搭建困难当代码中强依赖了数据库、第三方服务、中间件等外部依赖之后想要完整跑通一个测试用例需要确保所有依赖都能跑起来这个在项目早期是及其困难的。在项目后期也会由于各种系统的不稳定性而导致测试无法通过。运行耗时长耦合度高当耦合的子步骤越多时需要的测试用例呈指数级增长。假如一段脚本中有A、B、C三个子步骤而每个步骤有N个可能的状态当多个子步骤耦合度高时为了完整覆盖所有用例最多需要有N * N * N个测试用例。
DDD模型下的架构
上面列举了贫血模型带来的问题现我们使用DDD来尝试解决。
抽象数据存储层引入 Repository 和 Entity 新建Entity模型Account实体对象一个实体Entity是拥有ID的域对象除了拥有数据之外同时拥有行为。Entity和数据库储存格式无关在设计中要以该领域的通用严谨语言Ubiquitous Language为依据。新建Repository层对象储存接口类AccountRepositoryRepository只负责Entity对象的存储和读取而Repository的实现类完成Entity - DO 的转换、DB存储的细节。通过加入Repository接口底层的数据库连接可以通过不同的实现类而替换。
抽象第三方服务或中间件引入Anti-Corruption Layer防腐层或ACL ACL中可以做的事情
适配器外部依赖的数据、接口和协议并不符合内部规范通过适配器模式可以将数据转化逻辑封装到ACL内部降低对业务代码的侵入。在这个案例里我们通过封装了ExchangeRate和Currency对象转化了对方的入参和出参让入参出参更符合我们的标准。缓存对于频繁调用且数据变更不频繁的外部依赖通过在ACL里嵌入缓存逻辑能够有效的降低对于外部依赖的请求压力。同时很多时候缓存逻辑是写在业务代码里的通过将缓存逻辑嵌入ACL能够降低业务代码的复杂度。兜底当外部依赖稳定性较差时可以通过 ACL 层实现兜底的功能。比如当外部依赖出问题后返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑一般都比较复杂如果散落在核心业务代码中会很难维护通过集中在ACL中更加容易被测试和修改。易于测试类似于之前的RepositoryACL的接口类能够很容易的实现Mock或Stub以便于单元测试。功能开关在某些场景下开放或关闭某个接口的功能或者让某个接口返回一个特定的值我们可以在ACL中配置功能开关来实现而不会对真实业务代码造成影响。同时使用功能开关也能让我们容易的实现Mock测试而不需要真正物理性的关闭外部依赖。
封装业务逻辑通过Entity、Domain Primitive和Domain Service封装 用 DP 封装跟实体无关的无状态计算逻辑
ExchangeRate 中封装汇率计算逻辑返回应转入的金额对象
用Entity封装单对象的有状态的行为包括业务校验
Account实体类封装所有Account的行为包括业务逻辑校验
用Domain Service封装多对象逻辑
AccountTransferService中提供转账接口完成源头账户的转出、目标账户的转入能力
DDD重构结果
代码
public class TransferServiceImplNew implements TransferService {private AccountRepository accountRepository;private AuditMessageProducer auditMessageProducer;private ExchangeRateService exchangeRateService;private AccountTransferService accountTransferService;Overridepublic ResultBoolean transfer(Long sourceUserId, String targetAccountNumber, BigDecimal targetAmount, String targetCurrency) {// 参数校验Money targetMoney new Money(targetAmount, new Currency(targetCurrency));// 读数据Account sourceAccount accountRepository.find(new UserId(sourceUserId));Account targetAccount accountRepository.find(new AccountNumber(targetAccountNumber));ExchangeRate exchangeRate exchangeRateService.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());// 业务逻辑accountTransferService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);// 保存数据accountRepository.save(sourceAccount);accountRepository.save(targetAccount);// 发送审计消息AuditMessage message new AuditMessage(sourceAccount, targetAccount, targetMoney);auditMessageProducer.send(message);return Result.success(true);}
}分层结构图 通过对外部依赖的抽象和内部逻辑的封装重构应用整体的依赖关系变了
一、Domain Layer
最底层不再是数据库而是Entity、Domain Primitive和Domain Service。这些对象**不依赖任何外部服务和框架而是纯内存中的数据和操作。**领域层没有任何外部依赖关系。
Application Layer
再其次的是负责组件编排的Application Service但是这些服务仅仅依赖了一些抽象出来的ACL类和Repository类而其具体实现类是通过依赖注入注进来的。Application Service、Repository、ACL等我们统称为Application Layer应用层。应用层 依赖 领域层但不依赖具体实现。
Infrastructure Layer
最后是ACLRepository等的具体实现这些实现通常依赖外部具体的技术实现和框架所以统称为Infrastructure Layer基础设施层。Web框架里的对象如Controller之类的通常也属于基础设施层
DDD的下的架构建议 Types模块facade对外暴露的Domain Primitives的地方。DP因为是无状态的逻辑可以对外暴露所以经常被包含在对外的API接口中需要单独成为模块。Types模块不依赖任何类库纯POJO。Domain模块core-model 和 core-service是核心业务逻辑的集中地包含有状态的Entity、领域服务Domain Service、以及各种外部依赖的接口类如Repository、ACL、中间件等。Application模块biz-shared 和 biz-service主要包含Application Service和一些相关的类。Application模块依赖Domain模块。Infrastructure模块dal 和 integration包含了Persistence、Messaging、External等模块。比如Persistence模块包含数据库DAO的实现包含Data Object、ORM Mapper、Entity到DO的转化类等。Web模块webWeb模块包含Controller等相关代码
代码的演进/变化速度
在传统架构中代码从上到下的变化速度基本上是一致的改个需求需要从接口、到业务逻辑、到数据库全量变更而第三方变更可能会导致整个代码的重写。但在DDD中不同模块的代码的演进速度是不一样的
Domain 和 Application 层属于业务逻辑属于经常被修改的地方。通过Entity能够解决基于单个对象的逻辑变更通过Domain Service解决多个对象间的业务逻辑变更。Infrastructure 和 Types 层属于最低频变更的。Infrastructure 层的模块只有在外部依赖变更了之后才会跟着升级而外部依赖的变更频率一般远低于业务逻辑的变更频率。
所以在DDD架构中能明显看出越外层的代码越稳定越内层的代码演进越快真正体现了领域“驱动”的核心思想。
SOFA下的DDD划分