├── .DS_Store ├── .gitattributes ├── .gitignore ├── .nojekyll ├── 01~系统架构设计原则 ├── Consistency~一致性 │ └── Replication.md ├── Durability~持久性 │ ├── Archivability.md │ └── Durability~持久性.md ├── Extensibility~可扩展性 │ ├── 可扩展性.md │ └── 负载与性能.md ├── Maintainability~可维护性 │ ├── Configurability.md │ ├── Deployability.md │ ├── Maintainability.md │ └── 可维护性.md ├── Observeability~可观测性 │ └── README.md ├── README.md ├── Resiliency~弹性可恢复 │ ├── README.md │ ├── 可靠性.md │ ├── 防错设计.md │ └── 面向失败的设计.md ├── Scalability~可扩容性 │ └── README.md ├── Security~安全性 │ ├── Auditability.md │ ├── Authentication.md │ ├── Legality.md │ └── README.md ├── Testability~可测试性 │ └── README.md └── Usability~可使用性 │ ├── API Contract.md │ ├── Accessibility.md │ └── Learnability.md ├── 02~架构风格与模式 ├── 2024~软件架构风格与模式:系统化指南.md ├── CRUD │ ├── README.md │ └── 事务脚本 │ │ ├── 事务脚本.md │ │ ├── 活动记录.md │ │ └── 表模块.md ├── EDA 事件驱动架构 │ ├── 99~参考资料 │ │ ├── 2020~Event Sourcing and CQRS Examples │ │ │ └── codes │ │ │ │ ├── .github │ │ │ │ ├── dependabot.yml │ │ │ │ └── workflows │ │ │ │ │ └── build.yml │ │ │ │ ├── .gitignore │ │ │ │ ├── CODE_OF_CONDUCT.md │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── README.md │ │ │ │ ├── pom.xml │ │ │ │ └── src │ │ │ │ ├── environments │ │ │ │ └── development.yml │ │ │ │ ├── main │ │ │ │ ├── java │ │ │ │ │ └── bankservice │ │ │ │ │ │ ├── bootstrap │ │ │ │ │ │ └── BankServiceApplication.java │ │ │ │ │ │ ├── domain │ │ │ │ │ │ └── model │ │ │ │ │ │ │ ├── Aggregate.java │ │ │ │ │ │ │ ├── Event.java │ │ │ │ │ │ │ ├── EventStore.java │ │ │ │ │ │ │ ├── OptimisticLockingException.java │ │ │ │ │ │ │ ├── Specification.java │ │ │ │ │ │ │ ├── ValueObject.java │ │ │ │ │ │ │ ├── account │ │ │ │ │ │ │ ├── Account.java │ │ │ │ │ │ │ ├── AccountDepositedEvent.java │ │ │ │ │ │ │ ├── AccountOpenedEvent.java │ │ │ │ │ │ │ ├── AccountWithdrawnEvent.java │ │ │ │ │ │ │ └── NonSufficientFundsException.java │ │ │ │ │ │ │ └── client │ │ │ │ │ │ │ ├── Client.java │ │ │ │ │ │ │ ├── ClientEnrolledEvent.java │ │ │ │ │ │ │ ├── ClientUpdatedEvent.java │ │ │ │ │ │ │ └── Email.java │ │ │ │ │ │ ├── port │ │ │ │ │ │ ├── incoming │ │ │ │ │ │ │ └── adapter │ │ │ │ │ │ │ │ └── resources │ │ │ │ │ │ │ │ ├── OptimisticLockingExceptionMapper.java │ │ │ │ │ │ │ │ ├── accounts │ │ │ │ │ │ │ │ ├── AccountDto.java │ │ │ │ │ │ │ │ ├── AccountNotFoundExceptionMapper.java │ │ │ │ │ │ │ │ ├── AccountResource.java │ │ │ │ │ │ │ │ ├── AccountsResource.java │ │ │ │ │ │ │ │ ├── deposits │ │ │ │ │ │ │ │ │ ├── DepositDto.java │ │ │ │ │ │ │ │ │ └── DepositsResource.java │ │ │ │ │ │ │ │ └── withdrawals │ │ │ │ │ │ │ │ │ ├── WithdrawalDto.java │ │ │ │ │ │ │ │ │ └── WithdrawalsResource.java │ │ │ │ │ │ │ │ └── clients │ │ │ │ │ │ │ │ ├── ClientDto.java │ │ │ │ │ │ │ │ ├── ClientResource.java │ │ │ │ │ │ │ │ ├── ClientsResource.java │ │ │ │ │ │ │ │ └── Email.java │ │ │ │ │ │ └── outgoing │ │ │ │ │ │ │ └── adapter │ │ │ │ │ │ │ └── eventstore │ │ │ │ │ │ │ └── InMemoryEventStore.java │ │ │ │ │ │ ├── projection │ │ │ │ │ │ ├── accounttransactions │ │ │ │ │ │ │ ├── AccountTransactionsResource.java │ │ │ │ │ │ │ ├── InMemoryTransactionsRepository.java │ │ │ │ │ │ │ ├── TransactionProjection.java │ │ │ │ │ │ │ ├── TransactionsListener.java │ │ │ │ │ │ │ └── TransactionsRepository.java │ │ │ │ │ │ └── clientaccounts │ │ │ │ │ │ │ ├── AccountProjection.java │ │ │ │ │ │ │ ├── AccountsListener.java │ │ │ │ │ │ │ ├── AccountsRepository.java │ │ │ │ │ │ │ ├── ClientAccountsResource.java │ │ │ │ │ │ │ └── InMemoryAccountsRepository.java │ │ │ │ │ │ └── service │ │ │ │ │ │ ├── Retrier.java │ │ │ │ │ │ ├── account │ │ │ │ │ │ ├── AccountNotFoundException.java │ │ │ │ │ │ ├── AccountService.java │ │ │ │ │ │ ├── DepositAccountCommand.java │ │ │ │ │ │ ├── OpenAccountCommand.java │ │ │ │ │ │ └── WithdrawAccountCommand.java │ │ │ │ │ │ └── client │ │ │ │ │ │ ├── ClientNotFoundException.java │ │ │ │ │ │ ├── ClientService.java │ │ │ │ │ │ ├── EnrollClientCommand.java │ │ │ │ │ │ └── UpdateClientCommand.java │ │ │ │ └── resources │ │ │ │ │ └── checkstyle │ │ │ │ │ ├── google_checks.xml │ │ │ │ │ └── suppressions.xml │ │ │ │ └── test │ │ │ │ ├── java │ │ │ │ └── bankservice │ │ │ │ │ ├── domain │ │ │ │ │ └── model │ │ │ │ │ │ ├── AggregateTest.java │ │ │ │ │ │ ├── account │ │ │ │ │ │ └── AccountTest.java │ │ │ │ │ │ └── client │ │ │ │ │ │ ├── ClientTest.java │ │ │ │ │ │ └── EmailSpecificationTest.java │ │ │ │ │ ├── it │ │ │ │ │ ├── AccountIT.java │ │ │ │ │ ├── AccountTransactionsIT.java │ │ │ │ │ ├── AccountsIT.java │ │ │ │ │ ├── BaseIT.java │ │ │ │ │ ├── ClientAccountsIT.java │ │ │ │ │ ├── ClientIT.java │ │ │ │ │ ├── ClientsIT.java │ │ │ │ │ ├── DepositsIT.java │ │ │ │ │ ├── ExceptionMappersIT.java │ │ │ │ │ ├── HealthCheckIT.java │ │ │ │ │ ├── WithdrawalsIT.java │ │ │ │ │ ├── client │ │ │ │ │ │ ├── ResourcesClient.java │ │ │ │ │ │ ├── ResourcesDtos.java │ │ │ │ │ │ └── ResourcesUrls.java │ │ │ │ │ └── setup │ │ │ │ │ │ └── StateSetup.java │ │ │ │ │ ├── port │ │ │ │ │ ├── incoming │ │ │ │ │ │ └── adapter │ │ │ │ │ │ │ └── resources │ │ │ │ │ │ │ ├── OptimisticLockingExceptionMapperTest.java │ │ │ │ │ │ │ └── accounts │ │ │ │ │ │ │ └── AccountNotFoundExceptionMapperTest.java │ │ │ │ │ └── outgoing │ │ │ │ │ │ └── adapter │ │ │ │ │ │ └── eventstore │ │ │ │ │ │ └── InMemoryEventStoreTest.java │ │ │ │ │ ├── projection │ │ │ │ │ ├── accounttransactions │ │ │ │ │ │ └── InMemoryTransactionsRepositoryTest.java │ │ │ │ │ └── clientaccounts │ │ │ │ │ │ └── InMemoryAccountsRepositoryTest.java │ │ │ │ │ └── service │ │ │ │ │ ├── RetrierTest.java │ │ │ │ │ └── account │ │ │ │ │ └── AccountServiceTest.java │ │ │ │ └── resources │ │ │ │ └── integration.yml │ │ └── 22~“消息驱动、事件驱动、流 ”基础概念解析.md │ ├── EventSourcing.md │ ├── README.md │ └── 事件溯源.md ├── Polylith │ └── README.md ├── REST 架构风格 │ ├── 99~参考资料 │ │ └── 2022~Writing API Design Standards.md │ ├── API 增强 │ │ └── HAL │ │ │ └── README.md │ ├── API 生成 │ │ └── README.md │ ├── OpenAPI │ │ └── README.md │ ├── README.md │ ├── REST API 标准 │ │ ├── Microsoft API 设计标准.md │ │ ├── Paypal API 设计标准.md │ │ ├── README.md │ │ └── RESTful 接口.md │ └── 演化与变迁 │ │ ├── API 的过去,现在与未来.md │ │ └── WebAPI 风格变迁.md └── 分层架构设计 │ ├── README.md │ ├── 代码模型.md │ ├── 层的划分.md │ ├── 整洁架构 │ └── README.link │ └── 架构演化.md ├── 03~架构设计方式 ├── 99~参考资料 │ └── 2023-构建可持续架构的三大秘籍.md ├── README.md ├── UML │ ├── PlantUML.md │ ├── README.md │ ├── 时序图.md │ ├── 类关系.md │ └── 类图.md ├── 中台与平台 │ ├── README.md │ ├── 业务与架构 │ │ ├── 业务定义.md │ │ ├── 业务挑战.md │ │ ├── 业务模型.md │ │ └── 中台架构.md │ ├── 中台红与黑 │ │ ├── README.md │ │ ├── 中台的缺陷.md │ │ └── 中台的诉求.md │ ├── 大厂中台 │ │ └── 阿里.md │ └── 开放平台 │ │ └── README.md ├── 技术团队组织 │ ├── README.md │ ├── 管理模式 │ │ ├── 原子化管理.md │ │ └── 组织发展.md │ └── 组织架构 │ │ └── README.md ├── 架构可视化 │ ├── C4 │ │ ├── README.md │ │ └── 架构可视化的坏味道.md │ ├── README.md │ ├── techtribesjs.puml │ ├── 架构图分类.md │ └── 流程图 │ │ └── Flowchart.md ├── 架构域与推导 │ ├── README.md │ ├── 业务模型推导.md │ └── 架构域划分.md └── 架构描述框架 │ ├── DODAF.md │ ├── ITSA.md │ ├── README.md │ ├── TOGAF.md │ ├── TOGAF │ └── README.md │ └── Zachman.md ├── 04~服务化架构 └── README.link ├── 99~参考资料 ├── .DS_Store ├── 2018~什么是架构模式和架构风格.md ├── 2023~BytebyteGo~System Design 101 │ └── 2023~BytebyteGo~System Design 101.md └── Awesome System Design Articles │ └── README.md ├── INTRODUCTION.md ├── LICENSE ├── README.md ├── _sidebar.md ├── header.svg └── index.html /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xmind filter=lfs diff=lfs merge=lfs -text 2 | *.pdf filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/.nojekyll -------------------------------------------------------------------------------- /01~系统架构设计原则/Consistency~一致性/Replication.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Consistency~一致性/Replication.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Durability~持久性/Archivability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Durability~持久性/Archivability.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Durability~持久性/Durability~持久性.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Durability~持久性/Durability~持久性.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Extensibility~可扩展性/负载与性能.md: -------------------------------------------------------------------------------- 1 | # 负载与性能 2 | 3 | 负载可以用一些称为负载参数(load parameters)的数字来描述。参数的最佳选择取决于系统架构,它可能是每秒向 Web 服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。除此之外,也许平均情况对你很重要,也许你的瓶颈是少数极端场景。 4 | 5 | ## 负载案例 6 | 7 | 以推特为例,用户可以向其粉丝发布新消息(平均 4.6k 请求/秒,峰值超过 12k 请求/秒),用户可以查阅他们关注的人发布的推文(300k 请求/秒)。大体上讲,这一对操作有两种实现方式,首先是发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。在如下图所示的关系型数据库中,可以编写这样的查询: 8 | 9 | ```sql 10 | SELECT tweets.*, users.* 11 | FROM tweets 12 | JOIN users ON tweets.sender_id = users.id 13 | JOIN follows ON follows.followee_id = users.id 14 | WHERE follows.follower_id = current_user 15 | ``` 16 | 17 | ![推特主页时间线的关系型模式简单实现](https://s2.ax1x.com/2020/02/02/1YHmwV.md.png) 18 | 19 | 另一种办法是为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱。当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。 20 | 21 | ![用于分发推特至关注者的数据流水线,2012年11月的负载参数](https://s2.ax1x.com/2020/02/02/1t0IRe.png) 22 | 23 | 推特的第一个版本使用了方法 1,但系统很难跟上主页时间线查询的负载。所以公司转向了方法 2,方法 2 的效果更好,因为发推频率比查询主页时间线的频率几乎低了两个数量级,所以在这种情况下,最好在写入时做更多的工作,而在读取时做更少的工作。然而方法 2 的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约 75 个关注者,所以每秒 4.6k 的发推写入,变成了对主页时间线缓存每秒 345k 的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过 3000 万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的 3000 万次写入。 24 | 25 | 最终,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法 1 所示。这种混合方法能始终如一地提供良好性能。 26 | 27 | ## 性能 28 | 29 | 一旦系统的负载被描述好,就可以研究当负载增加会发生什么。我们可以从两种角度来看: 30 | 31 | - 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响? 32 | - 增加负载参数并希望保持性能不变时,需要增加多少系统资源? 33 | 34 | 这两个问题都需要性能数据,所以让我们简单地看一下如何描述系统性能。对于 Hadoop 这样的批处理系统,通常关心的是吞吐量(throughput),即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间。对于在线系统,通常更重要的是服务的响应时间(response time),即客户端发送请求到接收响应之间的时间。 35 | 36 | 理想情况下,批量作业的运行时间是数据集的大小除以吞吐量在实践中由于数据倾斜(数据不是均匀分布在每个工作进程中),需要等待最慢的任务完成,所以运行时间往往更长。即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的数值分布(distribution),而不是单个数值。 37 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Maintainability~可维护性/Configurability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Maintainability~可维护性/Configurability.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Maintainability~可维护性/Deployability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Maintainability~可维护性/Deployability.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Maintainability~可维护性/Maintainability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Maintainability~可维护性/Maintainability.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Maintainability~可维护性/可维护性.md: -------------------------------------------------------------------------------- 1 | # 可维护性(Maintainability) 2 | 3 | 众所周知,软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添加新的功能等等。不幸的是,许多从事软件系统行业的人不喜欢维护所谓的遗留(legacy)系统,每一个遗留系统都以自己的方式让人不爽,所以很难给出一个通用的建议来和它们打交道。 4 | 5 | 我们可以,也应该以这样一种方式来设计软件:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则: 6 | 7 | - 可操作性(Operability):便于运维团队保持系统平稳运行。 8 | 9 | - 简单性(Simplicity):从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。) 10 | 11 | - 可演化性(evolability):使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility),可修改性(modifiability)或可塑性(plasticity)。 12 | 13 | 和之前提到的可靠性、可扩展性一样,实现这些目标也没有简单的解决方案。不过我们会试着想象具有可操作性,简单性和可演化性的系统会是什么样子。 14 | 15 | # 可操作性 16 | 17 | 良好的运维经常可以绕开垃圾(或不完整)软件的局限性,而再好的软件摊上垃圾运维也没法可靠运行。尽管运维的某些方面可以,而且应该是自动化的,但在最初建立正确运作的自动化机制仍然取决于人。运维团队对于保持软件系统顺利运行至关重要。一个优秀运维团队的典型职责如下(或者更多): 18 | 19 | - 监控系统的运行状况,并在服务状态不佳时快速恢复服务 20 | - 跟踪问题的原因,例如系统故障或性能下降 21 | - 及时更新软件和平台,比如安全补丁 22 | - 了解系统间的相互作用,以便在异常变更造成损失前进行规避。 23 | - 预测未来的问题,并在问题出现之前加以解决(例如,容量规划) 24 | - 建立部署,配置、管理方面的良好实践,编写相应工具 25 | - 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台 26 | - 当配置变更时,维持系统的安全性 27 | - 定义工作流程,使运维操作可预测,并保持生产环境稳定。 28 | - 铁打的营盘流水的兵,维持组织对系统的了解。 29 | 30 | 良好的可操作性意味着更轻松的日常工作,进而运维团队能专注于高价值的事情。数据系统可以通过各种方式使日常任务更轻松: 31 | 32 | - 通过良好的监控,提供对系统内部状态和运行时行为的可见性(visibility) 33 | - 为自动化提供良好支持,将系统与标准化工具相集成 34 | - 避免依赖单台机器(在整个系统继续不间断运行的情况下允许机器停机维护) 35 | - 提供良好的文档和易于理解的操作模型(“如果做 X,会发生 Y”) 36 | - 提供良好的默认行为,但需要时也允许管理员自由覆盖默认值 37 | - 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态 38 | - 行为可预测,最大限度减少意外 39 | 40 | # 简单性:管理复杂度 41 | 42 | 小型软件项目可以使用简单讨喜的、富表现力的代码,但随着项目越来越大,代码往往变得非常复杂,难以理解。这种复杂度拖慢了所有系统相关人员,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被描述为 烂泥潭(a big ball of mud)。复杂度(complexity)有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的 Hack、需要绕开的特例等等,现在已经有很多关于这个话题的讨论。 43 | 44 | 因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。 45 | 46 | 简化系统并不一定意味着减少功能;它也可以意味着消除额外的(accidental)的复杂度 Moseley 和 Marks 把 额外复杂度 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。 47 | 48 | 用于消除额外复杂度的最好工具之一是抽象(abstraction)。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。 49 | 50 | 例如,高级编程语言是一种抽象,隐藏了机器码、CPU 寄存器和系统调用 SQL 也是一种抽象,隐藏了复杂的磁盘/内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有直接(directly)使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。 51 | 52 | 抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。 53 | 54 | # 可演化性:拥抱变化 55 | 56 | 系统的需求永远不变,基本是不可能的。更可能的情况是,它们处于常态的变化中,例如:你了解了新的事实、出现意想不到的应用场景、业务优先级发生变化、用户要求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。 57 | 58 | 在组织流程方面,敏捷(agile)工作模式为适应变化提供了一个框架。敏捷社区还开发了对在频繁变化的环境中开发软件很有帮助的技术工具和模式,如 测试驱动开发(TDD, test-driven development)和 重构(refactoring)。 59 | 60 | 这些敏捷技术的大部分讨论都集中在相当小的规模(同一个应用中的几个代码文件)。本书将探索在更大数据系统层面上提高敏捷性的方法,可能由几个不同的应用或服务组成。例如,为了将装配主页时间线的方法从方法 1 变为方法 2,你会如何“重构”推特的架构 61 | 62 | 修改数据系统并使其适应不断变化需求的容易程度,是与简单性和抽象性密切相关的:简单易懂的系统通常比复杂系统更容易修改。但由于这是一个非常重要的概念,我们将用一个不同的词来指代数据系统层面的敏捷性:可演化性(evolvability)。 63 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Observeability~可观测性/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Observeability~可观测性/README.md -------------------------------------------------------------------------------- /01~系统架构设计原则/README.md: -------------------------------------------------------------------------------- 1 | # 系统架构设计原则 2 | 3 | ![EP54~Top 10 Architecture Characteristics / Non-Functional Requirements with Cheatsheet](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/uPic/ANFRGQ5IxaUj.webp) 4 | 5 | # 理解高可用 6 | 7 | 高可用是一个具体而又抽象的名词,谈及此我们会联想到用户随时随地访问系统即可得到预期的响应。 8 | 9 | - 安全:内部的操作失误,外部的安全威胁 机密,完整,可用 10 | - 可靠:各种黑天鹅事件 能够应对故障,提供冗余,便于运维 11 | - 扩展:高并发峰值应对 可理解 12 | 13 | 高可用的挑战 14 | 15 | - 不可靠的系统:在《[DistributedSystem-Notes](https://github.com/wx-chevalier/DistributedSystem-Notes?q=)》中我们详细讨论过不可靠的分布式系统。 16 | - 高并发: 17 | - 复杂性: 18 | - 隐蔽性: 19 | - 其他威胁:安全攻击, 20 | 21 | # 代价 22 | 23 | 实现高可用系统并不是无代价的。 24 | 25 | # Run cost as architecture fitness function 26 | 27 | 对于今天的组织来说,自动化评估、跟踪和预测云基础设施的运行成本是必要的。云供应商精明的定价模型,以及基于定价参数的费用激增,再加上现代架构的动态本质,常常导致让人吃惊的运行成本。例如,无服务架构基于 API 访问量的费用,事件流方案中基于流量的费用,以及数据处理集群中基于运行任务数量的费用,它们都具有动态的本质,会随着架构演进而产生改变。当我们的团队在云平台上管理基础设施时,将运行成本实现为架构适应度函数是他们的早期活动之一。这意味着我们的团队可以观察运行服务的费用,并同交付的价值进行对比;当看到与期望或可接受的结果之间存在偏差时,他们就会探讨架构是否应该继续演进了。对运行成本的观察和计算需要被实现为自动化的函数。 28 | 29 | # Links 30 | 31 | - [2021~21 大软件架构特点的全面解析](https://mp.weixin.qq.com/s?__biz=MzA3OTc0MzY1Mg==&mid=2247502628&idx=5&sn=29d08b015eca4ffc8d72a11e54750f6e&scene=58&subscene=0): 我们从业务需求(业务特征)、我们期望的系统运营方式(运营特征)中总结出这些特点,它们是隐式的、贯穿各领域,是架构师在字里行间能看出来的特点。 32 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Resiliency~弹性可恢复/README.md: -------------------------------------------------------------------------------- 1 | ![概念示意图](https://s2.ax1x.com/2019/11/24/MOnpYq.png) 2 | 3 | # 容错、高可用与灾备 4 | 5 | 容错、高可用与灾备这三个术语,很容易混淆,专业人员有时也会用错。 6 | 7 | - 容错:发生故障时,如何让系统继续运行。 8 | 9 | - 高可用:系统中断时,如何尽快恢复。 10 | 11 | - 灾备:系统毁灭时,如何抢救数据。 12 | 13 | ## 容错 14 | 15 | 容错(fault tolerance)指的是,发生故障时,系统还能继续运行。 16 | 17 | ![飞机图片](https://s2.ax1x.com/2019/11/24/MOYq1J.png) 18 | 19 | 飞机有四个引擎,如果一个引擎坏了,剩下三个引擎,还能继续飞,这就是"容错"。同样的,汽车的一个轮子扎破了,剩下三个轮子,也还是勉强能行驶。容错的目的是,发生故障时,系统的运行水平可能有所下降,但是依然可用,不会完全失败。 20 | 21 | 值得一提的是,容错并不意味着能够容忍全部的错误,譬如假设全球服务器都坏了,那么系统必然是不可用的。在讨论容错时,只有谈论特定类型的错误才有意义。 22 | 23 | ## 高可用 24 | 25 | 高可用(high availability)指的是,系统能够比正常时间更久地保持一定的运行水平。 26 | 27 | ![汽车备胎](https://s2.ax1x.com/2019/11/24/MOtptO.png) 28 | 29 | 汽车的备胎就是一个高可用的例子。如果没有备胎,轮胎坏了,车就开不久了。备胎延长了汽车行驶的可用时间。注意,高可用不是指系统不中断(那是容错能力),而是指一旦中断能够快速恢复,即中断必须是短暂的。如果需要很长时间才能恢复可用性,就不叫高可用了。上面例子中,更换备胎就必须停车,但只要装上去,就能回到行驶状态。 30 | 31 | ## 灾备 32 | 33 | 灾备(又称灾难恢复,disaster recovery)指的是,发生灾难时恢复业务的能力。 34 | 35 | ![飞机灾难图片](https://s2.ax1x.com/2019/11/24/MOtQ3Q.png) 36 | 37 | 上图中,飞机是你的 IT 基础设施,飞行员是你的业务,飞行员弹射装置就是灾备措施。一旦飞机即将坠毁,你的基础设施就要没了,灾备可以让你的业务幸存下来。 38 | 39 | 灾备的目的就是,保存系统的核心部分。一个好的灾备方案,就是从失败的基础设施中获取企业最宝贵的数据,然后在新的基础设施上恢复它们。注意,灾备不是为了挽救基础设置,而是为了挽救业务。 40 | 41 | ## 小结 42 | 43 | 高可用 IT 系统,我们希望它具有良好的韧性,通俗讲就是具有鲁棒性,而非一碰就倒的花瓶、需要精心维护。设计一个韧性的系统,通常遵循以下两方面的设计原则: 44 | 45 | - 面向失败设计:充分考虑分布式微服务架构下会存在的各类故障,针对故障进行针对性的设计;进行故障测试,系统是否具备应对故障的能力。这样,当故障真正发生时,才能从容应对,避免造成严重影响; 46 | 47 | - 面向恢复设计:充分考虑系统中应用节点出现宕机等不可用情况时,能够及时发现并自动恢复。这里讲的恢复既包括集群中应用节点宕机之后的重新启动,也包括对宕机应用处理中的业务的自动恢复。 48 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Resiliency~弹性可恢复/可靠性.md: -------------------------------------------------------------------------------- 1 | # 可靠性(Reliability) 2 | 3 | 系统的可靠性是指在规定的时间内及规定的环境下完成规定功能的能力,也就是系统的无故障运行概率。人们对可靠软件的典型期望包括: 4 | 5 | - 应用程序表现出用户所期望的功能。 6 | - 允许用户犯错,允许用户以出乎意料的方式使用软件。 7 | - 在预期的负载和数据量下,性能满足要求。 8 | - 系统能防止未经授权的访问和滥用。 9 | 10 | 可靠性不仅仅是针对核电站和空中交通管制软件而言,我们也期望更多平凡的应用能可靠地运行。商务应用中的错误会导致生产力损失(也许数据报告不完整还会有法律风险),而电商网站的中断则可能会导致收入和声誉的巨大损失。 11 | 12 | ## 故障与错误 13 | 14 | 造成错误的原因叫做故障(fault),能预料并应对故障的系统特性可称为容错(fault-tolerant)或韧性(resilient)。注意故障(fault)不同于失效(failure),故障通常定义为系统的一部分状态偏离其标准,而失效则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因故障而导致失效。 15 | 16 | 在这类容错系统中,通过故意触发来提高故障率是有意义的,例如:在没有警告的情况下随机地杀死单个进程。许多高危漏洞实际上是由糟糕的错误处理导致的,像 Netflix 公司的 Chaos Monkey 就是通过故意引发故障来确保容错机制不断运行并接受考验,从而提高故障自然发生时系统能正确处理的信心的例子。 17 | 18 | ## 安全性和活性 19 | 20 | 为了澄清这种情况,有必要区分两种不同的性质:安全性(safety)和活性(liveness)。在刚刚给出的例子中,唯一性(uniqueness)和单调序列(monotonic sequence)是安全属性,但可用性是活性(liveness)属性。安全性通常被非正式地定义为,没有坏事发生,而活性通常就类似:最终好事发生。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。安全性和活性的实际定义是精确的和数学的: 21 | 22 | - 如果安全属性被违反,我们可以指向一个特定的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌返回的特定操作)。违反安全属性后,违规行为不能撤销——损失已经发生。 23 | 24 | - 活性属性反过来:在某个时间点(例如,一个节点可能发送了一个请求,但还没有收到响应),它可能不成立,但总是希望在未来(即通过接受答复)。 25 | 26 | 区分安全性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求始终保持安全属性是常见的。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全性得到满足)。但是,对于活性属性,我们可以提出一些注意事项:例如,只有在大多数节点没有崩溃的情况下,只有当网络最终从中断中恢复时,我们才可以说请求需要接收响应。部分同步模型的定义要求系统最终返回到同步状态——即任何网络中断的时间段只会持续一段有限的时间,然后进行修复。 27 | 28 | 安全性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实施算法时,现实的混乱事实再一次地让你咬牙切齿,很明显系统模型是对现实的简化抽象。例如,在故障恢复模型中的算法通常假设稳定存储器中的数据经历了崩溃。但是,如果磁盘上的数据被破坏,或者由于硬件错误或错误配置导致数据被清除,会发生什么情况?如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,也会发生什么情况? 29 | 30 | 法定人数算法依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多存在崩溃,但有时可能会丢失。但是那个模型就变得更难以推理了。算法的理论描述可以简单宣称一些事在假设上是不会发生的——在非拜占庭式系统中。但实际上我们还是需要对可能发生和不可能发生的故障做出假设,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是 printf("you sucks")和 exit(666),实际上也就是留给运维来擦屁股。(这可以说是计算机科学和软件工程间的一个差异)。 31 | 32 | 这并不是说理论上抽象的系统模型是毫无价值的,恰恰相反。它们对于将实际系统的复杂性降低到一个我们可以推理的可处理的错误是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。我们可以证明算法是正确的,通过显示它们的属性总是保持在某个系统模型中。证明算法正确并不意味着它在真实系统上的实现必然总是正确的。但这迈出了很好的第一步,因为理论分析可以发现算法中的问题,这种问题可能会在现实系统中长期潜伏,直到你的假设(例如,时间)因为不寻常的情况被打破。理论分析与经验测试同样重要。 33 | 34 | # 故障模型 35 | 36 | 系统故障是指硬件或者软件的错误状态,一般引进故障的原因是这些:部件的失效、环境的物理干扰、操作错误或不正确的设计。按照时间的长短,故障可以分为:永久性、间歇性、瞬时性。 37 | 38 | 故障的级别有:逻辑级故障、数据结构级故障、软件故障和差错故障、系统级故障。 39 | 40 | ## 硬件故障 41 | 42 | 硬件故障(hardware faults)譬如等等硬盘崩溃、内存出错、机房断电、有人拔错网线,任何大型的数据中心都可能出现这种错误。据报道称,硬盘的 平均无故障时间(MTTF mean time to failure)约为 10 到 50 年,因此从数学期望上讲,在拥有 10000 个磁盘的存储集群上,平均每天会有 1 个磁盘出故障。 43 | 44 | 为了减少系统的故障率,第一反应通常都是增加单个硬件的冗余度,例如:磁盘可以组建 RAID,服务器可能有双路电源和热插拔 CPU,数据中心可能有电池和柴油发电机作为后备电源,某个组件挂掉时冗余组件可以立刻接管。这种方法虽然不能完全防止由硬件问题导致的系统失效,但它简单易懂,通常也足以让机器不间断运行很多年。 45 | 46 | 我们通常认为硬件故障是随机的、相互独立的:一台机器的磁盘失效并不意味着另一台机器的磁盘也会失效。大量硬件组件不可能同时发生故障,除非它们存在比较弱的相关性(同样的原因导致关联性错误,例如服务器机架的温度)。随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外在一些云平台(如亚马逊网络服务(AWS, Amazon Web Services))中,虚拟机实例不可用却没有任何警告也是很常见的,因为云平台的设计就是优先考虑灵活性(flexibility)和弹性(elasticity),而不是单机可靠性。 47 | 48 | 如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次修复一个节点,无需整个系统停机。 49 | 50 | ## 软件错误 51 | 52 | 另一类错误是内部的系统性错误(systematic error)。这类错误难以预料,而且因为是跨节点相关的,所以比起不相关的硬件故障往往可能造成更多的系统失效。例子包括: 53 | 54 | - 接受特定的错误输入,便导致所有应用服务器实例崩溃的 BUG。例如 2012 年 6 月 30 日的闰秒,由于 Linux 内核中的一个错误,许多应用同时挂掉了。 55 | - 失控进程会占用一些共享资源,包括 CPU 时间、内存、磁盘空间或网络带宽。 56 | - 系统依赖的服务变慢,没有响应,或者开始返回错误的响应。 57 | - 级联故障,一个组件中的小故障触发另一个组件中的故障,进而触发更多的故障。 58 | 59 | 虽然软件中的系统性故障无法避免,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy)时报警。 60 | 61 | ## 人为错误 62 | 63 | 设计并构建了软件系统的工程师是人类,维持系统运行的运维也是人类,人非圣贤,孰能无过。一项关于大型互联网服务的研究发现,运维配置错误是导致服务中断的首要原因,而硬件故障(服务器或网络)仅导致了 10-25%的服务中断。 64 | 65 | - 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API 和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。 66 | - 将人们最容易犯错的地方与可能导致失效的地方解耦(decouple)。特别是提供一个功能齐全的非生产环境沙箱(sandbox),使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。 67 | - 在各个层次进行彻底的测试,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的边缘场景(corner case)。 68 | - 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。 69 | - 配置详细和明确的监控,比如性能指标和错误率在其他工程学科中这指的是遥测(telemetry)(一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的。)监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。 70 | - 良好的管理实践与充分的培训。 71 | 72 | # 可靠性模型 73 | 74 | 与故障模型想对应的,就是系统的可靠性模型。常用的有以下三种:时间模型、故障植入模型和数据模型。 75 | 76 | # 可靠性指标 77 | 78 | 可靠性指标,主要有以下几个: 79 | 80 | 平均无故障时间(MTTF-Mean Time To Failure) 81 | 82 | 它表示一个系统平均情况下,正常运行的时间。 83 | 84 | 与它相关的指标是“失效率”U,关系:U = 1 / MTTF。 85 | 86 | 平均故障修复时间(MTTR-Mean Time To Fix/Repire) 87 | 88 | 平均每次修复所需要的时间 89 | 90 | 平均故障间隔时间(MTBF-Mean Time Between Failure) 91 | 92 | 一看就知道,MTBF = MTTF + MTTR。 93 | 94 | 在实际情况下,一般 MTTR 都会比较小,所以我们近似地认为 MTBF = MTTF。 95 | 96 | MTTF 是用来说明一个软件系统能够正常运行的时间的指标。它越大,说明该系统越可靠。计算方法很简单, 97 | 98 | # 可靠性计算 99 | 100 | 一个系统的可靠性计算往往不能直接得出。这是因为计算机系统是一个复杂的系统,影响其可靠性的因素也非常复杂。所以我们需要为其建立适当的数据模型,把大系统划分为若干子系统,然后再根据一定原则进行组合计算。 101 | 102 | 这种计算方法,可以简化分析的过程。 103 | 104 | 对于系统的划分,我们可以把它分为:串联系统、并联系统、模冗余系统、混联系统。(其中模冗余系统是 M 个并联的子系统中,需要有 N 个以上的子系统能正常工作,整个系统才能正常工作。这种系统,常在并联后加上一个表决器。) 105 | 106 | 计算这些系统可靠性时,我们需要计算出每个子系统的失效率,然后根据概率的加法原则(串联系统)和乘法原则(并联系统)进行综合运算,最后得出整个系统的可靠性。 107 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Resiliency~弹性可恢复/防错设计.md: -------------------------------------------------------------------------------- 1 | # 防错设计 2 | 3 | Poka-yoke 是精益制造(Lean Manufacturing)领域的一个概念,意思是“防错”。Poka-yoke 的概念最早是新乡重夫在丰田汽车引入的。Wikepedia 上对 Poka-yoke 的介绍: 4 | 5 | > Poka-yoke (ポカヨケ) is a Japanese term that means "mistake-proofing" or "inadvertent error prevention". A poka-yoke is any mechanism in any process that helps an equipment operator avoid (yokeru) mistakes (poka). Its purpose is to eliminate product defects by preventing, correcting, or drawing attention to human errors as they occur. The concept was formalized, and the term adopted, by Shigeo Shingo as part of the Toyota Production System. 6 | 7 | > More broadly, the term can refer to any behavior-shaping constraint designed into a process to prevent incorrect operation by the user. 8 | 9 | Poka-yoke 在生活中的例子,譬如柴油的加油管是插不到汽油车的加油口里的,汽油的加油管是插不到柴油车的加油口里的。当时就觉得这个设计很赞,很安全,能够防止人搞错。 10 | 11 | # 输入值的及时校验 12 | 13 | - 有特定长度的、特定格式的,有 ISO 标准的,都要在值被输入的第一时刻,对长度格式等做充分的校验,校验发现不符合标准的,直接按照 illegal parameter 拒绝掉。 14 | 15 | - 含有非法字符的配置值,要在第一时间就校验出来并 rejec 掉,不能让它进入我们的系统。如果进来了,在生效前要做好校验。我们要有这样一种设计模式:当发现配置值非法的时候,系统能够自我保护,不加载有问题的新配置,继续使用老配置;同时,为了确保新部署的系统也能正常工作,配置要有多版本,否则,正在运行中的系统能自我保护,但一旦重启了就别无选择只能加载有问题的新配置了。 16 | 17 | - 全角双引号是东亚(中日韩)同学特别容易遇到的问题,很多富文本编辑器(包括 Microsoft Office)都会自动的把半角引号变成全角的。所以,如果需要开一个文本临时保存一下,要用 Sublime Text、[VS Code](https://code.visualstudio.com/)等代码编辑器。这些编辑器不会做半角/全角的自动转换。 18 | 19 | - 容易半角全角问题的除了引号以外,还有逗号(`,` vs. `,`)、减号(`-` vs. `−`)、括号(`()` vs. `()`)。这些也是很容易出问题的。文本编辑器里的字体要设置成比较容易辨识半角全角的。这个道理和识别`0O`和`1lI`是一样的道理。**增强视觉辨识效果**,是防错的一个很有效的手段。 20 | 21 | - 各种解析和转义代码(例如,URL escape、Unicode 到 ASCII 的转换、XML/Json/CSV 等格式在序列化和反序列化时对特殊字符处理等等),尽量不要自己去写代码,尽量用成熟的二方或三方类库。要对转义有敬畏心,健壮完备的转义逻辑不是件容易的事情,是需要投入一定的时间和精力去夯实的。宁可多花一点时间去找成熟的二方或三方类库,也不要为了节省时间自己写个简单的逻辑。 22 | 23 | - 接口和数据模型的各种字段要定义清楚:支持什么字符、不支持什么字符。需求文档如果有关于特殊字符的处理逻辑,例如“不支持特殊字符”,那必须要求需求文档完整定义清楚什么是“特殊字符”。后面要增加支持的字符,就要改需求、走一个完整的设计编码测试闭环。对于输入值,要第一时间做校验,如有不支持的字符,第一时间抛错。 24 | 25 | # 开发环境与生产环境的权限隔离 26 | 27 | - 网络隔离:生产环境的资源,例如数据库、SFTP、API 等,都要尽可能配 IP 地址白名单,只有白名单上的 source IP 可以访问。不过,IP 地址白名单这个东西本身也是很容易出错的,要非常小心,要有很好的防错设计和监控应急能力。IP 白名单一旦改错,网络访问不通,可能引起大面积的故障 28 | 29 | - 权限隔离:访问线下环境的账号和访问线上环境的账号必须是两个。同一个账号不可以同时有线上和线下的权限,防止搞错、调串。另外,如果是授予给工程师的生产环境访问权限,要做到授权范围最小、时间最短,必须要是 JIT(Just In Time),即要用的时候申请,用完之后马上回收。对范围大、有效期长的修改权限(读权限可以适当放宽)要严格管控。 30 | 31 | # 交互界面的高辨识度 32 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Resiliency~弹性可恢复/面向失败的设计.md: -------------------------------------------------------------------------------- 1 | # 面向失败的设计 2 | 3 | 软件架构也发生了一系列变化,从面向功能的单一系统架构,分布式架构,到面向业务的中台化架构,到现在的面向生态的云化架构。云化架构为我们带来了开放的生态,允许我们与更多的团队、更多的技术栈协同工作,但也带来了更多的不可控。因此在云时代,传统软件架构思想原则基础上,面向失败的架构设计也愈发越来越重要。 4 | 5 | 软件工程师通常会对这三个方面进行优化:可用性、性能、容错能力。 6 | 7 | - 可用性:系统正常响应和避免停机的能力。 8 | - 性能:在这里特指对延迟或资源成本的最小化。 9 | - 容错能力:系统从非正常状态中恢复的能力。 10 | 11 | 对于失败的定义,在上面三个维度也是有所不同,而如果以面向对象、功能或者业务视角来看,又是另一类维度的划分。抽象来说,面向失败的设计,就是假设系统必然会发生失败,天然为了失败而存在的,贯穿于软件整个生命周期的设计思想。譬如我们在设计阶段,就假设线上系统会出问题,从而在管控系统添加相应措施来防止一旦系统出现某种情况,可以及时补救。面向失败的设计思想的一个基石就是将监控、告警、管控系统作为系统基石的一部分,在设计阶段即融于整体。 12 | 13 | - 简单化设计原则:系统架构简单清晰,具备水平扩展能力。与之相反的就是过度设计,如何更好的平衡必要复杂度与意外复杂度是关键;不是在不能添加更多的时候,而是没有什么可以去掉的时候,才能达到完美。 14 | 15 | - 监控设计原则:监控系统架构及监控规则应该是简单的,易于理解的。监控项覆盖度高,需要控制好有效报警数,监控围绕四大黄金指标:延迟,流量,错误,饱和度展开。 16 | 17 | - 管控设计原则:需避免权限过大,系统应具备逃生能力,灰度能力。线上很多故障发生,都是因为权限过大或者不具备灰度能力导致的。管控系统作为服务提供方,理应当对自身行为带来的危害负责,需要具备自保护能力。 18 | 19 | - 敏捷设计原则:敏捷开发,小规模,多批次迭代。敏捷开发对面向失败设计来说可以有效预防,降低故障发生,同时能够快速定位及恢复故障。 20 | 21 | - 变更设计原则:可灰度,可监控,可回滚。无论系统发布还是配置项的改动,都需要遵从变更三板斧。线上 60% 故障是由于变更发布导致的,渐进式发布,快速准确检测到问题,同时快速回滚是非常必要的。 22 | 23 | - 容量设计原则:基于稳态容量及尖刺容量规划,适当冗余,具备快速弹性扩容能力。通过自然需求增长模型来预测稳态容量,通过适当冗余,流控,快速弹性扩容能力保障非自然需求增长。同时做好周期性压力测试。 24 | 25 | - 依赖设计原则:最小化依赖,避免循环依赖,通过异步化,服务降级,限流,隔离等手段控制由于依赖带来的影响面。上下游依赖需要建立基于接口,服务,应用等级别的 SLO,了解更多上下游信息,做好防护手段。 26 | 27 | - 自动化设计原则:对重复,人肉的操作尽可能通过自动化来保障操作一致性,提升效率。 28 | 29 | - 快恢设计原则:标准化的故障恢复流程,从故障被监控发现开始,人员上线响应,故障定位,恢复等一系列流程是人与系统共同参与的活动。从人的角度需要具备 oncall 能力,快速上线能力,快速登录系统定位处理问题能力,系统需要具备快速报警,回滚,隔离,容灾等能力。 30 | -------------------------------------------------------------------------------- /01~系统架构设计原则/Scalability~可扩容性/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Scalability~可扩容性/README.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Security~安全性/Auditability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Security~安全性/Auditability.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Security~安全性/Authentication.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Security~安全性/Authentication.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Security~安全性/Legality.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Security~安全性/Legality.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Security~安全性/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Security~安全性/README.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Testability~可测试性/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Testability~可测试性/README.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Usability~可使用性/API Contract.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Usability~可使用性/API Contract.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Usability~可使用性/Accessibility.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Usability~可使用性/Accessibility.md -------------------------------------------------------------------------------- /01~系统架构设计原则/Usability~可使用性/Learnability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/01~系统架构设计原则/Usability~可使用性/Learnability.md -------------------------------------------------------------------------------- /02~架构风格与模式/CRUD/README.md: -------------------------------------------------------------------------------- 1 | # CRUD 2 | 3 | CRUD 是最简单也是最经典的架构,命令(Command,通常用来更新数据,操作 DB)和查询(Query)通常使用的是在数据访问层中 Repository 中的实体对象(这些对象是对 DB 中表的映射),这些实体有可能是数据库中的一行数据或者多个表。 4 | 5 | 通常对 DB 执行的增,删,改,查(CRUD)都是针对的系统的实体对象。如通过数据访问层获取数据,然后通过数据传输对象 DTO 传给表现层。或者,用户需要更新数据,通过 DTO 对象将数据传给 Model,然后通过数据访问层写回数据库,系统中的所有交互都是和数据查询和存储有关,可以认为是数据驱动(Data-Driven)的。对于一些比较简单的系统,使用这种 CRUD 的设计方式能够满足要求。 6 | 7 | ![](https://assets.ng-tech.icu/item/20230430220945.png) 8 | 9 | # CRUD 的缺陷 10 | 11 | - 使用同一个对象实体来进行数据库读写可能会太粗糙,大多数情况下,比如编辑的时候可能只需要更新个别字段,但是却需要将整个对象都穿进去,有些字段其实是不需要更新的。在查询的时候在表现层可能只需要个别字段,但是需要查询和返回整个实体对象。 12 | 13 | - 使用同一实体对象对同一数据进行读写操作的时候,可能会遇到资源竞争的情况,经常要处理的锁的问题,在写入数据的时候,需要加锁。读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并且会对系统吞吐量的增长会产生影响。 14 | 15 | - 同步的,直接与数据库进行交互在大数据量同时访问的情况下可能会影响性能和响应性,并且可能会产生性能瓶颈。 16 | 17 | - 由于同一实体对象都会在读写操作中用到,所以对于安全和权限的管理会变得比较复杂。 18 | -------------------------------------------------------------------------------- /02~架构风格与模式/CRUD/事务脚本/事务脚本.md: -------------------------------------------------------------------------------- 1 | # 事务脚本 2 | 3 | 使用过程来组织业务逻辑,每个过程处理来自表现层的单个请求。对于很对业务应用来说都可以被看作是一系列事务。业务的一个请求将触发一系列的业务处理逻辑,而我们在代码中通常采用 SpringAOP 声明式事务方式将事务控制在业务层的实现类的方法上面,一个请求对应一套业务流程,对应一个事务控制。前面我们也提到了事物的 4 个特性中,有一个隔离性表示事务与事务间是相互隔离的,互不影响的。 4 | 5 | 事务脚本将所有这些业务逻辑组织成单个过程,在组织时通常都是考虑的业务逻辑的过程,将业务逻辑按照一定的顺序排列执行,然后将事务控制在某一个主方法的入口上,并且在过程中直接调用数据库,或者调用数据库封装器(如 MyBatis、Hibernate 等 ORM 映射框架)。每个事物都有属于它自己的事物脚本,这里的脚本指的是方法。事务脚本比较适合于业务逻辑相对简单的情况下,理论上来说,事务控制在 3 层架构中任何一层都是可以的,但是我们通常来说,都是控制在业务层的实现上的。事务脚本是属于领域逻辑模式中一种,将其置于与其他处理表现层和数据源层的类相对独立的类中。如果置于数据源层的类中,会失去事务脚本的控制原则,因为可能只控制了一部分业务逻辑,而其他的业务逻辑并没有控制到。 6 | 7 | 对于事务脚本来说,很多时候都有 3 个具体的类来构成,他们一般分别是 Sevice,Dao,JavaBean, Service 类主要用于组织业务逻辑,Dao 类主要用于实现数据的存储,JavaBean 主要用于数据的封装。事务脚本贵在简单,对于少量逻辑的应用程序来说,它非常实用。如果业务逻辑复杂时,就会导致事务之间的冗余代码。 8 | 9 | ```java 10 | public void Save(){ 11 | Dao1 dao1=new Dao1(); dao1.IsExists();//检查一些东西 12 | Dao2 dao1=new Dao2(); dao2.Save();//保存一些数据 13 | } 14 | 15 | 16 | public void Submit(){ 17 | try{ 18 | BeginTran(); 19 | Dao1 dao1=new Dao1(); dao1.IsExists();//检查一些东西 20 | Dao2 dao1=new Dao2(); dao2.Save();//保存一些数据 21 | Dao3 dao3=new Dao3(); dao3.GoToNextFlow();//流程提交 22 | Commit(); 23 | }catch(){ 24 | Rollback(); 25 | } 26 | } 27 | ``` 28 | 29 | 事务脚本的优势是简单,容易上手,DAO(数据访问对象)的重用可能性高,强于经典的三层。不过与与使用 O/RM 相比,还是需要写 SQL 语句,虽然可以重用数据访问的代码,因为没有领域模型,所以很多业务规则依然没办法重用。经典的三层一旦遇到事务密集的项目,业务层和数据访问层就会黏在一起了,如果对三层进行改良,把事务管理提上来,放在业务层,然后思维模式稍加改变,就变成了事务脚本。 30 | 31 | # 典型应用 32 | 33 | 这里以银行转账事务脚本实现为例,在事务脚本的实现中,关于在两个账号之间转账的领域业务逻辑都被写在了 MoneyTransferService 的实现里面了,而 Account 仅仅是 getters 和 setters 的数据结构,也就是我们说的贫血模型: 34 | 35 | ```java 36 | public class MoneyTransferServiceTransactionScriptImpl 37 | implements MoneyTransferService { 38 | private AccountDao accountDao; 39 | private BankingTransactionRepository bankingTransactionRepository; 40 | . . . 41 | @Override 42 | public BankingTransaction transfer( 43 | String fromAccountId, String toAccountId, double amount) { 44 | Account fromAccount = accountDao.findById(fromAccountId); 45 | Account toAccount = accountDao.findById(toAccountId); 46 | // . . . 47 | double newBalance = fromAccount.getBalance() - amount; 48 | switch (fromAccount.getOverdraftPolicy()) { 49 | case NEVER: 50 | if (newBalance < 0) { 51 | throw new DebitException("Insufficient funds"); 52 | } 53 | break; 54 | case ALLOWED: 55 | if (newBalance < -limit) { 56 | throw new DebitException( 57 | "Overdraft limit (of " + limit + ") exceeded: " + newBalance); 58 | } 59 | break; 60 | } 61 | fromAccount.setBalance(newBalance); 62 | toAccount.setBalance(toAccount.getBalance() + amount); 63 | BankingTransaction moneyTransferTransaction = 64 | new MoneyTranferTransaction(fromAccountId, toAccountId, amount); 65 | bankingTransactionRepository.addTransaction(moneyTransferTransaction); 66 | return moneyTransferTransaction; 67 | } 68 | } 69 | ``` 70 | 71 | 这完全是面向过程的代码风格。 72 | -------------------------------------------------------------------------------- /02~架构风格与模式/CRUD/事务脚本/活动记录.md: -------------------------------------------------------------------------------- 1 | # 活动记录 2 | -------------------------------------------------------------------------------- /02~架构风格与模式/CRUD/事务脚本/表模块.md: -------------------------------------------------------------------------------- 1 | # 表模块 2 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: maven 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up JDK 20 | uses: actions/setup-java@v4.2.2 21 | with: 22 | java-version: 19 23 | distribution: 'adopt' 24 | 25 | - name: Cache 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/.m2/repository 29 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 30 | restore-keys: | 31 | ${{ runner.os }}-maven- 32 | 33 | - name: Build with Maven 34 | run: mvn -B verify -Pcode-coverage --file pom.xml 35 | 36 | - name: Code Coverage Report 37 | if: ${{ github.actor != 'dependabot[bot]' }} 38 | env: 39 | GIT_BRANCH: ${GITHUB_REF/refs\/heads\//} 40 | JACOCO_SOURCE_PATH: src/main/java 41 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 42 | run: | 43 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 44 | chmod +x ./cc-test-reporter 45 | ./cc-test-reporter format-coverage -t jacoco target/site/jacoco/jacoco.xml -o coverage/unit.json 46 | ./cc-test-reporter format-coverage -t jacoco target/site/jacoco-it/jacoco.xml -o coverage/it.json 47 | ./cc-test-reporter sum-coverage coverage/*.json -p 2 48 | ./cc-test-reporter upload-coverage 49 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | *.iws 5 | 6 | # Maven 7 | target/ 8 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andre.schaffer@gmail.com or dan.eidmark@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | - Fork this project. 3 | - Create a topic branch. 4 | - Improve it with some commits. 5 | - Ensure new code has tests for it and all tests pass. 6 | - Push it to your forked project. 7 | - Submit a Pull Request. 8 | - Pour yourself a glass of champagne and feel good about contributing to open source! 9 | 10 | By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). 11 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/environments/development.yml: -------------------------------------------------------------------------------- 1 | server: 2 | applicationConnectors: 3 | - type: http 4 | port: 8080 5 | adminConnectors: 6 | - type: http 7 | port: 8081 8 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/bootstrap/BankServiceApplication.java: -------------------------------------------------------------------------------- 1 | package bankservice.bootstrap; 2 | 3 | import static java.util.concurrent.Executors.newSingleThreadExecutor; 4 | import static java.util.logging.Level.INFO; 5 | import static java.util.logging.Logger.getLogger; 6 | import static org.glassfish.jersey.logging.LoggingFeature.DEFAULT_LOGGER_NAME; 7 | import static org.glassfish.jersey.logging.LoggingFeature.Verbosity.PAYLOAD_ANY; 8 | 9 | import bankservice.domain.model.EventStore; 10 | import bankservice.port.incoming.adapter.resources.OptimisticLockingExceptionMapper; 11 | import bankservice.port.incoming.adapter.resources.accounts.AccountNotFoundExceptionMapper; 12 | import bankservice.port.incoming.adapter.resources.accounts.AccountResource; 13 | import bankservice.port.incoming.adapter.resources.accounts.AccountsResource; 14 | import bankservice.port.incoming.adapter.resources.accounts.deposits.DepositsResource; 15 | import bankservice.port.incoming.adapter.resources.accounts.withdrawals.WithdrawalsResource; 16 | import bankservice.port.incoming.adapter.resources.clients.ClientResource; 17 | import bankservice.port.incoming.adapter.resources.clients.ClientsResource; 18 | import bankservice.port.outgoing.adapter.eventstore.InMemoryEventStore; 19 | import bankservice.projection.accounttransactions.AccountTransactionsResource; 20 | import bankservice.projection.accounttransactions.InMemoryTransactionsRepository; 21 | import bankservice.projection.accounttransactions.TransactionsListener; 22 | import bankservice.projection.accounttransactions.TransactionsRepository; 23 | import bankservice.projection.clientaccounts.AccountsListener; 24 | import bankservice.projection.clientaccounts.AccountsRepository; 25 | import bankservice.projection.clientaccounts.ClientAccountsResource; 26 | import bankservice.projection.clientaccounts.InMemoryAccountsRepository; 27 | import bankservice.service.account.AccountService; 28 | import bankservice.service.client.ClientService; 29 | import com.google.common.eventbus.AsyncEventBus; 30 | import com.google.common.eventbus.EventBus; 31 | import io.dropwizard.core.Application; 32 | import io.dropwizard.core.Configuration; 33 | import io.dropwizard.core.setup.Environment; 34 | import org.glassfish.jersey.linking.DeclarativeLinkingFeature; 35 | import org.glassfish.jersey.logging.LoggingFeature; 36 | 37 | public class BankServiceApplication extends Application { 38 | 39 | public static void main(String[] args) throws Exception { 40 | new BankServiceApplication().run(args); 41 | } 42 | 43 | @Override 44 | public void run(Configuration configuration, Environment environment) throws Exception { 45 | registerFilters(environment); 46 | registerExceptionMappers(environment); 47 | registerHypermediaSupport(environment); 48 | registerResources(environment); 49 | } 50 | 51 | private void registerFilters(Environment environment) { 52 | environment.jersey() 53 | .register(new LoggingFeature(getLogger(DEFAULT_LOGGER_NAME), INFO, PAYLOAD_ANY, 1024)); 54 | } 55 | 56 | private void registerExceptionMappers(Environment environment) { 57 | environment.jersey().register(AccountNotFoundExceptionMapper.class); 58 | environment.jersey().register(OptimisticLockingExceptionMapper.class); 59 | } 60 | 61 | private void registerHypermediaSupport(Environment environment) { 62 | environment.jersey().getResourceConfig().register(DeclarativeLinkingFeature.class); 63 | } 64 | 65 | private void registerResources(Environment environment) { 66 | EventStore eventStore = new InMemoryEventStore(); 67 | EventBus eventBus = new AsyncEventBus(newSingleThreadExecutor()); 68 | 69 | // domain model 70 | AccountService accountService = new AccountService(eventStore, eventBus); 71 | environment.jersey().register(new AccountsResource(accountService)); 72 | environment.jersey().register(new AccountResource(accountService)); 73 | environment.jersey().register(new DepositsResource(accountService)); 74 | environment.jersey().register(new WithdrawalsResource(accountService)); 75 | 76 | ClientService clientService = new ClientService(eventStore); 77 | environment.jersey().register(new ClientsResource(clientService)); 78 | environment.jersey().register(new ClientResource(clientService)); 79 | 80 | // read model 81 | TransactionsRepository transactionsRepository = new InMemoryTransactionsRepository(); 82 | eventBus.register(new TransactionsListener(transactionsRepository)); 83 | environment.jersey().register(new AccountTransactionsResource(transactionsRepository)); 84 | 85 | AccountsRepository accountsRepository = new InMemoryAccountsRepository(); 86 | eventBus.register(new AccountsListener(accountsRepository)); 87 | environment.jersey().register(new ClientAccountsResource(accountsRepository)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/Aggregate.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | import static com.google.common.base.Preconditions.checkNotNull; 5 | import static java.lang.String.format; 6 | import static java.util.Collections.emptyList; 7 | 8 | import com.google.common.base.Throwables; 9 | import com.google.common.collect.ImmutableList; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.lang.reflect.Method; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.UUID; 15 | 16 | public abstract class Aggregate { 17 | 18 | private UUID id; 19 | private int baseVersion; 20 | private List newEvents; 21 | 22 | protected Aggregate(UUID id) { 23 | this(id, emptyList()); 24 | } 25 | 26 | protected Aggregate(UUID id, List eventStream) { 27 | checkNotNull(id); 28 | checkNotNull(eventStream); 29 | this.id = id; 30 | eventStream.forEach(e -> { 31 | apply(e); 32 | this.baseVersion = e.getVersion(); 33 | }); 34 | this.newEvents = new ArrayList<>(); 35 | } 36 | 37 | protected void applyNewEvent(Event event) { 38 | checkArgument(event.getVersion() == getNextVersion(), 39 | "New event version '%s' does not match expected next version '%s'", 40 | event.getVersion(), getNextVersion()); 41 | apply(event); 42 | newEvents.add(event); 43 | } 44 | 45 | private void apply(Event event) { 46 | try { 47 | Method method = this.getClass().getDeclaredMethod("apply", event.getClass()); 48 | method.setAccessible(true); 49 | method.invoke(this, event); 50 | } catch (InvocationTargetException e) { 51 | Throwables.propagate(e.getCause()); 52 | } catch (NoSuchMethodException | IllegalAccessException e) { 53 | throw new UnsupportedOperationException( 54 | format("Aggregate '%s' doesn't apply event type '%s'", this.getClass(), event.getClass()), 55 | e); 56 | } 57 | } 58 | 59 | public UUID getId() { 60 | return id; 61 | } 62 | 63 | public int getBaseVersion() { 64 | return baseVersion; 65 | } 66 | 67 | public List getNewEvents() { 68 | return ImmutableList.copyOf(newEvents); 69 | } 70 | 71 | protected int getNextVersion() { 72 | return baseVersion + newEvents.size() + 1; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/Event.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.time.ZonedDateTime; 6 | import java.util.UUID; 7 | 8 | public abstract class Event { 9 | 10 | private final UUID aggregateId; 11 | private final ZonedDateTime timestamp; 12 | private final int version; 13 | 14 | protected Event(UUID aggregateId, ZonedDateTime timestamp, int version) { 15 | this.aggregateId = checkNotNull(aggregateId); 16 | this.timestamp = checkNotNull(timestamp); 17 | this.version = version; 18 | } 19 | 20 | public UUID getAggregateId() { 21 | return aggregateId; 22 | } 23 | 24 | public ZonedDateTime getTimestamp() { 25 | return this.timestamp; 26 | } 27 | 28 | public int getVersion() { 29 | return version; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/EventStore.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | public interface EventStore { 7 | 8 | void store(UUID aggregateId, List newEvents, int baseVersion) 9 | throws OptimisticLockingException; 10 | 11 | List load(UUID aggregateId); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/OptimisticLockingException.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model; 2 | 3 | public class OptimisticLockingException extends RuntimeException { 4 | 5 | public OptimisticLockingException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/Specification.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model; 2 | 3 | public interface Specification { 4 | 5 | boolean isSatisfiedBy(T value); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/ValueObject.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | import org.apache.commons.lang3.builder.ToStringStyle; 7 | 8 | public abstract class ValueObject { 9 | 10 | @Override 11 | public boolean equals(Object o) { 12 | return EqualsBuilder.reflectionEquals(this, o); 13 | } 14 | 15 | @Override 16 | public int hashCode() { 17 | return HashCodeBuilder.reflectionHashCode(this); 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/account/Account.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.account; 2 | 3 | import static java.math.BigDecimal.ZERO; 4 | import static java.time.ZoneOffset.UTC; 5 | import static java.time.ZonedDateTime.now; 6 | 7 | import bankservice.domain.model.Aggregate; 8 | import bankservice.domain.model.Event; 9 | import java.math.BigDecimal; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | public class Account extends Aggregate { 14 | 15 | private BigDecimal balance; 16 | private UUID clientId; 17 | 18 | public Account(UUID id, UUID clientId) { 19 | super(id); 20 | AccountOpenedEvent accountOpenedEvent = new AccountOpenedEvent( 21 | id, now(UTC), getNextVersion(), clientId, ZERO); 22 | applyNewEvent(accountOpenedEvent); 23 | } 24 | 25 | public Account(UUID id, List eventStream) { 26 | super(id, eventStream); 27 | } 28 | 29 | public void deposit(BigDecimal amount) { 30 | BigDecimal newBalance = balance.add(amount); 31 | AccountDepositedEvent accountDepositedEvent = new AccountDepositedEvent( 32 | getId(), now(UTC), getNextVersion(), amount, newBalance); 33 | applyNewEvent(accountDepositedEvent); 34 | } 35 | 36 | public void withdraw(BigDecimal amount) { 37 | BigDecimal newBalance = balance.subtract(amount); 38 | if (newBalance.signum() == -1) { 39 | throw new NonSufficientFundsException(getId(), balance, amount); 40 | } 41 | AccountWithdrawnEvent accountWithdrawnEvent = new AccountWithdrawnEvent( 42 | getId(), now(UTC), getNextVersion(), amount, newBalance); 43 | applyNewEvent(accountWithdrawnEvent); 44 | } 45 | 46 | @SuppressWarnings("unused") 47 | private void apply(AccountOpenedEvent event) { 48 | clientId = event.getClientId(); 49 | balance = event.getBalance(); 50 | } 51 | 52 | @SuppressWarnings("unused") 53 | private void apply(AccountDepositedEvent event) { 54 | balance = event.getBalance(); 55 | } 56 | 57 | @SuppressWarnings("unused") 58 | private void apply(AccountWithdrawnEvent event) { 59 | balance = event.getBalance(); 60 | } 61 | 62 | public BigDecimal getBalance() { 63 | return balance; 64 | } 65 | 66 | public UUID getClientId() { 67 | return clientId; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/account/AccountDepositedEvent.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.Event; 6 | import java.math.BigDecimal; 7 | import java.time.ZonedDateTime; 8 | import java.util.UUID; 9 | 10 | public class AccountDepositedEvent extends Event { 11 | 12 | private final BigDecimal amount; 13 | private final BigDecimal balance; 14 | 15 | public AccountDepositedEvent(UUID aggregateId, ZonedDateTime timestamp, int version, 16 | BigDecimal amount, BigDecimal balance) { 17 | super(aggregateId, timestamp, version); 18 | this.amount = checkNotNull(amount); 19 | this.balance = checkNotNull(balance); 20 | } 21 | 22 | public BigDecimal getAmount() { 23 | return amount; 24 | } 25 | 26 | public BigDecimal getBalance() { 27 | return balance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/account/AccountOpenedEvent.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.Event; 6 | import java.math.BigDecimal; 7 | import java.time.ZonedDateTime; 8 | import java.util.UUID; 9 | 10 | public class AccountOpenedEvent extends Event { 11 | 12 | private final String clientId; 13 | private final BigDecimal balance; 14 | 15 | public AccountOpenedEvent(UUID aggregateId, ZonedDateTime timestamp, int version, UUID clientId, 16 | BigDecimal balance) { 17 | super(aggregateId, timestamp, version); 18 | this.clientId = checkNotNull(clientId).toString(); 19 | this.balance = checkNotNull(balance); 20 | } 21 | 22 | public UUID getClientId() { 23 | return UUID.fromString(clientId); 24 | } 25 | 26 | public BigDecimal getBalance() { 27 | return balance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/account/AccountWithdrawnEvent.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.Event; 6 | import java.math.BigDecimal; 7 | import java.time.ZonedDateTime; 8 | import java.util.UUID; 9 | 10 | public class AccountWithdrawnEvent extends Event { 11 | 12 | private final BigDecimal amount; 13 | private final BigDecimal balance; 14 | 15 | public AccountWithdrawnEvent(UUID aggregateId, ZonedDateTime timestamp, int version, 16 | BigDecimal amount, BigDecimal balance) { 17 | super(aggregateId, timestamp, version); 18 | this.amount = checkNotNull(amount); 19 | this.balance = checkNotNull(balance); 20 | } 21 | 22 | public BigDecimal getAmount() { 23 | return amount; 24 | } 25 | 26 | public BigDecimal getBalance() { 27 | return balance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/account/NonSufficientFundsException.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.account; 2 | 3 | import static java.lang.String.format; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.UUID; 7 | 8 | public class NonSufficientFundsException extends RuntimeException { 9 | 10 | public NonSufficientFundsException(UUID accountId, BigDecimal balance, BigDecimal amount) { 11 | super(format("Withdrawal of '%s' failed as there is only '%s' in account '%s'", amount, balance, 12 | accountId)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/client/Client.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.client; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | import static com.google.common.base.Preconditions.checkNotNull; 5 | import static java.time.ZoneOffset.UTC; 6 | import static java.time.ZonedDateTime.now; 7 | import static org.apache.commons.lang3.StringUtils.isNotBlank; 8 | 9 | import bankservice.domain.model.Aggregate; 10 | import bankservice.domain.model.Event; 11 | import java.util.List; 12 | import java.util.UUID; 13 | 14 | public class Client extends Aggregate { 15 | 16 | private String name; 17 | private Email email; 18 | 19 | public Client(UUID id, String name, Email email) { 20 | super(id); 21 | validateName(name); 22 | validateEmail(email); 23 | ClientEnrolledEvent clientEnrolledEvent = new ClientEnrolledEvent( 24 | id, now(UTC), getNextVersion(), name, email); 25 | applyNewEvent(clientEnrolledEvent); 26 | } 27 | 28 | public Client(UUID id, List eventStream) { 29 | super(id, eventStream); 30 | } 31 | 32 | public void update(String name, Email email) { 33 | ClientUpdatedEvent clientUpdatedEvent = new ClientUpdatedEvent( 34 | getId(), now(UTC), getNextVersion(), name, email); 35 | applyNewEvent(clientUpdatedEvent); 36 | } 37 | 38 | @SuppressWarnings("unused") 39 | public void apply(ClientEnrolledEvent event) { 40 | this.name = event.getName(); 41 | this.email = event.getEmail(); 42 | } 43 | 44 | @SuppressWarnings("unused") 45 | private void apply(ClientUpdatedEvent event) { 46 | this.name = event.getName(); 47 | this.email = event.getEmail(); 48 | } 49 | 50 | public String getName() { 51 | return name; 52 | } 53 | 54 | public Email getEmail() { 55 | return email; 56 | } 57 | 58 | private void validateName(String name) { 59 | checkArgument(isNotBlank(name)); 60 | } 61 | 62 | private void validateEmail(Email email) { 63 | checkNotNull(email); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/client/ClientEnrolledEvent.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.client; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.Event; 6 | import java.time.ZonedDateTime; 7 | import java.util.UUID; 8 | 9 | public class ClientEnrolledEvent extends Event { 10 | 11 | private final String name; 12 | private final String email; 13 | 14 | public ClientEnrolledEvent(UUID aggregateId, ZonedDateTime timestamp, int version, String name, 15 | Email email) { 16 | super(aggregateId, timestamp, version); 17 | this.name = checkNotNull(name); 18 | this.email = checkNotNull(email).getValue(); 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public Email getEmail() { 26 | return new Email(email); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/client/ClientUpdatedEvent.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.client; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.Event; 6 | import java.time.ZonedDateTime; 7 | import java.util.UUID; 8 | 9 | public class ClientUpdatedEvent extends Event { 10 | 11 | private final String name; 12 | private final String email; 13 | 14 | public ClientUpdatedEvent(UUID aggregateId, ZonedDateTime timestamp, int version, String name, 15 | Email email) { 16 | super(aggregateId, timestamp, version); 17 | this.name = checkNotNull(name); 18 | this.email = checkNotNull(email).getValue(); 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public Email getEmail() { 26 | return new Email(email); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/domain/model/client/Email.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.client; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import bankservice.domain.model.Specification; 6 | import bankservice.domain.model.ValueObject; 7 | import org.apache.commons.validator.routines.EmailValidator; 8 | 9 | public class Email extends ValueObject { 10 | 11 | private final String value; 12 | 13 | public Email(String value) { 14 | checkArgument(new EmailSpecification().isSatisfiedBy(value)); 15 | this.value = value; 16 | } 17 | 18 | public String getValue() { 19 | return value; 20 | } 21 | 22 | public static class EmailSpecification implements Specification { 23 | 24 | @Override 25 | public boolean isSatisfiedBy(String value) { 26 | return EmailValidator.getInstance().isValid(value); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/OptimisticLockingExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources; 2 | 3 | 4 | import static jakarta.ws.rs.core.Response.Status.CONFLICT; 5 | 6 | import bankservice.domain.model.OptimisticLockingException; 7 | import jakarta.ws.rs.core.Response; 8 | import jakarta.ws.rs.ext.ExceptionMapper; 9 | import jakarta.ws.rs.ext.Provider; 10 | 11 | @Provider 12 | public class OptimisticLockingExceptionMapper implements 13 | ExceptionMapper { 14 | 15 | @Override 16 | public Response toResponse(OptimisticLockingException exception) { 17 | return Response.status(CONFLICT).build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/AccountDto.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts; 2 | 3 | import static com.fasterxml.jackson.annotation.JsonProperty.Access.READ_ONLY; 4 | import static java.math.BigDecimal.ROUND_HALF_UP; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import java.math.BigDecimal; 8 | import java.util.UUID; 9 | 10 | public class AccountDto { 11 | 12 | @JsonProperty(access = READ_ONLY) 13 | private UUID id; 14 | 15 | @JsonProperty(access = READ_ONLY) 16 | private BigDecimal balance; 17 | 18 | private UUID clientId; 19 | 20 | @SuppressWarnings("unused") 21 | public UUID getId() { 22 | return id; 23 | } 24 | 25 | @SuppressWarnings("unused") 26 | public void setId(UUID id) { 27 | this.id = id; 28 | } 29 | 30 | @SuppressWarnings("unused") 31 | public BigDecimal getBalance() { 32 | return balance; 33 | } 34 | 35 | @SuppressWarnings("unused") 36 | public void setBalance(BigDecimal balance) { 37 | this.balance = balance.setScale(2, ROUND_HALF_UP); 38 | } 39 | 40 | @SuppressWarnings("unused") 41 | public UUID getClientId() { 42 | return clientId; 43 | } 44 | 45 | @SuppressWarnings("unused") 46 | public void setClientId(UUID clientId) { 47 | this.clientId = clientId; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/AccountNotFoundExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts; 2 | 3 | 4 | import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; 5 | 6 | import bankservice.service.account.AccountNotFoundException; 7 | import jakarta.ws.rs.core.Response; 8 | import jakarta.ws.rs.ext.ExceptionMapper; 9 | import jakarta.ws.rs.ext.Provider; 10 | 11 | @Provider 12 | public class AccountNotFoundExceptionMapper implements ExceptionMapper { 13 | 14 | @Override 15 | public Response toResponse(AccountNotFoundException exception) { 16 | return Response.status(NOT_FOUND).build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/AccountResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; 6 | 7 | import bankservice.domain.model.account.Account; 8 | import bankservice.service.account.AccountService; 9 | import io.dropwizard.jersey.params.UUIDParam; 10 | import jakarta.ws.rs.Consumes; 11 | import jakarta.ws.rs.GET; 12 | import jakarta.ws.rs.Path; 13 | import jakarta.ws.rs.PathParam; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.core.Response; 16 | import java.util.Optional; 17 | 18 | @Consumes(APPLICATION_JSON) 19 | @Produces(APPLICATION_JSON) 20 | @Path("/accounts/{id}") 21 | public class AccountResource { 22 | 23 | private final AccountService accountService; 24 | 25 | public AccountResource(AccountService accountService) { 26 | this.accountService = checkNotNull(accountService); 27 | } 28 | 29 | @GET 30 | public Response get(@PathParam("id") UUIDParam accountId) { 31 | Optional possibleAccount = accountService.loadAccount(accountId.get()); 32 | if (!possibleAccount.isPresent()) { 33 | return Response.status(NOT_FOUND).build(); 34 | } 35 | AccountDto accountDto = toDto(possibleAccount.get()); 36 | return Response.ok(accountDto).build(); 37 | } 38 | 39 | private AccountDto toDto(Account account) { 40 | AccountDto dto = new AccountDto(); 41 | dto.setId(account.getId()); 42 | dto.setBalance(account.getBalance()); 43 | dto.setClientId(account.getClientId()); 44 | return dto; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/AccountsResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static jakarta.ws.rs.core.UriBuilder.fromResource; 6 | 7 | import bankservice.domain.model.account.Account; 8 | import bankservice.service.account.AccountService; 9 | import bankservice.service.account.OpenAccountCommand; 10 | import jakarta.validation.Valid; 11 | import jakarta.ws.rs.Consumes; 12 | import jakarta.ws.rs.POST; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.core.Response; 16 | import java.net.URI; 17 | 18 | @Consumes(APPLICATION_JSON) 19 | @Produces(APPLICATION_JSON) 20 | @Path("/accounts") 21 | public class AccountsResource { 22 | 23 | private final AccountService accountService; 24 | 25 | public AccountsResource(AccountService accountService) { 26 | this.accountService = checkNotNull(accountService); 27 | } 28 | 29 | @POST 30 | public Response post(@Valid AccountDto accountDto) { 31 | OpenAccountCommand command = new OpenAccountCommand(accountDto.getClientId()); 32 | Account account = accountService.process(command); 33 | URI accountUri = fromResource(AccountResource.class).build(account.getId()); 34 | return Response.created(accountUri).build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/deposits/DepositDto.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts.deposits; 2 | 3 | import static com.fasterxml.jackson.annotation.JsonProperty.Access.READ_ONLY; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.math.BigDecimal; 8 | import java.util.UUID; 9 | 10 | public class DepositDto { 11 | 12 | @JsonProperty(access = READ_ONLY) 13 | private UUID accountId; 14 | 15 | @NotNull 16 | private BigDecimal amount; 17 | 18 | 19 | @SuppressWarnings("unused") 20 | public UUID getAccountId() { 21 | return accountId; 22 | } 23 | 24 | @SuppressWarnings("unused") 25 | public void setAccountId(UUID accountId) { 26 | this.accountId = accountId; 27 | } 28 | 29 | @SuppressWarnings("unused") 30 | public BigDecimal getAmount() { 31 | return amount; 32 | } 33 | 34 | @SuppressWarnings("unused") 35 | public void setAmount(BigDecimal amount) { 36 | this.amount = amount; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/deposits/DepositsResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts.deposits; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | 6 | import bankservice.domain.model.OptimisticLockingException; 7 | import bankservice.service.account.AccountNotFoundException; 8 | import bankservice.service.account.AccountService; 9 | import bankservice.service.account.DepositAccountCommand; 10 | import io.dropwizard.jersey.params.UUIDParam; 11 | import jakarta.validation.Valid; 12 | import jakarta.ws.rs.Consumes; 13 | import jakarta.ws.rs.POST; 14 | import jakarta.ws.rs.Path; 15 | import jakarta.ws.rs.PathParam; 16 | import jakarta.ws.rs.Produces; 17 | import jakarta.ws.rs.core.Response; 18 | 19 | @Consumes(APPLICATION_JSON) 20 | @Produces(APPLICATION_JSON) 21 | @Path("/accounts/{id}/deposits") 22 | public class DepositsResource { 23 | 24 | private final AccountService accountService; 25 | 26 | public DepositsResource(AccountService accountService) { 27 | this.accountService = checkNotNull(accountService); 28 | } 29 | 30 | @POST 31 | public Response post(@PathParam("id") UUIDParam accountId, @Valid DepositDto depositDto) 32 | throws AccountNotFoundException, OptimisticLockingException { 33 | 34 | DepositAccountCommand command = new DepositAccountCommand(accountId.get(), 35 | depositDto.getAmount()); 36 | accountService.process(command); 37 | return Response.noContent().build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/withdrawals/WithdrawalDto.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts.withdrawals; 2 | 3 | import static com.fasterxml.jackson.annotation.JsonProperty.Access.READ_ONLY; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.math.BigDecimal; 8 | import java.util.UUID; 9 | 10 | public class WithdrawalDto { 11 | 12 | @JsonProperty(access = READ_ONLY) 13 | private UUID accountId; 14 | 15 | @NotNull 16 | private BigDecimal amount; 17 | 18 | 19 | @SuppressWarnings("unused") 20 | public UUID getAccountId() { 21 | return accountId; 22 | } 23 | 24 | @SuppressWarnings("unused") 25 | public void setAccountId(UUID accountId) { 26 | this.accountId = accountId; 27 | } 28 | 29 | @SuppressWarnings("unused") 30 | public BigDecimal getAmount() { 31 | return amount; 32 | } 33 | 34 | @SuppressWarnings("unused") 35 | public void setAmount(BigDecimal amount) { 36 | this.amount = amount; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/accounts/withdrawals/WithdrawalsResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts.withdrawals; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; 6 | 7 | import bankservice.domain.model.OptimisticLockingException; 8 | import bankservice.domain.model.account.NonSufficientFundsException; 9 | import bankservice.service.account.AccountNotFoundException; 10 | import bankservice.service.account.AccountService; 11 | import bankservice.service.account.WithdrawAccountCommand; 12 | import io.dropwizard.jersey.params.UUIDParam; 13 | import jakarta.validation.Valid; 14 | import jakarta.ws.rs.Consumes; 15 | import jakarta.ws.rs.POST; 16 | import jakarta.ws.rs.Path; 17 | import jakarta.ws.rs.PathParam; 18 | import jakarta.ws.rs.Produces; 19 | import jakarta.ws.rs.core.Response; 20 | 21 | @Consumes(APPLICATION_JSON) 22 | @Produces(APPLICATION_JSON) 23 | @Path("/accounts/{id}/withdrawals") 24 | public class WithdrawalsResource { 25 | 26 | private final AccountService accountService; 27 | 28 | public WithdrawalsResource(AccountService accountService) { 29 | this.accountService = checkNotNull(accountService); 30 | } 31 | 32 | @POST 33 | public Response post(@PathParam("id") UUIDParam accountId, @Valid WithdrawalDto withdrawalDto) 34 | throws AccountNotFoundException, OptimisticLockingException { 35 | 36 | WithdrawAccountCommand command = new WithdrawAccountCommand(accountId.get(), 37 | withdrawalDto.getAmount()); 38 | try { 39 | accountService.process(command); 40 | } catch (NonSufficientFundsException e) { 41 | return Response.status(BAD_REQUEST).build(); 42 | } 43 | return Response.noContent().build(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/clients/ClientDto.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.clients; 2 | 3 | import static com.fasterxml.jackson.annotation.JsonProperty.Access.READ_ONLY; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import jakarta.validation.constraints.NotBlank; 7 | import java.util.UUID; 8 | 9 | public class ClientDto { 10 | 11 | @JsonProperty(access = READ_ONLY) 12 | private UUID id; 13 | 14 | @NotBlank 15 | private String name; 16 | 17 | @Email 18 | private String email; 19 | 20 | 21 | @SuppressWarnings("unused") 22 | public UUID getId() { 23 | return id; 24 | } 25 | 26 | @SuppressWarnings("unused") 27 | public void setId(UUID id) { 28 | this.id = id; 29 | } 30 | 31 | @SuppressWarnings("unused") 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | @SuppressWarnings("unused") 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | @SuppressWarnings("unused") 42 | public String getEmail() { 43 | return email; 44 | } 45 | 46 | @SuppressWarnings("unused") 47 | public void setEmail(String email) { 48 | this.email = email; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/clients/ClientResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.clients; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; 6 | 7 | import bankservice.domain.model.client.Client; 8 | import bankservice.domain.model.client.Email; 9 | import bankservice.service.client.ClientService; 10 | import bankservice.service.client.UpdateClientCommand; 11 | import io.dropwizard.jersey.params.UUIDParam; 12 | import jakarta.validation.Valid; 13 | import jakarta.validation.constraints.NotNull; 14 | import jakarta.ws.rs.Consumes; 15 | import jakarta.ws.rs.GET; 16 | import jakarta.ws.rs.PUT; 17 | import jakarta.ws.rs.Path; 18 | import jakarta.ws.rs.PathParam; 19 | import jakarta.ws.rs.Produces; 20 | import jakarta.ws.rs.core.Response; 21 | import java.util.Optional; 22 | 23 | @Consumes(APPLICATION_JSON) 24 | @Produces(APPLICATION_JSON) 25 | @Path("/clients/{id}") 26 | public class ClientResource { 27 | 28 | private ClientService clientService; 29 | 30 | public ClientResource(ClientService clientService) { 31 | this.clientService = checkNotNull(clientService); 32 | } 33 | 34 | @GET 35 | public Response get(@PathParam("id") UUIDParam clientId) { 36 | Optional possibleClient = clientService.loadClient(clientId.get()); 37 | if (!possibleClient.isPresent()) { 38 | return Response.status(NOT_FOUND).build(); 39 | } 40 | ClientDto clientDto = toDto(possibleClient.get()); 41 | return Response.ok(clientDto).build(); 42 | } 43 | 44 | @PUT 45 | public Response put(@PathParam("id") UUIDParam clientId, @Valid @NotNull ClientDto clientDto) { 46 | UpdateClientCommand command = new UpdateClientCommand( 47 | clientId.get(), clientDto.getName(), new Email(clientDto.getEmail())); 48 | clientService.process(command); 49 | return Response.noContent().build(); 50 | } 51 | 52 | private ClientDto toDto(Client client) { 53 | ClientDto dto = new ClientDto(); 54 | dto.setId(client.getId()); 55 | dto.setName(client.getName()); 56 | dto.setEmail(client.getEmail().getValue()); 57 | return dto; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/clients/ClientsResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.clients; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static jakarta.ws.rs.core.UriBuilder.fromResource; 6 | 7 | import bankservice.domain.model.client.Client; 8 | import bankservice.domain.model.client.Email; 9 | import bankservice.service.client.ClientService; 10 | import bankservice.service.client.EnrollClientCommand; 11 | import jakarta.validation.Valid; 12 | import jakarta.ws.rs.Consumes; 13 | import jakarta.ws.rs.POST; 14 | import jakarta.ws.rs.Path; 15 | import jakarta.ws.rs.Produces; 16 | import jakarta.ws.rs.core.Response; 17 | import java.net.URI; 18 | 19 | @Consumes(APPLICATION_JSON) 20 | @Produces(APPLICATION_JSON) 21 | @Path("/clients") 22 | public class ClientsResource { 23 | 24 | private ClientService clientService; 25 | 26 | public ClientsResource(ClientService clientService) { 27 | this.clientService = checkNotNull(clientService); 28 | } 29 | 30 | @POST 31 | public Response post(@Valid ClientDto newClientDto) { 32 | EnrollClientCommand enrollClientCommand = new EnrollClientCommand( 33 | newClientDto.getName(), new Email(newClientDto.getEmail())); 34 | Client client = clientService.process(enrollClientCommand); 35 | URI clientUri = fromResource(ClientResource.class).build(client.getId()); 36 | return Response.created(clientUri).build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/incoming/adapter/resources/clients/Email.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.clients; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import bankservice.domain.model.client.Email.EmailSpecification; 7 | import jakarta.validation.Constraint; 8 | import jakarta.validation.ConstraintValidator; 9 | import jakarta.validation.ConstraintValidatorContext; 10 | import jakarta.validation.Payload; 11 | import jakarta.validation.constraints.NotNull; 12 | import java.lang.annotation.Retention; 13 | import java.lang.annotation.Target; 14 | 15 | @NotNull 16 | @Target(FIELD) 17 | @Retention(RUNTIME) 18 | @Constraint(validatedBy = Email.EmailValidator.class) 19 | public @interface Email { 20 | 21 | String message() default "not a well-formed email address"; 22 | 23 | Class[] groups() default {}; 24 | 25 | Class[] payload() default {}; 26 | 27 | 28 | class EmailValidator implements ConstraintValidator { 29 | 30 | private EmailSpecification emailSpecification = new EmailSpecification(); 31 | 32 | @Override 33 | public void initialize(Email constraintAnnotation) { 34 | } 35 | 36 | @Override 37 | public boolean isValid(String value, ConstraintValidatorContext context) { 38 | return emailSpecification.isSatisfiedBy(value); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/port/outgoing/adapter/eventstore/InMemoryEventStore.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.outgoing.adapter.eventstore; 2 | 3 | import static java.util.Collections.emptyList; 4 | import static java.util.stream.Collectors.toList; 5 | 6 | import bankservice.domain.model.Event; 7 | import bankservice.domain.model.EventStore; 8 | import bankservice.domain.model.OptimisticLockingException; 9 | import com.google.common.collect.ImmutableList; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.stream.Stream; 15 | 16 | public class InMemoryEventStore implements EventStore { 17 | 18 | private final Map> eventStore = new ConcurrentHashMap<>(); 19 | 20 | @Override 21 | public void store(UUID aggregateId, List newEvents, int baseVersion) 22 | throws OptimisticLockingException { 23 | eventStore.merge(aggregateId, newEvents, (oldValue, value) -> { 24 | if (baseVersion != oldValue.get(oldValue.size() - 1).getVersion()) { 25 | throw new OptimisticLockingException("Base version does not match current stored version"); 26 | } 27 | 28 | return Stream.concat(oldValue.stream(), value.stream()).collect(toList()); 29 | }); 30 | } 31 | 32 | @Override 33 | public List load(UUID aggregateId) { 34 | return ImmutableList.copyOf(eventStore.getOrDefault(aggregateId, emptyList())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/accounttransactions/AccountTransactionsResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.accounttransactions; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | 6 | import jakarta.ws.rs.GET; 7 | import jakarta.ws.rs.Path; 8 | import jakarta.ws.rs.PathParam; 9 | import jakarta.ws.rs.Produces; 10 | import jakarta.ws.rs.core.Response; 11 | import java.util.List; 12 | import java.util.UUID; 13 | 14 | @Produces(APPLICATION_JSON) 15 | @Path("/accounts/{id}/transactions") 16 | public class AccountTransactionsResource { 17 | 18 | private TransactionsRepository transactionsRepository; 19 | 20 | public AccountTransactionsResource(TransactionsRepository transactionsRepository) { 21 | this.transactionsRepository = checkNotNull(transactionsRepository); 22 | } 23 | 24 | @GET 25 | public Response get(@PathParam("id") UUID accountId) { 26 | List transactionProjections = transactionsRepository 27 | .listByAccount(accountId); 28 | return Response.ok(transactionProjections).build(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/accounttransactions/InMemoryTransactionsRepository.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.accounttransactions; 2 | 3 | import static com.google.common.collect.Lists.newArrayList; 4 | import static java.util.Collections.emptyList; 5 | import static java.util.Comparator.comparing; 6 | import static java.util.stream.Collectors.toList; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.UUID; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.stream.Stream; 13 | 14 | public class InMemoryTransactionsRepository implements TransactionsRepository { 15 | 16 | private Map> accountTransactions = new ConcurrentHashMap<>(); 17 | 18 | @Override 19 | public List listByAccount(UUID accountId) { 20 | return accountTransactions.getOrDefault(accountId, emptyList()).stream() 21 | .sorted(comparing(TransactionProjection::getVersion)) 22 | .collect(toList()); 23 | } 24 | 25 | @Override 26 | public void save(TransactionProjection transactionProjection) { 27 | accountTransactions.merge( 28 | transactionProjection.getAccountId(), 29 | newArrayList(transactionProjection), 30 | (oldValue, value) -> Stream.concat(oldValue.stream(), value.stream()).collect(toList())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/accounttransactions/TransactionProjection.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.accounttransactions; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.ZonedDateTime; 7 | import java.util.UUID; 8 | 9 | public class TransactionProjection { 10 | 11 | private final UUID accountId; 12 | private final TransactionType type; 13 | private final BigDecimal amount; 14 | private final ZonedDateTime timestamp; 15 | private final int version; 16 | 17 | public TransactionProjection(UUID accountId, TransactionType type, BigDecimal amount, 18 | ZonedDateTime timestamp, int version) { 19 | this.accountId = checkNotNull(accountId); 20 | this.type = checkNotNull(type); 21 | this.amount = checkNotNull(amount).setScale(2, BigDecimal.ROUND_HALF_UP); 22 | this.timestamp = checkNotNull(timestamp); 23 | this.version = version; 24 | } 25 | 26 | public UUID getAccountId() { 27 | return accountId; 28 | } 29 | 30 | public TransactionType getType() { 31 | return type; 32 | } 33 | 34 | public BigDecimal getAmount() { 35 | return amount; 36 | } 37 | 38 | public ZonedDateTime getTimestamp() { 39 | return timestamp; 40 | } 41 | 42 | public int getVersion() { 43 | return version; 44 | } 45 | 46 | public enum TransactionType { 47 | DEPOSIT, WITHDRAWAL 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/accounttransactions/TransactionsListener.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.accounttransactions; 2 | 3 | import static bankservice.projection.accounttransactions.TransactionProjection.TransactionType.DEPOSIT; 4 | import static bankservice.projection.accounttransactions.TransactionProjection.TransactionType.WITHDRAWAL; 5 | import static com.google.common.base.Preconditions.checkNotNull; 6 | 7 | import bankservice.domain.model.account.AccountDepositedEvent; 8 | import bankservice.domain.model.account.AccountWithdrawnEvent; 9 | import com.google.common.eventbus.Subscribe; 10 | 11 | public class TransactionsListener { 12 | 13 | private TransactionsRepository transactionsRepository; 14 | 15 | public TransactionsListener(TransactionsRepository transactionsRepository) { 16 | this.transactionsRepository = checkNotNull(transactionsRepository); 17 | } 18 | 19 | @Subscribe 20 | @SuppressWarnings("unused") 21 | public void handle(AccountDepositedEvent event) { 22 | TransactionProjection tx = new TransactionProjection( 23 | event.getAggregateId(), DEPOSIT, event.getAmount(), event.getTimestamp(), 24 | event.getVersion()); 25 | transactionsRepository.save(tx); 26 | } 27 | 28 | @Subscribe 29 | @SuppressWarnings("unused") 30 | public void handle(AccountWithdrawnEvent event) { 31 | TransactionProjection tx = new TransactionProjection( 32 | event.getAggregateId(), WITHDRAWAL, event.getAmount(), event.getTimestamp(), 33 | event.getVersion()); 34 | transactionsRepository.save(tx); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/accounttransactions/TransactionsRepository.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.accounttransactions; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | public interface TransactionsRepository { 7 | 8 | void save(TransactionProjection transactionProjection); 9 | 10 | List listByAccount(UUID accountId); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/clientaccounts/AccountProjection.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.clientaccounts; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.UUID; 7 | 8 | public class AccountProjection { 9 | 10 | private final UUID accountId; 11 | private final UUID clientId; 12 | private final BigDecimal balance; 13 | private final int version; 14 | 15 | public AccountProjection(UUID accountId, UUID clientId, BigDecimal balance, int version) { 16 | this.accountId = checkNotNull(accountId); 17 | this.clientId = checkNotNull(clientId); 18 | this.balance = checkNotNull(balance); 19 | this.version = version; 20 | } 21 | 22 | public UUID getAccountId() { 23 | return accountId; 24 | } 25 | 26 | public UUID getClientId() { 27 | return clientId; 28 | } 29 | 30 | public BigDecimal getBalance() { 31 | return balance; 32 | } 33 | 34 | public int getVersion() { 35 | return version; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/clientaccounts/AccountsListener.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.clientaccounts; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.account.AccountDepositedEvent; 6 | import bankservice.domain.model.account.AccountOpenedEvent; 7 | import bankservice.domain.model.account.AccountWithdrawnEvent; 8 | import com.google.common.eventbus.Subscribe; 9 | 10 | public class AccountsListener { 11 | 12 | private final AccountsRepository accountsRepository; 13 | 14 | public AccountsListener(AccountsRepository accountsRepository) { 15 | this.accountsRepository = checkNotNull(accountsRepository); 16 | } 17 | 18 | @Subscribe 19 | @SuppressWarnings("unused") 20 | public void handle(AccountOpenedEvent event) { 21 | AccountProjection accountProjection = new AccountProjection( 22 | event.getAggregateId(), event.getClientId(), event.getBalance(), event.getVersion()); 23 | accountsRepository.save(accountProjection); 24 | } 25 | 26 | @Subscribe 27 | @SuppressWarnings("unused") 28 | public void handle(AccountDepositedEvent event) { 29 | accountsRepository 30 | .updateBalance(event.getAggregateId(), event.getBalance(), event.getVersion()); 31 | } 32 | 33 | @Subscribe 34 | @SuppressWarnings("unused") 35 | public void handle(AccountWithdrawnEvent event) { 36 | accountsRepository 37 | .updateBalance(event.getAggregateId(), event.getBalance(), event.getVersion()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/clientaccounts/AccountsRepository.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.clientaccounts; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | public interface AccountsRepository { 8 | 9 | void save(AccountProjection accountProjection); 10 | 11 | void updateBalance(UUID accountId, BigDecimal balance, int version); 12 | 13 | List getAccounts(UUID clientId); 14 | } 15 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/clientaccounts/ClientAccountsResource.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.clientaccounts; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | 6 | import io.dropwizard.jersey.params.UUIDParam; 7 | import jakarta.ws.rs.Consumes; 8 | import jakarta.ws.rs.GET; 9 | import jakarta.ws.rs.Path; 10 | import jakarta.ws.rs.PathParam; 11 | import jakarta.ws.rs.Produces; 12 | import jakarta.ws.rs.core.Response; 13 | import java.util.List; 14 | 15 | @Consumes(APPLICATION_JSON) 16 | @Produces(APPLICATION_JSON) 17 | @Path("clients/{id}/accounts") 18 | public class ClientAccountsResource { 19 | 20 | private final AccountsRepository accountsRepository; 21 | 22 | public ClientAccountsResource(AccountsRepository accountsRepository) { 23 | this.accountsRepository = checkNotNull(accountsRepository); 24 | } 25 | 26 | @GET 27 | public Response get(@PathParam("id") UUIDParam clientId) { 28 | List accounts = accountsRepository.getAccounts(clientId.get()); 29 | return Response.ok(accounts).build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/projection/clientaccounts/InMemoryAccountsRepository.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.clientaccounts; 2 | 3 | import static java.util.Collections.emptyMap; 4 | 5 | import com.google.common.collect.ImmutableList; 6 | import java.math.BigDecimal; 7 | import java.util.AbstractMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.UUID; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | public class InMemoryAccountsRepository implements AccountsRepository { 16 | 17 | private final Map> clientAccounts = new ConcurrentHashMap<>(); 18 | private final Map accountClientIndex = new ConcurrentHashMap<>(); 19 | 20 | @Override 21 | public void save(AccountProjection accountProjection) { 22 | clientAccounts.merge( 23 | accountProjection.getClientId(), 24 | newAccountsMap(accountProjection), 25 | (oldValue, value) -> { 26 | oldValue.putAll(value); 27 | return oldValue; 28 | } 29 | ); 30 | accountClientIndex.put(accountProjection.getAccountId(), accountProjection.getClientId()); 31 | } 32 | 33 | @Override 34 | public void updateBalance(UUID accountId, BigDecimal balance, int version) { 35 | UUID clientId = accountClientIndex.get(accountId); 36 | clientAccounts.get(clientId).merge( 37 | accountId, 38 | new AccountProjection(accountId, clientId, balance, version), 39 | (oldValue, value) -> value.getVersion() > oldValue.getVersion() ? value : oldValue); 40 | } 41 | 42 | @Override 43 | public List getAccounts(UUID clientId) { 44 | Map accounts = clientAccounts.getOrDefault(clientId, emptyMap()); 45 | return ImmutableList.copyOf(accounts.values()); 46 | } 47 | 48 | private Map newAccountsMap(AccountProjection accountProjection) { 49 | return Stream.of( 50 | new AbstractMap.SimpleEntry<>(accountProjection.getAccountId(), accountProjection)) 51 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/Retrier.java: -------------------------------------------------------------------------------- 1 | package bankservice.service; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import java.util.List; 6 | import java.util.function.Supplier; 7 | 8 | public class Retrier { 9 | 10 | private final List> retriableExceptions; 11 | private final int maxAttempts; 12 | 13 | public Retrier(List> retriableExceptions, int maxAttempts) { 14 | checkArgument(!retriableExceptions.isEmpty()); 15 | checkArgument(maxAttempts > 1); 16 | this.retriableExceptions = retriableExceptions; 17 | this.maxAttempts = maxAttempts; 18 | } 19 | 20 | public T get(Supplier supplier) { 21 | for (int attempt = 1; ; attempt++) { 22 | try { 23 | return supplier.get(); 24 | } catch (Exception exception) { 25 | if (!isRetriable(exception) || attempt == maxAttempts) { 26 | throw exception; 27 | } 28 | } 29 | } 30 | } 31 | 32 | private boolean isRetriable(Exception exception) { 33 | return retriableExceptions.stream().anyMatch(e -> e.isAssignableFrom(exception.getClass())); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/account/AccountNotFoundException.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.account; 2 | 3 | import static java.lang.String.format; 4 | 5 | import java.util.UUID; 6 | 7 | public class AccountNotFoundException extends RuntimeException { 8 | 9 | public AccountNotFoundException(UUID id) { 10 | super(format("Account with id '%s' could not be found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/account/AccountService.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static java.util.Collections.singletonList; 5 | import static java.util.UUID.randomUUID; 6 | 7 | import bankservice.domain.model.Event; 8 | import bankservice.domain.model.EventStore; 9 | import bankservice.domain.model.OptimisticLockingException; 10 | import bankservice.domain.model.account.Account; 11 | import bankservice.domain.model.account.NonSufficientFundsException; 12 | import bankservice.service.Retrier; 13 | import com.google.common.eventbus.EventBus; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.UUID; 17 | import java.util.function.Consumer; 18 | 19 | public class AccountService { 20 | 21 | private final EventStore eventStore; 22 | private final EventBus eventBus; 23 | private final Retrier conflictRetrier; 24 | 25 | public AccountService(EventStore eventStore, EventBus eventBus) { 26 | this.eventStore = checkNotNull(eventStore); 27 | this.eventBus = checkNotNull(eventBus); 28 | int maxAttempts = 3; 29 | this.conflictRetrier = new Retrier( 30 | singletonList(OptimisticLockingException.class), 31 | maxAttempts); 32 | } 33 | 34 | public Optional loadAccount(UUID id) { 35 | List eventStream = eventStore.load(id); 36 | if (eventStream.isEmpty()) { 37 | return Optional.empty(); 38 | } 39 | return Optional.of(new Account(id, eventStream)); 40 | } 41 | 42 | public Account process(OpenAccountCommand command) { 43 | Account account = new Account(randomUUID(), command.getClientId()); 44 | storeAndPublishEvents(account); 45 | return account; 46 | } 47 | 48 | public Account process(DepositAccountCommand command) 49 | throws AccountNotFoundException, OptimisticLockingException { 50 | return process(command.getId(), (account) -> account.deposit(command.getAmount())); 51 | } 52 | 53 | public Account process(WithdrawAccountCommand command) 54 | throws AccountNotFoundException, OptimisticLockingException, NonSufficientFundsException { 55 | return process(command.getId(), (account) -> account.withdraw(command.getAmount())); 56 | } 57 | 58 | private Account process(UUID accountId, Consumer consumer) 59 | throws AccountNotFoundException, OptimisticLockingException { 60 | 61 | return conflictRetrier.get(() -> { 62 | Optional possibleAccount = loadAccount(accountId); 63 | Account account = possibleAccount.orElseThrow(() -> new AccountNotFoundException(accountId)); 64 | consumer.accept(account); 65 | storeAndPublishEvents(account); 66 | return account; 67 | }); 68 | } 69 | 70 | private void storeAndPublishEvents(Account account) throws OptimisticLockingException { 71 | eventStore.store(account.getId(), account.getNewEvents(), account.getBaseVersion()); 72 | account.getNewEvents().forEach(eventBus::post); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/account/DepositAccountCommand.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.UUID; 7 | 8 | public class DepositAccountCommand { 9 | 10 | private final UUID id; 11 | private final BigDecimal amount; 12 | 13 | public DepositAccountCommand(UUID id, BigDecimal amount) { 14 | this.id = checkNotNull(id); 15 | this.amount = checkNotNull(amount); 16 | } 17 | 18 | public UUID getId() { 19 | return id; 20 | } 21 | 22 | public BigDecimal getAmount() { 23 | return amount; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/account/OpenAccountCommand.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.util.UUID; 6 | 7 | public class OpenAccountCommand { 8 | 9 | private final UUID clientId; 10 | 11 | public OpenAccountCommand(UUID clientId) { 12 | this.clientId = checkNotNull(clientId); 13 | } 14 | 15 | public UUID getClientId() { 16 | return clientId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/account/WithdrawAccountCommand.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.account; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.UUID; 7 | 8 | public class WithdrawAccountCommand { 9 | 10 | private final UUID id; 11 | private final BigDecimal amount; 12 | 13 | public WithdrawAccountCommand(UUID id, BigDecimal amount) { 14 | this.id = checkNotNull(id); 15 | this.amount = checkNotNull(amount); 16 | } 17 | 18 | public UUID getId() { 19 | return id; 20 | } 21 | 22 | public BigDecimal getAmount() { 23 | return amount; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/client/ClientNotFoundException.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.client; 2 | 3 | import static java.lang.String.format; 4 | 5 | import java.util.UUID; 6 | 7 | public class ClientNotFoundException extends RuntimeException { 8 | 9 | public ClientNotFoundException(UUID id) { 10 | super(format("Client with id '%s' could not be found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/client/ClientService.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.client; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static java.util.Collections.singletonList; 5 | import static java.util.UUID.randomUUID; 6 | 7 | import bankservice.domain.model.Event; 8 | import bankservice.domain.model.EventStore; 9 | import bankservice.domain.model.OptimisticLockingException; 10 | import bankservice.domain.model.client.Client; 11 | import bankservice.service.Retrier; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.UUID; 15 | import java.util.function.Consumer; 16 | 17 | public class ClientService { 18 | 19 | private final EventStore eventStore; 20 | private final Retrier conflictRetrier; 21 | 22 | public ClientService(EventStore eventStore) { 23 | this.eventStore = checkNotNull(eventStore); 24 | int maxAttempts = 3; 25 | this.conflictRetrier = new Retrier(singletonList(OptimisticLockingException.class), 26 | maxAttempts); 27 | } 28 | 29 | public Optional loadClient(UUID id) { 30 | List eventStream = eventStore.load(id); 31 | if (eventStream.isEmpty()) { 32 | return Optional.empty(); 33 | } 34 | return Optional.of(new Client(id, eventStream)); 35 | } 36 | 37 | public Client process(EnrollClientCommand command) { 38 | Client client = new Client(randomUUID(), command.getName(), command.getEmail()); 39 | storeEvents(client); 40 | return client; 41 | } 42 | 43 | public void process(UpdateClientCommand command) { 44 | process(command.getId(), c -> c.update(command.getName(), command.getEmail())); 45 | } 46 | 47 | private Client process(UUID clientId, Consumer consumer) 48 | throws ClientNotFoundException, OptimisticLockingException { 49 | 50 | return conflictRetrier.get(() -> { 51 | Optional possibleClient = loadClient(clientId); 52 | Client client = possibleClient.orElseThrow(() -> new ClientNotFoundException(clientId)); 53 | consumer.accept(client); 54 | storeEvents(client); 55 | return client; 56 | }); 57 | } 58 | 59 | private void storeEvents(Client client) { 60 | eventStore.store(client.getId(), client.getNewEvents(), client.getBaseVersion()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/client/EnrollClientCommand.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.client; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import bankservice.domain.model.client.Email; 6 | 7 | public class EnrollClientCommand { 8 | 9 | private final String name; 10 | private final Email email; 11 | 12 | public EnrollClientCommand(String name, Email email) { 13 | this.name = checkNotNull(name); 14 | this.email = checkNotNull(email); 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public Email getEmail() { 22 | return email; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/java/bankservice/service/client/UpdateClientCommand.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.client; 2 | 3 | 4 | import static com.google.common.base.Preconditions.checkNotNull; 5 | 6 | import bankservice.domain.model.client.Email; 7 | import java.util.UUID; 8 | 9 | public class UpdateClientCommand { 10 | 11 | private final UUID id; 12 | private final String name; 13 | private final Email email; 14 | 15 | public UpdateClientCommand(UUID id, String name, Email email) { 16 | this.id = checkNotNull(id); 17 | this.name = checkNotNull(name); 18 | this.email = checkNotNull(email); 19 | } 20 | 21 | public UUID getId() { 22 | return id; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public Email getEmail() { 30 | return email; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/main/resources/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/domain/model/account/AccountTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.account; 2 | 3 | import static java.math.BigDecimal.TEN; 4 | import static java.math.BigDecimal.ZERO; 5 | import static java.util.UUID.randomUUID; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.equalTo; 8 | import static org.hamcrest.Matchers.instanceOf; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | import bankservice.domain.model.Event; 12 | import java.math.BigDecimal; 13 | import java.util.List; 14 | import java.util.UUID; 15 | import org.junit.jupiter.api.Test; 16 | 17 | class AccountTest { 18 | 19 | @Test 20 | void newAccountHasNewAccountOpenedEvent() { 21 | UUID id = randomUUID(); 22 | UUID clientId = randomUUID(); 23 | Account account = new Account(id, clientId); 24 | List newEvents = account.getNewEvents(); 25 | 26 | assertThat(newEvents.size(), equalTo(1)); 27 | assertThat(newEvents.get(0), instanceOf(AccountOpenedEvent.class)); 28 | AccountOpenedEvent event = (AccountOpenedEvent) newEvents.get(0); 29 | assertThat(event.getAggregateId(), equalTo(id)); 30 | assertThat(event.getClientId(), equalTo(clientId)); 31 | assertThat(event.getBalance(), equalTo(ZERO)); 32 | 33 | assertThat(account.getId(), equalTo(id)); 34 | assertThat(account.getClientId(), equalTo(clientId)); 35 | assertThat(account.getBalance(), equalTo(ZERO)); 36 | 37 | } 38 | 39 | @Test 40 | void depositedAccountHasAccountDepositedEvent() { 41 | Account account = new Account(randomUUID(), randomUUID()); 42 | BigDecimal amount = BigDecimal.valueOf(100.50); 43 | 44 | account.deposit(amount); 45 | 46 | List newEvents = account.getNewEvents(); 47 | assertThat(newEvents.size(), equalTo(2)); 48 | assertThat(newEvents.get(1), instanceOf(AccountDepositedEvent.class)); 49 | AccountDepositedEvent event = (AccountDepositedEvent) newEvents.get(1); 50 | assertThat(event.getBalance(), equalTo(amount)); 51 | 52 | assertThat(account.getBalance(), equalTo(amount)); 53 | } 54 | 55 | @Test 56 | void withdrawnAccountHasAccountWithdrawnEvent() { 57 | Account account = new Account(randomUUID(), randomUUID()); 58 | account.deposit(BigDecimal.valueOf(30)); 59 | 60 | account.withdraw(BigDecimal.valueOf(20)); 61 | 62 | List newEvents = account.getNewEvents(); 63 | assertThat(newEvents.size(), equalTo(3)); 64 | assertThat(newEvents.get(2), instanceOf(AccountWithdrawnEvent.class)); 65 | AccountWithdrawnEvent event = (AccountWithdrawnEvent) newEvents.get(2); 66 | assertThat(event.getBalance(), equalTo(TEN)); 67 | 68 | assertThat(account.getBalance(), equalTo(TEN)); 69 | } 70 | 71 | @Test 72 | void failsWithdrawalWithNonSufficientFunds() { 73 | Account account = new Account(randomUUID(), randomUUID()); 74 | assertThrows( 75 | NonSufficientFundsException.class, 76 | () -> account.withdraw(BigDecimal.valueOf(1))); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/domain/model/client/ClientTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.client; 2 | 3 | import static java.util.UUID.randomUUID; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.equalTo; 6 | import static org.hamcrest.Matchers.instanceOf; 7 | 8 | import bankservice.domain.model.Event; 9 | import java.util.List; 10 | import java.util.UUID; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class ClientTest { 14 | 15 | @Test 16 | void newClientHasBeenEnrolled() { 17 | UUID id = randomUUID(); 18 | String name = "john"; 19 | Email email = new Email("john@example.com"); 20 | 21 | Client client = new Client(id, name, email); 22 | 23 | List newEvents = client.getNewEvents(); 24 | assertThat(newEvents.size(), equalTo(1)); 25 | assertThat(newEvents.get(0), instanceOf(ClientEnrolledEvent.class)); 26 | ClientEnrolledEvent event = (ClientEnrolledEvent) newEvents.get(0); 27 | assertThat(event.getAggregateId(), equalTo(id)); 28 | assertThat(event.getName(), equalTo(name)); 29 | assertThat(event.getEmail(), equalTo(email)); 30 | 31 | assertThat(client.getId(), equalTo(id)); 32 | assertThat(client.getName(), equalTo(name)); 33 | assertThat(client.getEmail(), equalTo(email)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/domain/model/client/EmailSpecificationTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.domain.model.client; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.equalTo; 5 | 6 | import java.util.stream.Stream; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.Arguments; 9 | import org.junit.jupiter.params.provider.MethodSource; 10 | 11 | class EmailSpecificationTest { 12 | 13 | private final Email.EmailSpecification specification = new Email.EmailSpecification(); 14 | 15 | @SuppressWarnings("unused") 16 | private static Stream emails() { 17 | return Stream.of( 18 | Arguments.of("email@example.com", true), 19 | Arguments.of("firstname.lastname@example.com", true), 20 | Arguments.of("email@subdomain.example.com", true), 21 | Arguments.of("firstname+lastname@example.com", true), 22 | Arguments.of("emailexample", false), 23 | Arguments.of("email@example", false), 24 | Arguments.of("email@-example.com", false), 25 | Arguments.of("email@example.web", false), 26 | Arguments.of("email@111.222.333.4444", false), 27 | Arguments.of("email@example..com", false), 28 | Arguments.of("abc..123@example.com", false) 29 | ); 30 | } 31 | 32 | @MethodSource("emails") 33 | @ParameterizedTest 34 | void validate(String value, boolean isValid) { 35 | assertThat(specification.isSatisfiedBy(value), equalTo(isValid)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/AccountIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static java.util.UUID.randomUUID; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.equalTo; 6 | 7 | import com.fasterxml.jackson.databind.JsonNode; 8 | import jakarta.ws.rs.core.Response; 9 | import java.util.UUID; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class AccountIT extends BaseIT { 13 | 14 | @Test 15 | void returnAccount() { 16 | String clientId = UUID.randomUUID().toString(); 17 | String accountId = stateSetup.newAccount(clientId); 18 | Response response = resourcesClient.getAccount(accountId); 19 | JsonNode account = response.readEntity(JsonNode.class); 20 | assertThat(account.get("id").asText(), equalTo(accountId)); 21 | assertThat(account.get("clientId").asText(), equalTo(clientId.toString())); 22 | assertThat(account.get("balance").asDouble(), equalTo(0.0)); 23 | assertThat(response.getStatus(), equalTo(200)); 24 | } 25 | 26 | @Test 27 | void returnAccountNotFound() { 28 | Response response = resourcesClient.getAccount(randomUUID().toString()); 29 | response.close(); 30 | assertThat(response.getStatus(), equalTo(404)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/AccountTransactionsIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static java.math.BigDecimal.ONE; 4 | import static java.math.BigDecimal.TEN; 5 | import static java.util.UUID.randomUUID; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.equalTo; 8 | import static org.hamcrest.Matchers.notNullValue; 9 | 10 | import com.fasterxml.jackson.databind.JsonNode; 11 | import com.fasterxml.jackson.databind.node.ArrayNode; 12 | import jakarta.ws.rs.core.Response; 13 | import java.math.BigDecimal; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class AccountTransactionsIT extends BaseIT { 17 | 18 | @Test 19 | void returnEmptyTransactions() { 20 | Response response = resourcesClient.getAccountTransactions(randomUUID().toString()); 21 | ArrayNode transactions = response.readEntity(ArrayNode.class); 22 | assertThat(transactions.size(), equalTo(0)); 23 | assertThat(response.getStatus(), equalTo(200)); 24 | } 25 | 26 | @Test 27 | void returnTransactions() { 28 | String accountId = stateSetup.newAccount(randomUUID().toString()); 29 | resourcesClient.postDeposit(accountId, resourcesDtos.depositDto(BigDecimal.valueOf(99))) 30 | .close(); 31 | resourcesClient.postDeposit(accountId, resourcesDtos.depositDto(ONE)).close(); 32 | resourcesClient.postWithdrawal(accountId, resourcesDtos.withdrawalDto(TEN)).close(); 33 | 34 | Response response = resourcesClient.getAccountTransactions(accountId); 35 | ArrayNode transactions = response.readEntity(ArrayNode.class); 36 | assertThat(transactions.size(), equalTo(3)); 37 | verifyTransaction(transactions.get(0), "DEPOSIT", 99.0); 38 | verifyTransaction(transactions.get(1), "DEPOSIT", 1.0); 39 | verifyTransaction(transactions.get(2), "WITHDRAWAL", 10.0); 40 | assertThat(response.getStatus(), equalTo(200)); 41 | } 42 | 43 | private void verifyTransaction(JsonNode transaction, String type, double amount) { 44 | assertThat(transaction.get("type").asText(), equalTo(type)); 45 | assertThat(transaction.get("amount").asDouble(), equalTo(amount)); 46 | assertThat(transaction.get("timestamp"), notNullValue()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/AccountsIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.core.IsEqual.equalTo; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import jakarta.ws.rs.core.Response; 8 | import java.util.ArrayList; 9 | import org.glassfish.jersey.uri.UriTemplate; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class AccountsIT extends BaseIT { 13 | 14 | @Test 15 | void newAccount() { 16 | String clientId = stateSetup.newClient("John", "john@example.com"); 17 | Response response = resourcesClient.postAccount(resourcesDtos.accountDto(clientId)); 18 | response.close(); 19 | assertThat(response.getStatus(), equalTo(201)); 20 | UriTemplate accountUriTemplate = resourcesClient.getResourcesUrls().accountUriTemplate(); 21 | assertTrue(accountUriTemplate.match(response.getHeaderString("Location"), new ArrayList<>())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/BaseIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; 4 | 5 | import bankservice.bootstrap.BankServiceApplication; 6 | import bankservice.it.client.ResourcesClient; 7 | import bankservice.it.client.ResourcesDtos; 8 | import bankservice.it.setup.StateSetup; 9 | import io.dropwizard.core.Configuration; 10 | import io.dropwizard.testing.junit5.DropwizardAppExtension; 11 | import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | 15 | @ExtendWith(DropwizardExtensionsSupport.class) 16 | abstract class BaseIT { 17 | 18 | protected static final DropwizardAppExtension BANK_SERVICE = 19 | new DropwizardAppExtension<>(BankServiceApplication.class, 20 | resourceFilePath("integration.yml")); 21 | 22 | protected static ResourcesClient resourcesClient; 23 | protected static ResourcesDtos resourcesDtos; 24 | protected static StateSetup stateSetup; 25 | 26 | @BeforeAll 27 | public static void setUpBaseClass() { 28 | resourcesClient = new ResourcesClient(BANK_SERVICE.getEnvironment(), 29 | BANK_SERVICE.getLocalPort()); 30 | resourcesDtos = new ResourcesDtos(BANK_SERVICE.getObjectMapper()); 31 | stateSetup = new StateSetup(resourcesClient, resourcesDtos); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/ClientAccountsIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static java.math.BigDecimal.TEN; 4 | import static java.util.Collections.emptyMap; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.hamcrest.Matchers.equalTo; 7 | 8 | import com.fasterxml.jackson.databind.node.ArrayNode; 9 | import jakarta.ws.rs.core.Response; 10 | import java.math.BigDecimal; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.UUID; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class ClientAccountsIT extends BaseIT { 17 | 18 | @Test 19 | void returnClientAccounts() { 20 | String clientId = UUID.randomUUID().toString(); 21 | verifyClientAccounts(clientId, emptyMap()); 22 | 23 | String accountId1 = stateSetup.newAccountWithBalance(clientId, BigDecimal.valueOf(100)); 24 | String accountId2 = stateSetup.newAccountWithBalance(clientId, BigDecimal.valueOf(100)); 25 | String accountId3 = stateSetup.newAccountWithBalance(clientId, BigDecimal.valueOf(100)); 26 | resourcesClient.postWithdrawal(accountId2, resourcesDtos.withdrawalDto(TEN)).close(); 27 | resourcesClient.postDeposit(accountId3, resourcesDtos.depositDto(BigDecimal.valueOf(0.01))) 28 | .close(); 29 | 30 | Map expectedAccountsBalances = new HashMap<>(); 31 | expectedAccountsBalances.put(accountId1, BigDecimal.valueOf(100.0)); 32 | expectedAccountsBalances.put(accountId2, BigDecimal.valueOf(90.0)); 33 | expectedAccountsBalances.put(accountId3, BigDecimal.valueOf(100.01)); 34 | verifyClientAccounts(clientId, expectedAccountsBalances); 35 | } 36 | 37 | private void verifyClientAccounts(String clientId, 38 | Map expectedAccountsBalances) { 39 | Response response = resourcesClient.getClientAccounts(clientId); 40 | ArrayNode accounts = response.readEntity(ArrayNode.class); 41 | 42 | accounts.forEach(account -> { 43 | String accountId = account.get("accountId").asText(); 44 | BigDecimal balance = new BigDecimal(account.get("balance").asText()); 45 | assertThat(balance, equalTo(expectedAccountsBalances.get(accountId))); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/ClientIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static java.util.UUID.randomUUID; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.core.IsEqual.equalTo; 6 | 7 | import com.fasterxml.jackson.databind.JsonNode; 8 | import com.fasterxml.jackson.databind.node.ObjectNode; 9 | import jakarta.ws.rs.core.Response; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ClientIT extends BaseIT { 13 | 14 | @Test 15 | void returnClient() { 16 | String name = "Jensen"; 17 | String email = "jensen@example.com"; 18 | String clientId = stateSetup.newClient(name, email); 19 | Response response = resourcesClient.getClient(clientId); 20 | JsonNode client = response.readEntity(JsonNode.class); 21 | assertThat(client.get("id").textValue(), equalTo(clientId)); 22 | assertThat(client.get("name").textValue(), equalTo(name)); 23 | assertThat(client.get("email").textValue(), equalTo(email)); 24 | } 25 | 26 | @Test 27 | void returnClientNotFound() { 28 | Response response = resourcesClient.getClient(randomUUID().toString()); 29 | response.close(); 30 | assertThat(response.getStatus(), equalTo(404)); 31 | } 32 | 33 | @Test 34 | void updateClient() { 35 | String clientId = stateSetup.newClient("Jaya", "jaya@example.com"); 36 | String newName = "Jayan"; 37 | String newEmail = "jayan@example.com"; 38 | { 39 | ObjectNode clientDto = resourcesDtos.clientDto(newName, newEmail); 40 | Response response = resourcesClient.putClient(clientId, clientDto); 41 | response.close(); 42 | assertThat(response.getStatus(), equalTo(204)); 43 | } 44 | { 45 | Response response = resourcesClient.getClient(clientId); 46 | JsonNode clientDto = response.readEntity(JsonNode.class); 47 | assertThat(clientDto.get("name").textValue(), equalTo(newName)); 48 | assertThat(clientDto.get("email").textValue(), equalTo(newEmail)); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/ClientsIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.core.IsEqual.equalTo; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import jakarta.ws.rs.core.Response; 9 | import java.util.ArrayList; 10 | import org.glassfish.jersey.uri.UriTemplate; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class ClientsIT extends BaseIT { 14 | 15 | @Test 16 | void newClient() { 17 | ObjectNode clientDto = resourcesDtos.clientDto("John", "john@example.com"); 18 | Response response = resourcesClient.postClient(clientDto); 19 | response.close(); 20 | assertThat(response.getStatus(), equalTo(201)); 21 | UriTemplate clientUriTemplate = resourcesClient.getResourcesUrls().clientUriTemplate(); 22 | assertTrue(clientUriTemplate.match(response.getHeaderString("Location"), new ArrayList<>())); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/DepositsIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static java.math.BigDecimal.TEN; 4 | import static java.util.UUID.randomUUID; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.hamcrest.Matchers.equalTo; 7 | 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import com.fasterxml.jackson.databind.node.ObjectNode; 10 | import jakarta.ws.rs.core.Response; 11 | import java.math.BigDecimal; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class DepositsIT extends BaseIT { 15 | 16 | @Test 17 | void returnAccountNotFound() { 18 | ObjectNode deposit = resourcesDtos.depositDto(TEN); 19 | Response response = resourcesClient.postDeposit(randomUUID().toString(), deposit); 20 | response.close(); 21 | assertThat(response.getStatus(), equalTo(404)); 22 | } 23 | 24 | @Test 25 | void depositAccount() { 26 | String accountId = stateSetup.newAccount(randomUUID().toString()); 27 | BigDecimal amount = TEN; 28 | { 29 | ObjectNode deposit = resourcesDtos.depositDto(amount); 30 | Response response = resourcesClient.postDeposit(accountId, deposit); 31 | response.close(); 32 | assertThat(response.getStatus(), equalTo(204)); 33 | } 34 | { 35 | Response response = resourcesClient.getAccount(accountId); 36 | JsonNode account = response.readEntity(JsonNode.class); 37 | assertThat(account.get("balance").asDouble(), equalTo(amount.doubleValue())); 38 | assertThat(response.getStatus(), equalTo(200)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/ExceptionMappersIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import bankservice.port.incoming.adapter.resources.OptimisticLockingExceptionMapper; 7 | import bankservice.port.incoming.adapter.resources.accounts.AccountNotFoundExceptionMapper; 8 | import java.util.Set; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ExceptionMappersIT extends BaseIT { 12 | 13 | @Test 14 | void registeredExceptionMappers() { 15 | Set> classes = BANK_SERVICE.getEnvironment().jersey().getResourceConfig().getClasses(); 16 | assertTrue(classes.contains(OptimisticLockingExceptionMapper.class)); 17 | assertTrue(classes.contains(AccountNotFoundExceptionMapper.class)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/HealthCheckIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static jakarta.ws.rs.core.UriBuilder.fromUri; 4 | import static java.util.logging.Level.INFO; 5 | import static java.util.logging.Logger.getLogger; 6 | import static org.glassfish.jersey.logging.LoggingFeature.DEFAULT_LOGGER_NAME; 7 | import static org.glassfish.jersey.logging.LoggingFeature.Verbosity.PAYLOAD_ANY; 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.core.IsEqual.equalTo; 10 | 11 | import io.dropwizard.client.JerseyClientBuilder; 12 | import jakarta.ws.rs.client.Client; 13 | import jakarta.ws.rs.core.Response; 14 | import java.net.URI; 15 | import org.glassfish.jersey.logging.LoggingFeature; 16 | import org.junit.jupiter.api.BeforeAll; 17 | import org.junit.jupiter.api.Test; 18 | 19 | class HealthCheckIT extends BaseIT { 20 | 21 | private static Client client; 22 | 23 | @BeforeAll 24 | static void setUpClass() { 25 | client = new JerseyClientBuilder(BANK_SERVICE.getEnvironment()) 26 | .build(HealthCheckIT.class.getName()) 27 | .register(new LoggingFeature(getLogger(DEFAULT_LOGGER_NAME), INFO, PAYLOAD_ANY, 1024)); 28 | } 29 | 30 | @Test 31 | void overallHealth() { 32 | Response response = client.target(healthCheckUrl()).request().get(); 33 | response.close(); 34 | assertThat(response.getStatus(), equalTo(200)); 35 | } 36 | 37 | private URI healthCheckUrl() { 38 | return fromUri("http://localhost").port(BANK_SERVICE.getAdminPort()).path("healthcheck") 39 | .build(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/WithdrawalsIT.java: -------------------------------------------------------------------------------- 1 | package bankservice.it; 2 | 3 | import static java.math.BigDecimal.TEN; 4 | import static java.util.UUID.randomUUID; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.hamcrest.Matchers.equalTo; 7 | 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import com.fasterxml.jackson.databind.node.ObjectNode; 10 | import jakarta.ws.rs.core.Response; 11 | import java.math.BigDecimal; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class WithdrawalsIT extends BaseIT { 15 | 16 | @Test 17 | void returnAccountNotFound() { 18 | ObjectNode deposit = resourcesDtos.withdrawalDto(TEN); 19 | Response response = resourcesClient.postWithdrawal(randomUUID().toString(), deposit); 20 | response.close(); 21 | assertThat(response.getStatus(), equalTo(404)); 22 | } 23 | 24 | @Test 25 | void withdrawAccount() { 26 | BigDecimal previousBalance = BigDecimal.valueOf(9.99); 27 | BigDecimal withdrawalAmount = BigDecimal.valueOf(5.55); 28 | BigDecimal expectedBalance = BigDecimal.valueOf(4.44); 29 | String accountId = stateSetup.newAccountWithBalance(randomUUID().toString(), previousBalance); 30 | { 31 | ObjectNode withdrawal = resourcesDtos.withdrawalDto(withdrawalAmount); 32 | Response response = resourcesClient.postWithdrawal(accountId, withdrawal); 33 | response.close(); 34 | assertThat(response.getStatus(), equalTo(204)); 35 | } 36 | { 37 | Response response = resourcesClient.getAccount(accountId); 38 | JsonNode account = response.readEntity(JsonNode.class); 39 | assertThat(account.get("balance").asDouble(), equalTo(expectedBalance.doubleValue())); 40 | assertThat(response.getStatus(), equalTo(200)); 41 | } 42 | } 43 | 44 | @Test 45 | void returnBadRequestForNonSufficientFunds() { 46 | String accountId = stateSetup.newAccount(randomUUID().toString()); 47 | ObjectNode withdrawal = resourcesDtos.withdrawalDto(BigDecimal.valueOf(1000)); 48 | Response response = resourcesClient.postWithdrawal(accountId, withdrawal); 49 | response.close(); 50 | assertThat(response.getStatus(), equalTo(400)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/client/ResourcesClient.java: -------------------------------------------------------------------------------- 1 | package bankservice.it.client; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static jakarta.ws.rs.client.Entity.json; 5 | import static java.util.logging.Level.INFO; 6 | import static java.util.logging.Logger.getLogger; 7 | import static org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT; 8 | import static org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT; 9 | import static org.glassfish.jersey.logging.LoggingFeature.DEFAULT_LOGGER_NAME; 10 | import static org.glassfish.jersey.logging.LoggingFeature.Verbosity.PAYLOAD_ANY; 11 | 12 | import com.fasterxml.jackson.databind.JsonNode; 13 | import io.dropwizard.client.JerseyClientBuilder; 14 | import io.dropwizard.core.setup.Environment; 15 | import jakarta.ws.rs.client.Client; 16 | import jakarta.ws.rs.core.Response; 17 | import org.glassfish.jersey.logging.LoggingFeature; 18 | 19 | public class ResourcesClient { 20 | 21 | private final Client client; 22 | private final ResourcesUrls resourcesUrls; 23 | 24 | public ResourcesClient(Environment environment, int port) { 25 | this.client = new JerseyClientBuilder(checkNotNull(environment)) 26 | .build(ResourcesClient.class.getName()) 27 | .property(CONNECT_TIMEOUT, 2000) 28 | .property(READ_TIMEOUT, 3000) 29 | .register(new LoggingFeature(getLogger(DEFAULT_LOGGER_NAME), INFO, PAYLOAD_ANY, 1024)); 30 | this.resourcesUrls = new ResourcesUrls(port); 31 | } 32 | 33 | public ResourcesUrls getResourcesUrls() { 34 | return resourcesUrls; 35 | } 36 | 37 | public Response postClient(JsonNode clientDto) { 38 | return client.target(resourcesUrls.clientsUrl()).request().post(json(clientDto)); 39 | } 40 | 41 | public Response getClient(String clientId) { 42 | return client.target(resourcesUrls.clientUrl(clientId)).request().get(); 43 | } 44 | 45 | public Response putClient(String clientId, JsonNode clientDto) { 46 | return client.target(resourcesUrls.clientUrl(clientId)).request().put(json(clientDto)); 47 | } 48 | 49 | public Response postAccount(JsonNode accountDto) { 50 | return client.target(resourcesUrls.accountsUrl()).request().post(json(accountDto)); 51 | } 52 | 53 | public Response getAccount(String accountId) { 54 | return client.target(resourcesUrls.accountUrl(accountId)).request().get(); 55 | } 56 | 57 | public Response postDeposit(String accountId, JsonNode depositDto) { 58 | return client.target(resourcesUrls.depositsUrl(accountId)).request().post(json(depositDto)); 59 | } 60 | 61 | public Response postWithdrawal(String accountId, JsonNode withdrawalDto) { 62 | return client.target(resourcesUrls.withdrawalsUrl(accountId)).request() 63 | .post(json(withdrawalDto)); 64 | } 65 | 66 | public Response getClientAccounts(String clientId) { 67 | return client.target(resourcesUrls.clientAccountsUrl(clientId)).request().get(); 68 | } 69 | 70 | public Response getAccountTransactions(String accountId) { 71 | return client.target(resourcesUrls.accountTransactionsUrl(accountId)).request().get(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/client/ResourcesDtos.java: -------------------------------------------------------------------------------- 1 | package bankservice.it.client; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import java.math.BigDecimal; 9 | 10 | public class ResourcesDtos { 11 | 12 | private final ObjectMapper objectMapper; 13 | 14 | public ResourcesDtos(ObjectMapper objectMapper) { 15 | this.objectMapper = checkNotNull(objectMapper); 16 | } 17 | 18 | public ObjectNode clientDto(String name, String email) { 19 | ObjectNode newClientDto = objectMapper.createObjectNode(); 20 | return newClientDto.put("name", name).put("email", email); 21 | } 22 | 23 | public JsonNode accountDto(String clientId) { 24 | ObjectNode newAccountDto = objectMapper.createObjectNode(); 25 | return newAccountDto.put("clientId", clientId); 26 | } 27 | 28 | public ObjectNode depositDto(BigDecimal amount) { 29 | ObjectNode newAccountDto = objectMapper.createObjectNode(); 30 | return newAccountDto.put("amount", amount.doubleValue()); 31 | } 32 | 33 | public ObjectNode withdrawalDto(BigDecimal amount) { 34 | ObjectNode newAccountDto = objectMapper.createObjectNode(); 35 | return newAccountDto.put("amount", amount.doubleValue()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/client/ResourcesUrls.java: -------------------------------------------------------------------------------- 1 | package bankservice.it.client; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | import static jakarta.ws.rs.core.UriBuilder.fromUri; 5 | import static java.lang.String.format; 6 | 7 | import java.net.URI; 8 | import org.glassfish.jersey.uri.UriTemplate; 9 | 10 | public class ResourcesUrls { 11 | 12 | private int port; 13 | 14 | public ResourcesUrls(int port) { 15 | checkArgument(port > 0); 16 | this.port = port; 17 | } 18 | 19 | public URI clientsUrl() { 20 | return fromUri("http://localhost").port(port).path("clients").build(); 21 | } 22 | 23 | public URI clientUrl(String clientId) { 24 | return fromUri(clientUriTemplate().createURI(clientId)).build(); 25 | } 26 | 27 | public UriTemplate clientUriTemplate() { 28 | return new UriTemplate(format("http://localhost:%d/clients/{id}", port)); 29 | } 30 | 31 | public URI accountsUrl() { 32 | return fromUri("http://localhost").port(port).path("accounts").build(); 33 | } 34 | 35 | public URI accountUrl(String accountId) { 36 | return fromUri(accountUriTemplate().createURI(accountId)).build(); 37 | } 38 | 39 | public UriTemplate accountUriTemplate() { 40 | return new UriTemplate(format("http://localhost:%d/accounts/{id}", port)); 41 | } 42 | 43 | public URI depositsUrl(String accountId) { 44 | return fromUri("http://localhost").port(port) 45 | .path("accounts").path("{id}").path("deposits").build(accountId); 46 | } 47 | 48 | public URI withdrawalsUrl(String accountId) { 49 | return fromUri("http://localhost").port(port) 50 | .path("accounts").path("{id}").path("withdrawals").build(accountId); 51 | } 52 | 53 | public URI clientAccountsUrl(String clientId) { 54 | return fromUri("http://localhost").port(port).path("clients").path("{id}").path("accounts") 55 | .build(clientId.toString()); 56 | } 57 | 58 | public URI accountTransactionsUrl(String accountId) { 59 | return fromUri("http://localhost").port(port) 60 | .path("accounts").path("{id}").path("transactions").build(accountId); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/it/setup/StateSetup.java: -------------------------------------------------------------------------------- 1 | package bankservice.it.setup; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.equalTo; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import bankservice.it.client.ResourcesClient; 9 | import bankservice.it.client.ResourcesDtos; 10 | import com.fasterxml.jackson.databind.node.ObjectNode; 11 | import jakarta.ws.rs.core.Response; 12 | import java.math.BigDecimal; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import org.glassfish.jersey.uri.UriTemplate; 16 | 17 | public class StateSetup { 18 | 19 | private final ResourcesClient resourcesClient; 20 | private final ResourcesDtos resourcesDtos; 21 | 22 | public StateSetup(ResourcesClient resourcesClient, ResourcesDtos resourcesDtos) { 23 | this.resourcesClient = checkNotNull(resourcesClient); 24 | this.resourcesDtos = checkNotNull(resourcesDtos); 25 | } 26 | 27 | public String newClient(String name, String email) { 28 | ObjectNode clientDto = resourcesDtos.clientDto(name, email); 29 | Response response = resourcesClient.postClient(clientDto); 30 | response.close(); 31 | assertThat(response.getStatus(), equalTo(201)); 32 | Map params = new HashMap<>(); 33 | UriTemplate clientUriTemplate = resourcesClient.getResourcesUrls().clientUriTemplate(); 34 | assertTrue(clientUriTemplate.match(response.getHeaderString("Location"), params)); 35 | return params.get("id"); 36 | } 37 | 38 | public String newAccount(String clientId) { 39 | Response response = resourcesClient.postAccount(resourcesDtos.accountDto(clientId)); 40 | response.close(); 41 | assertThat(response.getStatus(), equalTo(201)); 42 | Map params = new HashMap<>(); 43 | UriTemplate accountUriTemplate = resourcesClient.getResourcesUrls().accountUriTemplate(); 44 | assertTrue(accountUriTemplate.match(response.getHeaderString("Location"), params)); 45 | return params.get("id"); 46 | } 47 | 48 | public String newAccountWithBalance(String clientId, BigDecimal balance) { 49 | String accountId = newAccount(clientId); 50 | ObjectNode deposit = resourcesDtos.depositDto(balance); 51 | Response response = resourcesClient.postDeposit(accountId, deposit); 52 | response.close(); 53 | assertThat(response.getStatus(), equalTo(204)); 54 | return accountId; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/port/incoming/adapter/resources/OptimisticLockingExceptionMapperTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources; 2 | 3 | import static jakarta.ws.rs.client.Entity.json; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.hamcrest.Matchers.equalTo; 7 | 8 | import bankservice.domain.model.OptimisticLockingException; 9 | import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; 10 | import io.dropwizard.testing.junit5.ResourceExtension; 11 | import jakarta.ws.rs.Consumes; 12 | import jakarta.ws.rs.PUT; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.core.Response; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | 18 | @ExtendWith(DropwizardExtensionsSupport.class) 19 | class OptimisticLockingExceptionMapperTest { 20 | 21 | static final ResourceExtension RESOURCES = ResourceExtension.builder() 22 | .addProvider(OptimisticLockingExceptionMapper.class) 23 | .addResource(new ConcurrentlyModifiedResource()) 24 | .build(); 25 | 26 | @Test 27 | void returnConflict() { 28 | Response response = RESOURCES.client() 29 | .target("/concurrently-modified-resource") 30 | .request().put(json("{}")); 31 | response.close(); 32 | assertThat(response.getStatus(), equalTo(409)); 33 | } 34 | 35 | @Consumes(APPLICATION_JSON) 36 | @Path("/concurrently-modified-resource") 37 | public static class ConcurrentlyModifiedResource { 38 | 39 | @PUT 40 | public Response put(String entity) throws OptimisticLockingException { 41 | throw new OptimisticLockingException("Testing exception mapper"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/port/incoming/adapter/resources/accounts/AccountNotFoundExceptionMapperTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.incoming.adapter.resources.accounts; 2 | 3 | import static bankservice.port.incoming.adapter.resources.accounts.AccountNotFoundExceptionMapperTest.NeverFoundAccountResource.calls; 4 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; 5 | import static java.lang.String.format; 6 | import static java.util.UUID.randomUUID; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.equalTo; 9 | 10 | import bankservice.service.account.AccountNotFoundException; 11 | import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; 12 | import io.dropwizard.testing.junit5.ResourceExtension; 13 | import jakarta.ws.rs.GET; 14 | import jakarta.ws.rs.Path; 15 | import jakarta.ws.rs.PathParam; 16 | import jakarta.ws.rs.Produces; 17 | import jakarta.ws.rs.core.Response; 18 | import java.util.UUID; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | 22 | @ExtendWith(DropwizardExtensionsSupport.class) 23 | class AccountNotFoundExceptionMapperTest { 24 | 25 | static final ResourceExtension RESOURCES = ResourceExtension.builder() 26 | .addProvider(AccountNotFoundExceptionMapper.class) 27 | .addResource(new NeverFoundAccountResource()) 28 | .build(); 29 | 30 | @Test 31 | void returnNotFound() { 32 | Response response = RESOURCES.client() 33 | .target(format("/never-found-account-resource/%s", randomUUID())) 34 | .request().get(); 35 | response.close(); 36 | assertThat(response.getStatus(), equalTo(404)); 37 | assertThat(calls, equalTo(1)); 38 | } 39 | 40 | @Produces(APPLICATION_JSON) 41 | @Path("/never-found-account-resource/{id}") 42 | public static class NeverFoundAccountResource { 43 | 44 | static int calls = 0; 45 | 46 | @GET 47 | public Response get(@PathParam("id") String id) throws AccountNotFoundException { 48 | calls++; 49 | throw new AccountNotFoundException(UUID.fromString(id)); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/port/outgoing/adapter/eventstore/InMemoryEventStoreTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.port.outgoing.adapter.eventstore; 2 | 3 | import static com.google.common.collect.Lists.newArrayList; 4 | import static java.time.ZoneOffset.UTC; 5 | import static java.time.ZonedDateTime.now; 6 | import static java.util.UUID.randomUUID; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.equalTo; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | import static org.mockito.Mockito.mock; 11 | 12 | import bankservice.domain.model.Event; 13 | import bankservice.domain.model.EventStore; 14 | import bankservice.domain.model.OptimisticLockingException; 15 | import java.util.List; 16 | import java.util.UUID; 17 | import org.junit.jupiter.api.Test; 18 | 19 | class InMemoryEventStoreTest { 20 | 21 | private EventStore eventStore = new InMemoryEventStore(); 22 | 23 | @Test 24 | void storeEventsInOrder() { 25 | UUID aggregateId = randomUUID(); 26 | Event e1 = new Event(aggregateId, now(UTC), 1) { 27 | }; 28 | Event e2 = new Event(aggregateId, now(UTC), 2) { 29 | }; 30 | Event e3 = new Event(aggregateId, now(UTC), 3) { 31 | }; 32 | eventStore.store(aggregateId, newArrayList(e1), 0); 33 | eventStore.store(aggregateId, newArrayList(e2), 1); 34 | eventStore.store(aggregateId, newArrayList(e3), 2); 35 | 36 | List eventStream = eventStore.load(aggregateId); 37 | assertThat(eventStream.size(), equalTo(3)); 38 | assertThat(eventStream.get(0), equalTo(e1)); 39 | assertThat(eventStream.get(1), equalTo(e2)); 40 | assertThat(eventStream.get(2), equalTo(e3)); 41 | } 42 | 43 | @Test 44 | void optimisticLocking() { 45 | UUID aggregateId = randomUUID(); 46 | Event e1 = new Event(aggregateId, now(UTC), 1) { 47 | }; 48 | Event e2 = new Event(aggregateId, now(UTC), 2) { 49 | }; 50 | Event e3 = new Event(aggregateId, now(UTC), 2) { 51 | }; 52 | eventStore.store(aggregateId, newArrayList(e1), 0); 53 | eventStore.store(aggregateId, newArrayList(e2), 1); 54 | assertThrows( 55 | OptimisticLockingException.class, 56 | () -> eventStore.store(aggregateId, newArrayList(e3), 1) 57 | ); 58 | } 59 | 60 | @Test 61 | void loadedEventStreamIsImmutable() { 62 | UUID aggregateId = randomUUID(); 63 | Event e1 = new Event(aggregateId, now(UTC), 1) { 64 | }; 65 | eventStore.store(aggregateId, newArrayList(e1), 0); 66 | assertThrows( 67 | UnsupportedOperationException.class, 68 | () -> eventStore.load(aggregateId).add(mock(Event.class)) 69 | ); 70 | } 71 | } -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/projection/accounttransactions/InMemoryTransactionsRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.accounttransactions; 2 | 3 | import static bankservice.projection.accounttransactions.TransactionProjection.TransactionType.DEPOSIT; 4 | import static java.math.BigDecimal.ONE; 5 | import static java.math.BigDecimal.TEN; 6 | import static java.time.ZoneOffset.UTC; 7 | import static java.time.ZonedDateTime.now; 8 | import static java.util.UUID.randomUUID; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.equalTo; 11 | 12 | import java.util.List; 13 | import java.util.UUID; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class InMemoryTransactionsRepositoryTest { 17 | 18 | private TransactionsRepository transactionsRepository = 19 | new InMemoryTransactionsRepository(); 20 | 21 | @Test 22 | void listEventsSortedByVersion() { 23 | UUID accountId = randomUUID(); 24 | TransactionProjection tx2 = new TransactionProjection(accountId, DEPOSIT, TEN, now(UTC), 2); 25 | TransactionProjection tx1 = new TransactionProjection(accountId, DEPOSIT, ONE, now(UTC), 1); 26 | transactionsRepository.save(tx2); 27 | transactionsRepository.save(tx1); 28 | 29 | List transactions = transactionsRepository.listByAccount(accountId); 30 | assertThat(transactions.get(0), equalTo(tx1)); 31 | assertThat(transactions.get(1), equalTo(tx2)); 32 | } 33 | } -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/projection/clientaccounts/InMemoryAccountsRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.projection.clientaccounts; 2 | 3 | import static java.math.BigDecimal.ONE; 4 | import static java.math.BigDecimal.TEN; 5 | import static java.math.BigDecimal.ZERO; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.equalTo; 8 | 9 | import java.util.UUID; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class InMemoryAccountsRepositoryTest { 13 | 14 | private AccountsRepository accountsRepository = 15 | new InMemoryAccountsRepository(); 16 | 17 | @Test 18 | void ignoreEventOutOfOrder() { 19 | UUID clientId = UUID.randomUUID(); 20 | UUID accountId = UUID.randomUUID(); 21 | accountsRepository.save(new AccountProjection(accountId, clientId, ZERO, 1)); 22 | accountsRepository.updateBalance(accountId, TEN, 3); 23 | accountsRepository.updateBalance(accountId, ONE, 2); 24 | assertThat(accountsRepository.getAccounts(clientId).get(0).getBalance(), equalTo(TEN)); 25 | } 26 | } -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/service/RetrierTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.service; 2 | 3 | import static java.util.Collections.singletonList; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.instanceOf; 6 | import static org.junit.jupiter.api.Assertions.fail; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.times; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.when; 11 | 12 | import java.util.function.Supplier; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class RetrierTest { 16 | 17 | private RuntimeException exceptionToRetryOn = new IllegalStateException(); 18 | private int maxAttempts = 10; 19 | private Retrier retrier = new Retrier(singletonList(exceptionToRetryOn.getClass()), maxAttempts); 20 | 21 | @Test 22 | void retriesGetUpToMaxAttemptsWithoutSharedStateBetweenCalls() { 23 | retryGetUpToMaxAttempts(); 24 | retryGetUpToMaxAttempts(); 25 | } 26 | 27 | private void retryGetUpToMaxAttempts() { 28 | Supplier supplier = mock(Supplier.class); 29 | when(supplier.get()).thenThrow(exceptionToRetryOn); 30 | try { 31 | retrier.get(() -> supplier.get()); 32 | fail("Should have thrown exception after max attempts"); 33 | } catch (RuntimeException e) { 34 | verify(supplier, times(maxAttempts)).get(); 35 | assertThat(e, instanceOf(exceptionToRetryOn.getClass())); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/java/bankservice/service/account/AccountServiceTest.java: -------------------------------------------------------------------------------- 1 | package bankservice.service.account; 2 | 3 | import static java.math.BigDecimal.ONE; 4 | import static java.math.BigDecimal.TEN; 5 | import static java.util.UUID.randomUUID; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.equalTo; 8 | import static org.mockito.ArgumentMatchers.anyList; 9 | import static org.mockito.ArgumentMatchers.eq; 10 | import static org.mockito.Mockito.anyInt; 11 | import static org.mockito.Mockito.doThrow; 12 | import static org.mockito.Mockito.spy; 13 | import static org.mockito.Mockito.times; 14 | import static org.mockito.Mockito.verify; 15 | 16 | import bankservice.domain.model.EventStore; 17 | import bankservice.domain.model.OptimisticLockingException; 18 | import bankservice.domain.model.account.Account; 19 | import bankservice.domain.model.account.AccountDepositedEvent; 20 | import bankservice.domain.model.account.AccountOpenedEvent; 21 | import bankservice.domain.model.account.AccountWithdrawnEvent; 22 | import bankservice.port.outgoing.adapter.eventstore.InMemoryEventStore; 23 | import com.google.common.eventbus.EventBus; 24 | import com.google.common.eventbus.Subscribe; 25 | import java.util.Map; 26 | import java.util.UUID; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Test; 30 | 31 | class AccountServiceTest { 32 | 33 | private EventStore eventStore; 34 | private EventBusCounter eventBusCounter; 35 | private AccountService accountService; 36 | 37 | @BeforeEach 38 | void setUp() { 39 | eventStore = spy(new InMemoryEventStore()); 40 | EventBus eventBus = new EventBus(); 41 | eventBusCounter = new EventBusCounter(); 42 | eventBus.register(eventBusCounter); 43 | accountService = new AccountService(eventStore, eventBus); 44 | } 45 | 46 | @Test 47 | void retryOnAccountWithdrawalConflictsUpToThreeAttempts() { 48 | Account account = accountService.process(new OpenAccountCommand(randomUUID())); 49 | UUID id = account.getId(); 50 | accountService.process(new DepositAccountCommand(id, TEN)); 51 | doThrow(OptimisticLockingException.class) 52 | .doThrow(OptimisticLockingException.class) 53 | .doCallRealMethod() 54 | .when(eventStore).store(eq(id), anyList(), anyInt()); 55 | 56 | accountService.process(new WithdrawAccountCommand(id, ONE)); 57 | int creationAttempts = 1; 58 | int depositAttempts = 1; 59 | int withdrawalAttempts = 3; 60 | int loadTimes = depositAttempts + withdrawalAttempts; 61 | int storeTimes = creationAttempts + depositAttempts + withdrawalAttempts; 62 | verify(eventStore, times(loadTimes)).load(eq(id)); 63 | verify(eventStore, times(storeTimes)).store(eq(id), anyList(), anyInt()); 64 | assertThat(eventBusCounter.eventsCount.get(AccountOpenedEvent.class), equalTo(1)); 65 | assertThat(eventBusCounter.eventsCount.get(AccountDepositedEvent.class), equalTo(1)); 66 | assertThat(eventBusCounter.eventsCount.get(AccountWithdrawnEvent.class), equalTo(1)); 67 | assertThat(eventBusCounter.eventsCount.size(), equalTo(3)); 68 | } 69 | 70 | private static class EventBusCounter { 71 | 72 | Map, Integer> eventsCount = new ConcurrentHashMap<>(); 73 | 74 | @Subscribe 75 | @SuppressWarnings("unused") 76 | public void handle(Object event) { 77 | eventsCount.merge(event.getClass(), 1, Integer::sum); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/2020~Event Sourcing and CQRS Examples/codes/src/test/resources/integration.yml: -------------------------------------------------------------------------------- 1 | server: 2 | applicationConnectors: 3 | - type: http 4 | port: 0 5 | adminConnectors: 6 | - type: http 7 | port: 0 -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/99~参考资料/22~“消息驱动、事件驱动、流 ”基础概念解析.md: -------------------------------------------------------------------------------- 1 | # “消息驱动、事件驱动、流 ”基础概念解析 2 | 3 | 简介: 本文旨在帮助大家对近期消息领域的高频词“消息驱动(Message-Driven),事件驱动(Event-Driven)和流(Streaming)”有更清晰的了解和认知,其中事件驱动 EDA 作为 Gartner 预测的十大技术趋势之一,EventBridge 作为下一代消息中间件,也是目前的重点方向之一。 4 | 5 | # 背景 6 | 7 | 首先这三个概念具体翻译如下: 8 | 9 | - **Message-Driven:**消息驱动的通信; 10 | - **Event- Driven:**事件驱动的通信; 11 | - **Streaming:**流模式。 12 | 13 | 这三个模式都是类似异步通信的模式,发送消息的服务不会等待消费消息服务响应任何数据,做服务解耦是三个模式共同的特性; 14 | 15 | 只要是在服务通讯领域内,在选型时还要考虑如下特性: 16 | 17 | - **排序:**是否可以保证特定的顺序交付; 18 | - **事务:**生产者或消费者是否可以参与分布式事务; 19 | - **持久化:**数据如何被持久化,以及是否可以重放数据; 20 | - **订阅过滤:**是否拥有根据 Tag 或其他字段做订阅过滤的能力; 21 | - At – least -once(最少交付一次),At-most-once(最多交付一次),Exactly-once (精确交付)。 22 | 23 | 通用背景介绍完,依次来看看各个模型代表的是什么意思。 24 | 25 | # 消息驱动 Message-Driven 26 | 27 | 在消息驱动通信中,一般链路就是消息生产者(Producer)向消息消费者(Consumer)发送消息。模型如下: 28 | 29 | ![Producer to Consumer](https://assets.ng-tech.icu/item/20230409213718.png) 30 | 31 | 消息驱动模式下通常会用到中间件,比较常见的中间组件有 RocketMQ,Kafka,RabbitMQ 等。这些中间件的目的是缓存生产者投递的消息直到消费者准备接收这些消息,以此将两端系统解耦。在消息驱动架构中,消息的格式是基于消费者的需求制定的;消息传递可以是一对一,多对多,一对多或多对一。 32 | 33 | 消息驱动通讯比较常见的一个例子是商品订单推送,上游组件负责生成订单,下游组件负责接收订单并处理。通过这样的通讯方式上游生成组件其实无需关心整个订单的生命周期,更专注于如何快速生成订单,使单个组件的性能得以提升。 34 | 35 | ![串行业务的异步解耦](https://assets.ng-tech.icu/item/20230409213754.png) 36 | 37 | 消息驱动模式在服务之间提供了轻的耦合(这部分耦合指代 Producer/Consumer SDK),并可以对生产和消费服务根据诉求进行扩展。 38 | 39 | # 事件驱动 Event-Driven 40 | 41 | 首先要申明一个观点:事件驱动其实是对消息驱动方法的改进,它对消息体大小,消息格式做了较为严格的限制,这层基于消息的限制封装其实就称为事件(Event)。在事件驱动模式中,生产者发布事件来表示系统变更,任何感兴趣且有权限接入的服务都可以订阅这些事件,并将这些事件作为触发器来启动某些逻辑/存储/任务。 42 | 43 | ![事件驱动](https://assets.ng-tech.icu/item/20230409213851.png) 44 | 45 | 事件驱动的模式可以是一对一,多对一,一对多或多对多。通常情况下一般是多个目标根据过滤条件执行不同的事件。 46 | 47 | 在事件驱动架构中,事件的格式是由生产者根据事件标准协议制定的;由于更规范限制和封装,事件的生产者完全不需要关心有哪些系统正在消费它生成的事件。 48 | 49 | 事件不是命令,事件不会告诉消费者如何处理信息,他们的作用只是告诉消费者此时此刻有个事件发生了;事件是一份不可变的数据,重要的数据,它与消息的数据价值相同;通常情况下当某个事件发生并执行时,往往伴随着另一个事件的产生。 50 | 51 | 事件驱动提供了服务间的最小耦合,并允许生产服务和消费服务根据需求进行扩展;事件驱动可以在不影响现有服务的情况下添加各类新增组件。 52 | 53 | 事件驱动也可以举一个非常贴切的例子,我们以“客户购买完一款商品”为一个事件,举证在事件场景的应用: 54 | 55 | - CRM(客户关系系统)系统接收到客户购买信息,可自行更新客户的购买记录; 56 | - EMR(库存管理系统) 系统接收到客户购买信息,动态调整库存并及时补货; 57 | - 快递服务接收到客户购买信息,自行打单并通知快递公司派送。 58 | 59 | 这么看,事件驱动模式是不是可以应用并出现在任何地方! 60 | 61 | 在 EventBridge 产品化方向,也正是由于针对消息做了一些标准化封装,才有可能实现譬如针对事件本身的 filter(过滤) ,transform(转换),schema(事件结构),search(查询) 等能力。这些能力也拓展出更多针对事件驱动特有的场景功能及相关特性。 62 | 63 | # 流 Streaming 64 | 65 | 流是一组有序的无界事件或数据,执行操作通常是固定的某个事件段(e.g. 00:00 – 12:00)或一个相对事件(E.g. 过去 12 小时)。 66 | 67 | ![流模式](https://assets.ng-tech.icu/item/20230409214504.png) 68 | 69 | 通常情况下单个事件往往就是使用事件本身,但是对于流可能的操作大概率是过滤,组合,拆分,映射等等。 70 | 71 | 流的操作可以是无状态也可以是有状态的: 72 | 73 | - 对于单个事件操作是无状态的,包括过滤和映射; 74 | - 依赖消息在流的时间或位置(e.g. offset,time)是有状态的。有状态操作中,流处理逻辑必须保留一些已被消费消息的内存。有状态包括对数据做 Batch Size,Batch Window 等。 75 | 76 | 流这里也可以举一个比较简单的例子,比如我们的物流系统在物品通过一个物流节点时会生成一个事件,但是要查到这个物品完整的流转状态事件,则必须是各个物流节点单个事件的聚合,那这个聚合事件就是流事件。 77 | 78 | Kafka 是最典型的流式中间件,在流式场景中,事件的位置信息至关重要。通常情况下位置信息(E.g. offset)是由消费者托管的。 79 | 80 | # 事件规范标准 81 | 82 | 聊完 Event 和 Streaming 是什么,再来补充一点有关于它们的规范。 83 | 84 | 事件规范存在的目的是为了清晰事件生产者和消费者的关系,目前主要有两部分:AsyncAPI 和 CloudEvents; 85 | 86 | **AsyncAPI:**基于事件 API 提供了与之对应的 Open API 和 Swagger 等;**CloudEvents:**侧重于处理事件的元数据。 87 | 88 | 下面也重点介绍一些关于 CloudEvents 的相关概念参考:CloudEvents 的核心其实是定义了一组关于不同组件间传输事件的元数据,以及这些元数据应该如何出现在消息体中。 89 | 90 | 其主旨大抵如下: 91 | 92 | - 事件规范化; 93 | - 降低平台集成难度; 94 | - 提高 FaaS 的可移植性; 95 | - 源事件可追踪; 96 | - 提升事件关联性 97 | 98 | 准确的事件体,事件信息才可以做出更稳定的系统架构,永远保持对事件的敬畏。 99 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/EventSourcing.md: -------------------------------------------------------------------------------- 1 | # Event Sourcing 2 | 3 | 查询方面,直接通过方法查询数据库,然后通过 DTO 将数据返回。在操作(Command)方面,是通过发送 Command 实现,由 CommandBus 处理特定的 Command,然后由 Command 将特定的 Event 发布到 EventBus 上,然后 EventBus 使用特定的 Handler 来处理事件,执行一些诸如,修改,删除,更新等操作。这里,所有与 Command 相关的操作都通过 Event 实现。这样我们可以通过记录 Event 来记录系统的运行历史记录,并且能够方便的回滚到某一历史状态。Event Sourcing 就是用来进行存储和管理事件的。 4 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/README.md: -------------------------------------------------------------------------------- 1 | # Event Driven Architecture Pattern 2 | 3 | 事件代表过去发生的事件,事件既是技术架构概念,也是业务概念,以事件为驱动的编程模型称为事件驱动架构 EDA。EDA 架构的三个特性:异步、实时、彻底解耦。EDA 架构的核心是基于消息的发布订阅模式,通过发布订阅模式实现事件的一对多灵活分发。消息消费方对发送方而言完全透明,消息发送方只管把消息发送到消息中间件,其它事情全部不用关心,由于消息中间件中的 MQ 等技术,即使发送消息时候,消息接收方不可用,但仍然可以正常发送,这才叫彻底解耦。其次一对多的发布订阅模式也是一个核心重点,对于消息的订阅方和订阅机制,可以在消息中间件灵活的进行配置和管理,而对于消息发送方和发送逻辑基本没有任何影响。 4 | 5 | EDA 要求我们的是通过业务流程,首先要识别出有价值的业务事件,这些事件符合异步、实时和发布订阅等基本的事件特征;其次是对事件进行详细的分析和定义,对事件对应的消息格式进行定义,对事件的发布订阅机制进行定义等,最后才是基于消息事件模式的开发和测试等工作。 6 | 7 | # EDA 成熟度模型 8 | 9 | 我们通过 Gartner 报告总结的 EDA 成熟度模型,展望以下 EDA 架构的未来: 10 | 11 | - Incidental:偶发性地使用事件通知机制来进行一些状态的捕获,没有明确的事件处理策略; 12 | - Brokered:提供托管的事件代理服务,组织中部分应用开始采用基于消息或者事件的异步化架构; 13 | - Centralized:以战略的形式提出中心化的 EDA 解决方案,有专门的组织团队提供 EDA 实现,EDA 架构开始广泛被采用; 14 | - Advanced:EDA 架构开始触达更多的业务领域,比如流计算,数据分析,AI,以及 API 市场等,跨组织的事件生态开始形成并进行扩张; 15 | - Pervasive:事件变得无处不在,庞大的事件生态形成,组织间的隔离被事件彻底打通,企业的关键业务都将采取 EDA 架构;事件驱动与请求驱动两个生态完成融合。 16 | 17 | ![EDA 成熟度模型](https://s1.ax1x.com/2020/09/13/w0qKHK.png) 18 | 19 | # Links 20 | 21 | - https://zhuanlan.zhihu.com/p/79095599 22 | - 提取最后的参考资料 http://www.cnblogs.com/xishuai/p/iddd-cqrs-and-eda.html#xishuai_h1 23 | -------------------------------------------------------------------------------- /02~架构风格与模式/EDA 事件驱动架构/事件溯源.md: -------------------------------------------------------------------------------- 1 | # 事件溯源 2 | 3 | 事件溯源。而对于领域对象来说,我们也应该知晓其整个生命周期的演变过程,这样有利于查看并还原某一“时刻”的领域对象,在 EDA 架构中,对于领域对象状态的保存是通过领域事件进行完成,所以我们要想记录领域对象的状态记录,就需要把领域对象所经历的所有事件进行保存下来,这就是 Event Store(事件存储),这个东西就相当于 Git 代码服务器,存储所有领域对象所经历的事件,对于某一事件来说,可以看作是对应领域对象的“快照”。 4 | -------------------------------------------------------------------------------- /02~架构风格与模式/Polylith/README.md: -------------------------------------------------------------------------------- 1 | # Polylith 2 | 3 | [Polylith](https://polylith.gitbook.io/polylith) 4 | -------------------------------------------------------------------------------- /02~架构风格与模式/REST 架构风格/99~参考资料/2022~Writing API Design Standards.md: -------------------------------------------------------------------------------- 1 | # Writing API Design Standards 2 | 3 | # Links 4 | 5 | - https://medium.com/@trgoodwill/writing-api-design-standards-84cb7cbb3fd7 6 | -------------------------------------------------------------------------------- /02~架构风格与模式/REST 架构风格/API 生成/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/02~架构风格与模式/REST 架构风格/API 生成/README.md -------------------------------------------------------------------------------- /02~架构风格与模式/REST 架构风格/OpenAPI/README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI 2 | 3 | OpenAPI 规范(以前称为 Swagger 规范)是 REST API 的 API 描述格式。OpenAPI 文件允许您描述整个 API,包括:每个端点上的可用端点(/users)和操作(GET /users,POST /users)、操作参数每个操作的输入和输出、认证方法、联系信息,许可证,使用条款和其他信息、API 规范可以用 YAML 或 JSON 编写。 4 | 5 | ```yaml 6 | openapi: 3.0.0 7 | 8 | info: 9 | title: A simplified version of fromAtoB’s backend API 10 | version: "1.0" 11 | 12 | paths: 13 | /lookup: 14 | get: 15 | description: Look up the coordinates for a city by name. 16 | parameters: 17 | - in: query 18 | name: name 19 | schema: 20 | type: string 21 | description: City name. 22 | responses: 23 | "200": 24 | description: OK 25 | content: 26 | application/json: 27 | schema: 28 | $ref: "#/components/schemas/Coordinate" 29 | "404": 30 | description: Not Found 31 | content: 32 | text/plain: 33 | schema: 34 | type: string 35 | 36 | components: 37 | schemas: 38 | Coordinate: 39 | type: object 40 | description: A Coordinate identifies a location on Earth by latitude and longitude. 41 | properties: 42 | latitude: 43 | type: number 44 | description: Latitude is the degrees latitude of the location, in the range [-90, 90]. 45 | longitude: 46 | type: number 47 | description: Longitude is the degrees longitude of the location, in the range [-180, 180]. 48 | ``` 49 | 50 | # Links 51 | 52 | - https://www.breakyizhan.com/swagger/2806.html 53 | -------------------------------------------------------------------------------- /02~架构风格与模式/REST 架构风格/README.md: -------------------------------------------------------------------------------- 1 | # WebAPI 2 | 3 | 基于 HTTP 协议的 Web API 是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中,我们都见到了越来越多的 SOA 或 RESTful 的 Web API。为什么 Web API 如此流行呢?我认为很大程度上应归功于简单有效的 HTTP 协议。HTTP 协议是一种分布式的面向资源的网络应用层协议,无论是服务器端提供 Web 服务,还是客户端消费 Web 服务都非常简单。再加上浏览器、Javascript、AJAX、JSON 以及 HTML5 等技术和工具的发展,互联网应用架构设计表现出了从传统的 PHP、JSP、ASP.NET 等服务器端动态网页向 Web API + RIA(富互联网应用)过渡的趋势。 4 | 5 | Web API 专注于提供业务服务,RIA 专注于用户界面和交互设计,从此两个领域的分工更加明晰。在这种趋势下,Web API 设计将成为服务器端程序员的必修课。然而,正如简单的 Java 语言并不意味着高质量的 Java 程序,简单的 HTTP 协议也不意味着高质量的 Web API。要想设计出高质量的 Web API,还需要深入理解分布式系统及 HTTP 协议的特性。 6 | -------------------------------------------------------------------------------- /02~架构风格与模式/REST 架构风格/REST API 标准/README.md: -------------------------------------------------------------------------------- 1 | # REST 2 | 3 | Roy Fielding 提出了一种用于设计 Web 服务的架构方法,称为 Representational State Transfer(REST)。REST 的概念是将 API 结构分离为操作和资源。使用 HTTP 方法 GET、DELETE、POST 和 PUT 操作资源。 4 | 5 | REST,即 Representational State Transfer 的缩写,表现层状态转化。 6 | 7 | Resource(资源):对象的单个实例。例如,一只动物。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个 URI(统一资源定位符)指向它,每种资源对应一个特定的 URI。要获取这个资源,访问它的 URI 就可以,因此 URI 就成了每一个资源的地址或独一无二的识别符。 8 | 9 | "资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。状态转化(State Transfer) 10 | 访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。互联网通信协议 HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。 11 | 12 | 客户端用到的手段,只能是 HTTP 协议。具体来说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。 13 | 比如,文本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚至可以采用二进制格式;图片可以用 JPG 格式表现,也可以用 PNG 格式表现。 14 | URI 只代表资源的实体,不代表它的形式。严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而 URI 应该只代表"资源"的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对"表现层"的描述。 15 | 16 | # 接口规范 17 | 18 | ## Action | 动作 19 | 20 | GET(SELECT):从服务器检索特定资源,或资源列表。 21 | POST(CREATE):在服务器上创建一个新的资源。 22 | PUT(UPDATE):更新服务器上的资源,提供整个资源。 23 | PATCH(UPDATE):更新服务器上的资源,仅提供更改的属性。 24 | DELETE(DELETE):从服务器删除资源。 25 | 首先是四个半种动作: 26 | post、delete、put/patch、get 27 | 因为 put/patch 只能算作一类,所以将 patch 归为半个。 28 | 另外还有有两个较少知名的 HTTP 动词: 29 | HEAD - 检索有关资源的元数据,例如数据的哈希或上次更新时间。 30 | OPTIONS - 检索关于客户端被允许对资源做什么的信息。 31 | 32 | ## 接口命名 33 | 34 | 在 RESTful 架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以 API 中的名词也应该使用复数。 35 | 36 | 举例来说,有一个 API 提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。 37 | 接口尽量使用名词,禁止使用动词,下面是一些例子。 38 | 39 | ``` 40 | GET /zoos:列出所有动物园 41 | POST /zoo:新建一个动物园 42 | GET /zoos/ID:获取某个指定动物园的信息 43 | PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息) 44 | PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息) 45 | DELETE /zoos/ID:删除某个动物园 46 | GET /zoos/ID/animals:列出某个指定动物园的所有动物 47 | DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物 48 | ``` 49 | 50 | 对于版本控制,则应该将 API 的版本号放入 URL。如: 51 | 52 | ``` 53 | https://api.example.com/v1/ 54 | ``` 55 | 56 | 另一种做法是,将版本号放在 HTTP 头信息中,但不如放入 URL 方便和直观。Github 采用这种做法。 57 | 58 | 参考业界实现 59 | /menu/create 微信自定义菜单创建接口 60 | /menu/get 微信自定义菜单查询接口 61 | /menu/delete 微信自定义菜单删除接口 62 | comments/show 获取某条微博的评论列表 63 | comments/create 评论一条微博 64 | comments/destroy 删除一条我的评论 65 | comments/destroy_batch 批量删除我的评论 66 | 1.HTTP 动词有限,难以满足复杂业务需求,在 URL 中使用自定义动词可以更好的扩展 2.可以更好的从 URL 体现业务含义 67 | 3.REST 规范需要 URL+HTTP 动词,对某个资源的查询和更新的 URL 是相同的,而一般 URL 统计、流控只会针对 URL 4.统一基本的资源操作含义 68 | create 创建 69 | get 查询 70 | delete 删除 71 | update 更新 72 | 批量 xxx batch_xxx 73 | 74 | ## Filter & Search | 过滤与检索 75 | 76 | 如果记录数量很多,服务器不可能都将它们返回给用户。API 应该提供参数,过滤返回结果。 77 | 下面是一些常见的参数。 78 | 79 | ``` 80 | ?limit=10:指定返回记录的数量 81 | ?offset=10:指定返回记录的开始位置。 82 | ?page_number=2&page_size=100:指定第几页,以及每页的记录数。 83 | ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。 84 | ?animal_type_id=1:指定筛选条件 85 | 参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如, 86 | GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。 87 | ``` 88 | -------------------------------------------------------------------------------- /02~架构风格与模式/REST 架构风格/REST API 标准/RESTful 接口.md: -------------------------------------------------------------------------------- 1 | # RESTful 接口 2 | 3 | # HTTP Methods 4 | 5 | ## 幂等性 6 | 7 | # State Management | 状态管理 8 | 9 | 状态管理即维持一个页面状态或者控制的状态。HTTP 协议本身是一个无状态的协议,因此状态管理就是用来维持一系列的来自客户端的请求状态的机制。REST 服务本身提供了两个基础的状态类型: 10 | 11 | - Active : 当服务被调用或者执行的时候即进入 Active 状态 12 | - Passive : 当服务没有被使用是即进入 Passive 状态 13 | 14 | ## Active 15 | 16 | ## Stateful 17 | 18 | 这种状态下,服务处于激活状态,并且能够处理连续的任务。如果服务处于 Stateful 状态下,它可以处理三个类型的数据:Session、Context 与 Business。 19 | 20 | ### Session 21 | 22 | Session data represents information associated with retaining a connection made between a program and its client program.Sessions are identified by a unique identifier that can be read by using the SessionID property.A session is considered active as long as requests continue to be made with the same SessionID value. If the time between requests for a particular session exceeds the specified time-out value in minutes, the session is considered expired. Requests made with an expired SessionID value result in a new session. 23 | 24 | ### Context 25 | 26 | When the service is active,stateful and executing the main task of the service.If the logic is tied with workflow, then it is further divided to context data and context rules. Context rules are the rules for executing workflow. 27 | 28 | ### Business 29 | 30 | When the service is active and stateful,the service can execute a business task by executing multiple services. 31 | 32 | ## Stateless 33 | 34 | 这种状态下,服务处于激活状态,但是没有在处理一个连贯的任务。 35 | 36 | [1]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm 37 | [2]: http://www.restapitutorial.com/lessons/whatisrest.html# 38 | -------------------------------------------------------------------------------- /02~架构风格与模式/分层架构设计/README.md: -------------------------------------------------------------------------------- 1 | # 分层架构 2 | 3 | 分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。常见的分层结构如下图所示: 4 | 5 | ![](https://assets.ng-tech.icu/item/20230430220925.png) 6 | 7 | # 参考资料 8 | 9 | - Freewheel 首席工程师:我对软件分层设计的思考 https://mp.weixin.qq.com/s?__biz=MzIzNjUxMzk2NQ==&mid=2247512924&idx=2&sn=f7b56385388867190049b335cb89a380 10 | -------------------------------------------------------------------------------- /02~架构风格与模式/分层架构设计/层的划分.md: -------------------------------------------------------------------------------- 1 | # 领域建模中的分层架构 2 | 3 | 传统企业应用大多是单体架构,而单体架构则大多是三层架构。三层架构解决了程序内代码间调用复杂、代码职责不清的问题,但这种分层是逻辑概念,在物理上它是中心化的集中式架构,并不适合分布式微服务架构。DDD 分层架构中的要素其实和三层架构类似,只是在 DDD 分层架构中,这些要素被重新归类,重新划分了层,确定了层与层之间的交互规则和职责边界;DDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分,改善了三层架构核心业务逻辑混乱,代码改动相互影响大的情况。 4 | 5 | ![三层架构向四层架构演化](https://s3.ax1x.com/2021/02/05/y8qztK.png) 6 | 7 | 三层架构向 DDD 分层架构演进,主要发生在业务逻辑层和数据访问层。 8 | 9 | - DDD 分层架构在用户接口层引入了 DTO,给前端提供了更多的可使用数据和更高的展示灵活性。DDD 分层架构将业务逻辑层的服务拆分到了应用层和领域层。应用层快速响应前端的变化,领域层实现领域模型的能力。 10 | - 另外一个重要的变化发生在数据访问层和基础层之间。三层架构数据访问采用 DAO 方式;DDD 分层架构的数据库等基础资源访问,采用了仓储(Repository)设计模式,通过依赖倒置实现各层对基础资源的解耦。仓储又分为两部分:仓储接口和仓储实现。仓储接口放在领域层中,仓储实现放在基础层。原来三层架构通用的第三方工具包、驱动、Common、Utility、Config 等通用的公共的资源类统一放到了基础层。 11 | 12 | 在最早的传统 DDD 四层架构中,基础层是被其它层依赖的,它位于最核心的位置,那按照分层架构的思想,它应该就是核心,但实际上领域层才是软件的核心,所以这种依赖是有问题的。后来我们采用了依赖倒置(Dependency inversion principle,DIP)的设计,优化了传统的四层架构,实现了各层对基础层的解耦。 13 | 14 | ![DDD 内部分层演化](https://s3.ax1x.com/2021/02/05/y846tH.png) 15 | 16 | 在微服务架构模型比较常用的有几个,例如:整洁架构,CQRS(命令查询分离)以及六边形架构。每种架构模型都有自己的应用场景,但其核心都是“高内聚低耦合”原则。而运用领域驱动设计(DDD)理念以应对日常加速的业务变化对架构的影响,架构的边界越来越清晰,各司其职,这也符合微服务架构的设计思想。以领域驱动设计(DDD)为理念的分层架构已经成为微服务架构实践的最佳实践方法。 17 | 18 | - 展现层 19 | 20 | 展现层负责向用户显示信息和解释用户指令,常表示客户侧相关应用。 21 | 22 | - 用户接口层 23 | 24 | 用户接口层面向用户访问的数据入向接口,可按不同场景提供不一样的用户接口实现,这里的用户可能是:用户、程序、自动化测试和批处理脚本等等。面向 Web 的可使用 HTTP RESTful 的方式提供服务;面向微服务的可使用 RPC 方式提供服务,可增加限流、熔断等功能。 25 | 26 | - 应用层 27 | 28 | 应用层是尽量简单的很薄的一层,它不包含业务规则或知识,只为下一层的领域对象协调任务,协调和指挥领域对象来完成业务逻辑,是与其他系统的应用层进行交互的必要渠道。应用层位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。 29 | 30 | 此外,应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间的服务组合和编排。应用服务是在应用层的,它负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,以粗粒度的服务通过 API 网关向前端发布。还有,应用服务还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。需要注意的是,在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的微服务就会演化为传统的三层架构,业务逻辑会变得混乱。 31 | 32 | - 领域层 33 | 34 | 领域层是软件的核心所在,它实现全部业务逻辑并且通过各种校验手段保证业务正确性。它包含业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。它负责表达业务概念、业务状态以及业务规则,具体表现形式就是领域模型。需要注意的是,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域服务在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。 35 | 36 | - 基础层 37 | 38 | 基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。 39 | 40 | 比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多公司在架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后,应用层就可以通过解耦来保持独立的核心业务逻辑。当数据库变更时,我们只需要更换数据库基础服务就可以了,这样就将资源变更对应用的影响降到了最低。 41 | 42 | ![DDD 分层结构模型](https://s3.ax1x.com/2021/02/05/y84IHS.png) 43 | 44 | ## COLA 45 | 46 | COLA 的分层是一种改良了的三层架构。主要是将传统的业务逻辑层拆分成应用层、领域层和基础实施层。如下图所示,左边是传统的分层架构,右边是 COLA 的分层架构。 47 | 48 | ![COLA 架构模式](https://assets.ng-tech.icu/item/20230426100353.png) 49 | 50 | 其每一层的作用范围和含义如下: 51 | 52 | - 展现层(Presentation Layer):负责以 Rest 的格式接受 Web 请求,然后将请求路由给 Application 层执行,并返回视图模型(View Model),其载体通常是 DTO(Data Transfer Object); 53 | - 应用层(Application Layer):主要负责获取输入,组装上下文,做输入校验,调用领域层做业务处理,如果需要的话,发送消息通知。当然,层次是开放的,若有需要,应用层也可以直接访问基础实施层; 54 | - 领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Entities)的函数对外部提供业务逻辑的计算和处理; 55 | - 基础实施层(Infrastructure Layer)主要包含 Tunnel(数据通道)、Config 和 Common。这里我们使用 Tunnel 这个概念来对所有的数据来源进行抽象,这些数据来源可以是数据库(MySQL,NoSql)、搜索引擎、文件系统、也可以是 SOA 服务等;Config 负责应用的配置;Common 是通用的工具类。 56 | 57 | ## 六边形架构 58 | 59 | 六边形架构主打的是内外分离,体现业务逻辑的应用层与领域层处于六边形架构的内核,并通过内部的六边形边界与基础设施的模块隔离开。当我们在进行软件开发时,只要恪守架构上的六边形边界,则不会让技术实现的复杂度污染到业务逻辑,保证了领域的整洁。边界还隔离了变化产生的影响。如果我们在领域层或应用层抽象了技术实现的接口,再通过依赖注入将控制的方向倒转,业务内核就会变得更加的稳定,不会因为技术选型或其他决策的变化而导致领域代码的修改。 60 | 61 | ![六边形架构示意](https://s3.ax1x.com/2021/02/03/yKQ91A.md.png) 62 | 63 | # 领域服务 64 | 65 | 有些领域中的动作,它们是一些动词,看上去却不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,最佳实践是将它声明成一个服务。这样的对象不再拥有内置的状态。它的作用仅仅是为领域提供相应的功能。Service 往往是以一个活动来命名,而不是 Entity 来命名。 66 | 67 | 例如转账的例子,转账(transfer)这个行为是一个非常重要的领域概念,但是它是发生在两个账号之间的,归属于账号 Entity 并不合适,因为一个账号 Entity 没有必要去关联他需要转账的账号 Entity,这种情况下,使用 MoneyTransferDomainService 就比较合适了。识别领域服务,主要看它是否满足以下三个特征: 68 | 69 | - 服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。 70 | - 被执行的操作涉及到领域中的其他的对象。 71 | - 操作是无状态的。 72 | 73 | 在使用领域服务时要特别当心,一个比较常见的错误是没有努力为行为找到一个适当的对象,就直接抽象成领域服务,这会使我们的代码逐渐转化为过程式的编程,一个极端的例子是把所有的行为都放到领域服务中,而领域模型退化成只有属性的贫血 DO,那 DDD 就没有任何意义了。所以一定要深入思考,既不能勉强将行为放到不符合对象定义的对象中,破坏对象的内聚性,使其语义变得模糊。也不能不加思考的都放到领域服务中,从而退化成面向过程的编程。 74 | 75 | 决定一个服务(Service)应该归属于哪一层是很困难的。如果所执行的操作概念上属于应用层,那么服务就应该放到这个层。如果操作是关于领域对象的,而且确实是与领域有关的、为领域的需要服务,那么它就应该属于领域层。总的来说,涉及到重要领域概念的行为应该放在 Domain 层,而其它非领域逻辑的技术代码放在 App 层,例如参数的解析,上下文的组装,调用领域服务,消息发送等。还是银行转账的 case 为例,下图给出了划分的建议: 76 | 77 | # Links 78 | 79 | - https://mp.weixin.qq.com/s/BAK4AjupwNW05nPUHRPILw 80 | -------------------------------------------------------------------------------- /02~架构风格与模式/分层架构设计/整洁架构/README.link: -------------------------------------------------------------------------------- 1 | https://github.com/wx-chevalier/DDD-and-Clean-Architecture-Notes.git -------------------------------------------------------------------------------- /02~架构风格与模式/分层架构设计/架构演化.md: -------------------------------------------------------------------------------- 1 | # 分层架构演化 2 | 3 | # 领域合并 4 | 5 | ## 微服务架构的演进 6 | 7 | 域模型中对象的层次从内到外依次是:值对象、实体、聚合和限界上下文。实体或值对象的简单变更,一般不会让领域模型和微服务发生大的变化。但聚合的重组或拆分却可以。这是因为聚合内业务功能内聚,能独立完成特定的业务逻辑。那聚合的重组或拆分,势必就会引起业务模块和系统功能的变化了。这里我们可以以聚合为基础单元,完成领域模型和微服务架构的演进。聚合可以作为一个整体,在不同的领域模型之间重组或者拆分,或者直接将一个聚合独立为微服务。 8 | 9 | ![微服务演化](https://s3.ax1x.com/2021/02/05/y8o30f.png) 10 | 11 | 我们结合上图,以微服务 1 为例,讲解下微服务架构的演进过程: 12 | 13 | - 当你发现微服务 1 中聚合 a 的功能经常被高频访问,以致拖累整个微服务 1 的性能时,我们可以把聚合 a 的代码,从微服务 1 中剥离出来,独立为微服务 2。这样微服务 2 就可轻松应对高性能场景。 14 | - 在业务发展到一定程度以后,你会发现微服务 3 的领域模型有了变化,聚合 d 会更适合放到微服务 1 的领域模型中。这时你就可以将聚合 d 的代码整体搬迁到微服务 1 中。如果你在设计时已经定义好了聚合之间的代码边界,这个过程不会太复杂,也不会花太多时间。 15 | - 最后我们发现,在经历模型和架构演进后,微服务 1 已经从最初包含聚合 a、b、c,演进为包含聚合 b、c、d 的新领域模型和微服务了。 16 | 17 | ## 微服务内服务的演进 18 | 19 | 在微服务内部,实体的方法被领域服务组合和封装,领域服务又被应用服务组合和封装。在服务逐层组合和封装的过程中,你会发现这样一个有趣的现象。 20 | 21 | ![应用层内合并](https://s3.ax1x.com/2021/02/05/y8TiCQ.png) 22 | 23 | 我们看下上面这张图。在服务设计时,你并不一定能完整预测有哪些下层服务会被多少个上层服务组装,因此领域层通常只提供一些原子服务,比如领域服务 a、b、c。但随着系统功能增强和外部接入越来越多,应用服务会不断丰富。有一天你会发现领域服务 b 和 c 同时多次被多个应用服务调用了,执行顺序也基本一致。这时你可以考虑将 b 和 c 合并,再将应用服务中 b、c 的功能下沉到领域层,演进为新的领域服务(b+c)。这样既减少了服务的数量,也减轻了上层服务组合和编排的复杂度。 24 | 25 | # 渐进式能力下沉 26 | 27 | 在现实业务中,很多的功能都是用例特有的(Use case specific)的,如果盲目的使用 Domain 收拢业务并不见得能带来多大的益处。相反,这种收拢会导致 Domain 层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。所谓的能力下沉,是指我们不强求一次就能设计出 Domain 的能力,也不需要强制要求把所有的业务功能都放到 Domain 层,而是采用实用主义的态度,即只对那些需要在多个场景中需要被复用的能力进行抽象下沉,而不需要复用的,就暂时放在 App 层的 Use Case 里就好了。这里的 Use Case 是《架构整洁之道》里面的术语,简单理解就是响应一个 Request 的处理过程。 28 | 29 | 这种循序渐进的能力下沉策略,应该是一种更符合实际、更敏捷的方法。因为我们承认模型不是一次性设计出来的,而是迭代演化出来的。下沉的过程如下图所示,假设两个 Use Case 中,我们发现 uc1 的 step3 和 uc2 的 step1 有类似的功能,我们就可以考虑让其下沉到 Domain 层,从而增加代码的复用性。 30 | 31 | ![应用层能力下沉示意图](https://assets.ng-tech.icu/item/20230426100517.png) 32 | 33 | 指导下沉有两个关键指标:代码的复用性和内聚性。复用性是告诉我们 When(什么时候该下沉了),即有重复代码的时候。内聚性是告诉我们 How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上;因为 Domain 层的能力也是有两个层次的,一个是 Domain Service 这是相对比较粗的粒度,另一个是 Domain 的 Model 这个是最细粒度的复用。 34 | 35 | 比如,在我们的商品域,经常需要判断一个商品是不是最小单位,是不是中包商品。像这种能力就非常有必要直接挂载在 Model 上。 36 | 37 | ```java 38 | public class CSPU { 39 | private String code; 40 | private String baseCode; 41 | 42 | //省略其它属性 43 | /** 44 | * 单品是否为最小单位。 45 | * 46 | */ 47 | public boolean isMinimumUnit() { 48 | return StringUtils.equals(code, baseCode); 49 | } 50 | 51 | /** 52 | * 针对中包的特殊处理 53 | * 54 | */ 55 | public boolean isMidPackage() { 56 | return StringUtils.equals(code, midPackageCode); 57 | } 58 | } 59 | ``` 60 | 61 | 之前,因为老系统中没有领域模型,没有 CSPU 这个实体。你会发现像判断单品是否为最小单位的逻辑是以 StringUtils.equals(code, baseCode)的形式散落在代码的各个角落。这种代码的可理解性是可想而知的,至少我在第一眼看到这个代码的时候,是完全不知道什么意思。 62 | -------------------------------------------------------------------------------- /03~架构设计方式/README.md: -------------------------------------------------------------------------------- 1 | # 架构的推导 2 | 3 | ![架构设计流程](https://assets.ng-tech.icu/item/20230418155906.png) 4 | 5 | - 产品功能架构:描述的是系统能做什么。用于指导用户使用产品。 6 | - 业务概念架构:表示拥有哪些业务模块,且各自的能力是什么。用于研发人员和业务人员理解业务内在的概念和联系。 7 | - 应用逻辑架构:表示系统中有哪些模块、模块的职责以及模块的关系,模块主要包含功能模块、复用模块、非功能模块。用于指导软件的研发。 8 | - 工程骨架架构:约束代码组织结构,防止代码混乱不堪。 9 | 10 | # 非功能性需求决定架构 11 | 12 | 因为软件是为了满足客户的功能性需求的,所以很多设计人员可能会认为架构是由要实现的功能性需求决定的。但实际上真正决定软件架构的其实是非功能性需求。架构师要更加关注非功能性需求,常见的非功能性包括:性能,伸缩性,扩展性和可维护性等,甚至还包括团队技术水平和发布时间要求。 13 | 14 | 能实现功能的设计总是有很多,考虑了非功能性需求后才能筛选出最合适的设计。 15 | -------------------------------------------------------------------------------- /03~架构设计方式/UML/README.md: -------------------------------------------------------------------------------- 1 | # UML 2 | 3 | 统一建模语言(英语:Unified Modeling Language,缩写 UML)是非专利的第三代建模和规约语言。UML 是一种为面向对象开发系统的产品进行说明、可视化、和编制文档的标准语言;UML 作为一种模型语言,它使开发人员专注于建立产品的模型和结构,而不是选用什么程序语言和算法实现;UML 是不同于其他常见的编程语言,如 C++,Java 中,COBOL 等,它是一种绘画语言,用来做软件蓝图;UML 不是一种编程语言,但工具可用于生成各种语言的代码中使用 UML 图。 4 | 5 | # 图表 6 | 7 | UML 的核心是图表,大致可以将这些图归类为结构图和行为图。 8 | 9 | - 结构图是由静态图,如类图,对象图等静态图; 10 | - 行为图是由像序列图,协作图等动态图; 11 | 12 | 一个系统的静态和动态特性是通过使用这些图的可视化。 13 | 14 | ## 结构图 15 | 16 | - 类图:类图是使用面向对象的社会最流行的 UML 图。它描述了在一个系统中的对象和他们的关系,能够让我们在正确编写代码以前对系统有一个全面的认识。一个单独的类图描述系统的一个具体方面,收集类图表示整个系统。基本上,类图表示系统的静态视图。类图是唯一可以直接映射到面向对象的语言 UML 图。因此,它被广泛应用于开发者社区。 17 | 18 | - 对象图:对象图是类图的一个实例。因此,一类图的基本要素是类似的。对象图是由对象和链接。在一个特定的时刻,它捕获该系统的实例。对象图用于原型设计,逆向工程和实际场景建模。 19 | 20 | - 用例图:用例图是从用户角度描述系统功能,并指出各功能的操作者,用来捕捉系统的动态性质。一个高层次的设计用例图是用来捕捉系统的要求,因此它代表系统的功能和流向。虽然用例图的正向和反向工程是不是一个很好的选择,但他们仍然在一个稍微不同的方法来模拟它。 21 | 22 | ## 行为图 23 | 24 | - 交互图:交互图,用于捕获系统的动态性质。交互图包括时序图和协作图,其中:序列图显示对象之间的动态合作关系,它强调对象之间消息发送的顺序,同时显示对象之间的交互;协作图描述对象间的协作关系,协作图跟时序图相似,显示对象间的动态合作关系。 25 | 26 | - 状态图:状态图是一个用于模拟系统的动态性质的五个图。这些图用来模拟一个对象的整个生命周期。一个对象的状态被定义为对象所在的条件下,特定的时间和对象移动对其他状态,在某些事件发生时。状态图还用于正向和反向工程。状态图着重描述从一个状态到另一个状态的流程,主要有外部事件的参与。 27 | 28 | - 活动图:活动图描述满足用例要求所要进行的活动以及活动间的约束关系,有利于识别并行活动。活动图是一种特殊的状态图,它对于系统的功能建模特别重要,强调对象间的控制流程。 29 | -------------------------------------------------------------------------------- /03~架构设计方式/UML/时序图.md: -------------------------------------------------------------------------------- 1 | # 时序图 2 | -------------------------------------------------------------------------------- /03~架构设计方式/UML/类关系.md: -------------------------------------------------------------------------------- 1 | # 类关系 2 | 3 | 下图是对于类关系的简要呈现: 4 | 5 | ![](https://assets.ng-tech.icu/item/20230427200611.png) 6 | 7 | 我们首先通过某个简单的例子对类关系有基础认知: 8 | 9 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_class_struct.jpg) 10 | 11 | - 车的类图结构为 `<>`,表示车是一个抽象类; 12 | - 它有两个继承类:小汽车和自行车;它们之间的关系为实现关系,使用带空心箭头的虚线表示; 13 | - 小汽车为与 SUV 之间也是继承关系,它们之间的关系为泛化关系,使用带空心箭头的实线表示; 14 | - 小汽车与发动机之间是组合关系,使用带实心箭头的实线表示; 15 | - 学生与班级之间是聚合关系,使用带空心箭头的实线表示; 16 | - 学生与身份证之间为关联关系,使用一根实线表示; 17 | - 学生上学需要用到自行车,与自行车是一种依赖关系,使用带箭头的虚线表示; 18 | 19 | # 泛化关系(Generalization) 20 | 21 | 类的继承结构表现在 UML 中为:泛化(generalize)与实现(realize):继承关系为 is-a 的关系;两个对象之间如果可以用 is-a 来表示,就是继承关系:(..是..)。eg:自行车是车、猫是动物。泛化关系用一条带空心箭头的直接表示;如下图表示(A 继承自 B); 22 | 23 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_generalization.jpg) 24 | 25 | eg:汽车在现实中有实现,可用汽车定义具体的对象;汽车与 SUV 之间为泛化关系; 26 | 27 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_generalize.jpg) 28 | 29 | 最终代码中,泛化关系表现为继承非抽象类。 30 | 31 | # 实现关系(Realize) 32 | 33 | 实现关系用一条带空心箭头的虚线表示;eg:”车”为一个抽象概念,在现实中并无法直接用来定义对象;只有指明具体的子类(汽车还是自行车),才 可以用来定义对象(”车”这个类在 C++中用抽象类表示,在 Java 中有接口这个概念,更容易理解) 34 | 35 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_realize.jpg) 36 | 37 | 最终代码中,实现关系表现为继承抽象类。 38 | 39 | # 关联关系(Association) 40 | 41 | 关联关系是用一条直线表示的;它描述不同类的对象之间的结构关系;它是一种静态关系,通常与运行状态无关,一般由常识等因素决定的;它一般用来定义对象之间静态的、天然的结构;所以,关联关系是一种“强关联”的关系;比如,乘车人和车票之间就是一种关联关系;学生和学校就是一种关联关系;关联关系默认不强调方向,表示对象间相互知道;如果特别强调方向,如下图,表示 A 知道 B,但 B 不知道 A; 42 | 43 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_association.jpg) 44 | 45 | 在最终代码中,关联对象通常是以成员变量的形式实现的。 46 | 47 | # 聚合关系(Aggregation) 48 | 49 | 聚合关系用一条带空心菱形箭头的直线表示,如下图表示 A 聚合到 B 上,或者说 B 由 A 组成; 50 | 51 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_aggregation.jpg) 52 | 53 | 聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义;例如一个部门由多个员工组成;与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在;例如,部门撤销了,人员不会消失,他们依然存在; 54 | 55 | # 组合关系(Composition) 56 | 57 | 组合关系用一条带实心菱形箭头直线表示,如下图表示 A 组成 B,或者 B 由 A 组成; 58 | 59 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_composition.jpg) 60 | 61 | 与聚合关系一样,组合关系同样表示整体由部分构成的语义;比如公司由多个部门组成;但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了;例如,公司不存在了,部门也将不存在了; 62 | 63 | # 依赖关系(Dependency) 64 | 65 | 依赖关系是用一套带箭头的虚线表示的;如下图表示 A 依赖于 B;他描述一个对象在运行期间会用到另一个对象的关系; 66 | 67 | ![](http://design-patterns.readthedocs.org/zh_CN/latest/_images/uml_dependency.jpg) 68 | 69 | 与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化;依赖关系也可能发生变化;显然,依赖也有方向,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生;注:在最终代码中,依赖关系体现为类构造方法及类方法的传入参数,箭头的指向为调用关系;依赖关系处理临时知道对方外,还是“使用”对方的方法和属性; 70 | -------------------------------------------------------------------------------- /03~架构设计方式/UML/类图.md: -------------------------------------------------------------------------------- 1 | # 类图 2 | 3 | 一般来说一个类会分为三个区域:最上面是类名称,中间部分包含类的属性,底部部分包含类的方法。 4 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/README.md: -------------------------------------------------------------------------------- 1 | # 业务中台化架构 2 | 3 | 中台的核心是提供封装业务模型的方法。它旨在帮助新型的小型企业提供一流的服务,而无需传统企业基础架构的成本,并使现有组织能够以惊人的速度将创新服务推向市场。中台战略最初是由阿里巴巴提出的,并很快被许多中国的互联网公司所采用,因为它们的商业模式是数字原生的,可以复制到新的市场和领域。如今,越来越多的中国公司将中台作为数字化转型的杠杆。 4 | 5 | ## 中台源起 6 | 7 | 美军的特种部队加航空母舰的策略:10 人以内的一支特种部队在战斗的最前沿侦查,独立决策,一旦发现目标,迅速呼叫强大的中台航母群对其进行毁灭性打击。从汽油车到电动车的转变,汽油车是每台车会内置一个内燃机,而电动车则将分散的内燃机收归到一个大的发电厂,从而降低前端的复杂度。这样发电厂自身的升级改造,譬如从火电到核电或其他更高效清洁的能源,就无须将复杂性透出给终端用户了。 8 | 9 | 芬兰著名的游戏公司 SuperCell,开发了部落冲突、皇室战争等现象级的手游。整个公司才 200 多号人,就被腾讯以 86 亿美金收购。在 SuperCell 里,一个游戏开发团队平均是 3-7 个人,但有一个强大的游戏中台在做支撑,可以并行开发 50 款游戏,然后通过“内部赛马”产生出一到两款经典。据说马云在带领阿里众多高官参观了 SuperCell 后,回来就在阿里整个集团层面启动了大中台战略。 10 | 11 | ## 业务中台、数据中台 12 | 13 | 进入两千年后,随着互联网应用的快速发展,很多传统企业开始触网,建设自己的互联网电商平台。后来又随着微信和 App 等移动互联应用的兴起,又形成了新一轮的移动应用热潮。这些移动互联应用大多面向个人或者第三方,市场和需求变化快,需要以更敏捷的速度适应市场变化,为了保持快速响应能力和频繁发版的要求,很多时候这些移动互联网应用是独立于传统核心系统建设的,但两者承载的业务大部分又都是同质的,因此很容易出现业务能力重叠的问题。 14 | 15 | 中台是典型的认知折叠,提升后端的复杂性以降低前端的复杂性。中台源于平台,对其定义也是见仁见智。从笔者的角度来看,中台提供可被集成的能力,不直接构建面向终端用户的应用;平台也是能够允许第三方集成进来,或者直接面向终端用户提供服务。中台的核心本质就是:业务为本、网络连接、数据智能,因此我们常说的中台架构可分为业务中台与《[Datawarehousr-Notes/数据中台](https://github.com/wx-chevalier/-Notes?q=)》,数据中台更多地偏向于中立、统一的数据治理与共享,而本篇专注于业务中台的理念与设计原则。 16 | 17 | 业务中台,是为了提炼各个业务条线的共性需求,并将这些打造成组件化的资源包,然后以接口的形式提供给前台各业务部门使用,可以使产品在更新迭代、创新拓展的过程中研发更灵活、业务更敏捷,最大限度地减少重复造轮子的 KPI 项目。中台不同于平台,不是最终用户能直接使用的,它必须被集成到各个业务场景中。前台要做什么业务,需要什么资源可以直接同公共服务部要。搜索、共享组件、数据技术等模块不需要每次去改动底层进行研发,而是在底层不变动的情况下,在更丰富灵活的大中台基础上获取支持,让小前台更加灵活敏捷。 18 | 19 | - 核心能力的重复建设。由于销售同质产品,二者在核心业务流程和功能上必然相似,因此在核心业务能力上存在功能重叠是不可避免的。传统核心应用有报价、投保、核保和出单功能,同样在互联网电商平台也有。这就是核心能力的重复建设。 20 | 21 | - 通用能力的重复建设。传统核心应用的通用平台大而全,通常会比较重。而互联网电商平台离不开这些通用能力的支撑,但为了保持敏捷性,一般会自己建设缩小版的通用功能,比如用户、客户等。这是通用能力的重复建设。 22 | 23 | - 业务职能的分离建设。有一类业务功能,在互联网电商平台中建设了一部分,在传统核心应用中也建设了一部分,二者功能不重叠而且还互补,组合在一起是一个完整的业务职能。比如缴费功能,互联网电商平台主要面向个人客户,于是采用了支付宝和微信支付的方式。而传统核心应用主要是柜台操作,仍在采用移动 POS 机的缴费方式。二者都是缴费,为了保证业务模型的完整性,在构建中台业务模型时,我们可以考虑将这两部分模型重组为一个完整的业务模型。 24 | 25 | - 互联网电商平台和传统核心功能前后完全独立建设。传统核心应用主要面向柜台,不需要互联网电商平台的在线客服、话务、订单和购物车等功能。而互联网电商平台主要面向个人客户,它不需要后端比较重的再保、佣金、打印等功能。在构建中台业务模型时,对这种情况应区别对待,将面向后端业务管理的应用沉淀到后台,将前端能力构建为面向互联网渠道的通用中台,比如订单等。 26 | 27 | # 前台,中台,后台 28 | 29 | 既然讲中台,必然还有前台和后台。前台很好理解,指的是面向 C 端的应用,包括前端 (如 App/ 小程序) 和对应的服务端。至于后台,很多人把它等同于管理后台,比如商品管理后台,负责商品定义 / 上下架等,提供给内部运营人员使用,这可能不够准确。简单来说,对于一个交易系统,前台对应用户能看到的部分,如商品浏览和下单,属于接单的部分;后台对应履单部分,如仓库拣货 / 配送 / 财务结算 / 采购补货等,属于实际干活的,由企业内部人员负责,处于一个交易处理流程的后端。 30 | 31 | 在传统企业,没有在线的前台,基本是线下手工接单,内部信息管理系统基本都属于履单范畴,例如 ERP、CRM、采购系统、仓库管理系统,财务系统等,这些系统属于一般意义上的后台概念。在互联网企业,因为系统一般是自己开发,管理后台既包含面向前台销售的功能,如商品上下架和促销管理,也包含面向履单部分,比如配送、采购、财务结算,所以互联网企业的管理后台并不简单等同于履单后台。 32 | 33 | 接单和履单之间还有一系列事情要做,包括生成订单时的优惠计算 / 创建实际的订单 / 支付 / 库存扣减等, 这部分功能属于交易逻辑的核心。在简单场景下,前台应用包含这部分功能,在复杂的场景下,就有必要把这部分独立出来,构成独立的中台,为前台减负。 34 | 35 | | 端 | 计算机系统 | 电商系统 | 36 | | ---- | ---------- | ------------ | 37 | | 前台 | 桌面应用 | C 端应用 | 38 | | 中台 | 操作系统 | 新零售中台 | 39 | | 后台 | 硬件设备 | 内部基础设施 | 40 | 41 | 桌面应用能不能直接操纵底层硬件设备完成功能?理论上是可以的,比如在应用里嵌入汇编语言直接操作硬件,但显然开发效率低,可维护性很差。如果中间加一层操作系统进行转换,向下管理硬件,向上提供简洁的 API,应用开发就非常方便,这里操作系统类似中台的定位。 42 | 43 | 对于大型传统零售企业 (上图右边部分),企业经过多年信息化建设,购买了大量的商业套装软件,形成内部 IT 基础设施,现在要往新零售转型,理论上 C 端的应用可以直接调用老系统的 API(如 SAP 产品提供一定的开放能力) 来实现功能打通。但和桌面应用直接控制电脑硬件设备类似,这两者直接对接是低效的,两者的服务对象 (2C 和 2B)/ 数据模型 / 技术栈 / 实时性要求差异很大,而且新的应用进来,又要从头到尾对接一遍,新业务上线,至少需要大半年的时间。 44 | 45 | 这时如果有个中间层负责桥接和转换,就非常方便。C 端应用可以快速基于这个中间层构建,不用关心底层遗留系统的实现细节。这个中间层就是中台,起到类似操作系统的作用,把旧的基础设施转换成面向互联网的基础平台,而且这个平台非常通用,新业务可以快速对接,短时间搞定上线。传统企业在做全面数字化转型时,这样的一个中台必不可少。 46 | 47 | # 中台与现有服务对比 48 | 49 | ## 中台和微服务 50 | 51 | 中台是微服务的升级,原来只是一个个离散的服务,只负责提供接口功能,如商品服务 / 订单服务 / 权限服务,在中台里,升级为商品中心 / 订单中心,每个中心更强调体系,包括更好的边界划分和业务抽象,更好地监控和系统运营能力 (稳定性 / 故障定位),更好的业务运营能力 (比如商品中心自带商品管理后台,支持基础商品定义)。每个服务中心围绕核心业务,自成体系,成为一个微内核,这些微内核既相互独立,又形成一个整体,共同构成基础业务平台,也即中台。松散的微服务 ->共享服务体系 ->中台,这是微服务架构和中台的区别和联系。 52 | 53 | ## 中台与云 54 | 55 | PaaS 提供了一种服务,客户的程序员通过二次开发使用 PaaS 服务,最终完成某个功能给最终用户。PaaS 的通用性需要非常强,这样才能获得足够大的市场,比如 IM、视频云服务等,也因此 PaaS 往往是没有界面的。SaaS 提供的服务不需要客户进行开发,只需要开通服务,在管理后台上配置一下就可以直接使用。但 SaaS 服务往往针对的是一个细分领域,其定制化能力也相对弱很多。即使是像钉钉,钉钉选择 IM 这种企业中最通用化的服务,同时做成企业服务的开放生态,目标客户也主要是覆盖中小企业。定制化需求强的大客户,也往往会需要希望借助 IM PaaS 服务来自主研发内部 IM 工具。 56 | 57 | PaaS 和 SaaS 定位在服务外部客户,必须具备很低的使用成本。即使是需要通过技术来接入的 PaaS 服务,接入成本也一定要足够低,接口清晰,文档完善。中台首先是定位在服务公司内部客户,由于这个范围的限定,导致 中台的通用性可以在很多约束条件下来实现,可服务的领域比 SaaS 广。比如即使同样是电商,淘宝、天猫、聚划算、闲鱼、飞猪的站点都是不一样的,而阿里共享事业部就在中台层服务多个业务。此外,由于中台的用户是公司内部的程序员,大家有相似的背景,也可以频繁沟通,所以服务接入的成本可以做得比面向外部客户的 PaaS 要高。 58 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/业务与架构/业务定义.md: -------------------------------------------------------------------------------- 1 | # 业务定义 2 | 3 | 本节主要讨论,什么是业务?应该如何去划分业务?业务描述的对象既有可能指个人也有可能指企业,它更多的指的是行业的工作,而从中台的角度来看,特指企业组织以达成企业利益目标为目的,所组织的一系列活动称为业务。 4 | 5 | 在软件系统层面,我们必然要针对这些业务给出标识,以便于我们在软件系统中知道每一次的系统请求和操作是属于哪一个业务的,所有针对业务的这些标识,我们称之为业务身份。按照前面对业务的概念和特征的定义,我们可以明确,业务身份应当包含的内容: 6 | 7 | - 团队与组织机构; 8 | 9 | - 业务的识别条件,譬如类目,可能是商品标或者是买家特征、渠道信息等等; 10 | 11 | 有了业务身份之后,我们就在软件系统中就有了业务逻辑隔离的依据。因为不同的业务,都会有自己独有的业务特征,这些特征会导致业务在流程、规则上有不同的要求。在传统的开发中,我们会通过 `if...else...` 来实现不同业务逻辑的划分,这也就会导致复杂业务间的鲁棒性较低。在此之上,我们也会认知到,业务本身也是分类型的: 12 | 13 | - 水平业务:如果一个业务需要依赖其它业务所包含的商品,并且,需要结合其它业务规则才能完成完整的经营活动,称为水平业务。 14 | 15 | - 垂直业务:反之,如果能够独立的提供商品的,就称之为垂直业务。 16 | 17 | 垂直业务和水平业务之间是可以进行业务规则叠加的,在业务规则产生冲突的情况下,需要业务确认优先级。而垂直业务和垂直业务之间是不能叠加的。 18 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/业务与架构/业务挑战.md: -------------------------------------------------------------------------------- 1 | # 业务挑战 2 | 3 | 在很多的互联网领域中,会根据领域服务来建设分布式架构;而随着业务领域的不断交叉,业务生命周期的不断延长,从招商、选品、供应链、仓储、营销/导购、下单、履约、物流、售后等,其业务链路长、业务逻辑上游对下游又有影响。在这业务主线的链路上,又建设了众多系统进行支撑,比如商品平台、购物车系统、下单系统、履约系统、优惠系统、物流系统、供应链系统等,围绕这些核心系统,还有数不清的辅助系统/服务。 4 | 5 | 业务型平台在面对业务数量和类型的不断增加,在支持效率和成本上都受到挑战,单靠平台侧的人员无法独立解决好这个问题,需要和业务团队的同学更好的协同才有可能。在[微服务与云原生](https://ng-tech.icu/books/MicroService-Notes/#/) 篇中,我们讨论过某个交付的系统是受该组织的内部结构极大的影响;那么在每一次跨团队的对焦、分析、协作中,也存在着很多的痛点。 6 | 7 | # 业务与平台耦合性高 8 | 9 | 业务本身会分成“垂直”和“水平”两个维度。在一次业务交互过程中,其业务复杂度就在于业务“垂直”维度与“水平”维度产生的叠加,并由叠加而产生的业务规则上的冲突。针对业务叠加的处理,各系统基本上还是基于 SPI 扩展机制,这些 SPI 缺少按照业务维度进行组织与隔离。在业务种类少,不同业务在逻辑叠加度小的情况下还是可以在很大程度上解决业务可定制化、多样化的问题。但随着各类业务越来越多时,就会导致各类业务在同一个扩展点上的叠加效应越来越突出。其中最薄弱的点就是 SPI 接口中是否需要执行的过滤方法(filter)的编写。一旦过滤方法写得不好,就可能会造成不该执行的逻辑被执行了,或者把后续本该执行的逻辑给跳过了。 10 | 11 | 在共享的各个平台中,提供给业务方可扩展的 SPI 多达几百个。一个业务的最终逻辑是否正确,就需要该业务确保这几百个 SPI 决策树中每个节点注册的位置正确,过滤方法中的过滤条件正确,同时执行逻辑也必须确。不仅如此,本业务注册的 SPI 都正确了,还需要其他的业务注册的 SPI 也都是正确的,这最终导致了业务与业务之间高度耦合。这种耦合,又进一步导致了各业务方之间、业务方与平台之间的大量联调、集成与回归等配合工作,无法做到自助式的业务设计、开发与交付 12 | 13 | # 协同效率低,口径难以统一 14 | 15 | 跨团队协作时候,往往缺少业务全链路视角的需求管理机制,协同效率低。往往需求的描述非常简单,或者存在着大量的重复建设的需求。并且需求传递效率低,需要反复沟通。 16 | 17 | 而从技术同学的角度来看,因为业务与平台耦合性高,因此技术同学也很难给出准确的排期,从而导致双方的预期值有一定差异,对于业务的支持也就难以及时到位。 18 | 19 | # 缺少可复用的业务资产 20 | 21 | 一个企业的 IT 体系建设是否成熟,业界是有一些指导框架来进行评估的,比如 TOGAF 框架。在该信息系统建设框架中,有一个很重要的系统成熟度评估项目:Enterprise Continumm(企业统一体)。这里面的关键是企业需要建立: 22 | 23 | - 架构统一体(Architecture Continuum):该统一体能从特定架构中提取出可复用的组件到仓库中(Reposity),为后续的类似业务的重用(Gerneralization for future re-use)。在具体应用中,可以从组件仓库中选择可复用的组件并进行与实际应用场景适配(Adaptation for use)。 24 | 25 | - 解决方案统一体(Solutions Continuum):与架构统一体类似,在面对不同的市场,需要能从可复用的解决方案库中选择并快速复制。对于新兴市场的交付,也能提取成可复用的解决方案到资产库中。 26 | 27 | # 中台接入门槛高 28 | 29 | 对于大型的业务平台而言,其链路流程往往较长,整个迭代周期也会愈发缓慢。而很多的新业务需要的是快速试错,这就需要我们提供精简的、能够快速转向的中台能力。 30 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/业务与架构/业务模型.md: -------------------------------------------------------------------------------- 1 | # 业务模型 2 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/业务与架构/中台架构.md: -------------------------------------------------------------------------------- 1 | # 中台架构 2 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/中台红与黑/README.md: -------------------------------------------------------------------------------- 1 | # 中台红与黑 2 | 3 | 定义中台我认为可以有两个角度,一个是从中台本身的价值和出发点来的:中台是在多个部门之间共享的开发资源所提供的业务能力,数据能力和计算能力的集合。另一个定义从中台的相对定位来的:前台是面向终端用户的一组业务能力。业务中台是对前台应用的抽象,提供多个前台业务之间共享的业务逻辑,数据和计算能力。 4 | 5 | 我想特别强调中台和前台的定义差别。前台服务单个业务,目标是就是这个业务的增长。前台必须紧贴业务做好差异化。前台的定位要考虑到竞争环境,目标客群,业务成长阶段,运营人员能力,人才供给,监管环境等因素。前台要有自己的技术内容,定制流程,流程对接和个性化数据应用。中台服务整个集团,目标往往是降低成本,加强管控,或者是扩大规模优势 。中台的定位在以集团利益最大化的前提下最大化服务前台业务的需求。中台有自己的技术实现,研发流程,和数据标准。而后台是不具备任何业务语义的基础计算能力。 6 | 7 | 我们可以先看到中台最初的动力来自哪里? 不论是甲骨文还是后来的阿里,其实本质动因是一个大公司内部的大业务呈军阀割据现象,导致多条业务线重复造轮。由此而衍生出其他的问题,比如说团队之间内耗严重;小业务无资源,增长乏力;整个公司数字资产不统一,损失机会成本;业务线也不能对核心系统做打磨,业务线不稳定。因为这些原因,所以阿里的高管们就以美国海军陆战队和 Supercell 的组织形式为启发,做了“大(业务)中台,小(业务)前台”的策略。这里先不谈中台是否能解决这些问题,或者是说战略启发是否正确,但是毫无疑问的是,中台想解决的问题既没有过时,也依然正在不同的公司里发生,所以这些问题也必须解决。也就是说从问题定义角度来说,中台是个完全正确的方向。 8 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/中台红与黑/中台的缺陷.md: -------------------------------------------------------------------------------- 1 | # 中台的缺陷 2 | 3 | 建中台,组织架构一定要调整,但动组织架构就意味着在动“权力、金钱和人员”。不难看出,这个话题太敏感。 4 | 5 | ## 难以应对 Cross cutting concern 6 | 7 | 根据中台进行系统拆分和部门调整之后,还是会遇到 cross cutting concern,什么是 cross cutting concern: 8 | 9 | > The crosscutting concern is a concern which is applicable throughout the application and it affects the entire application. For example: logging, security and data transfer are the concerns which are needed in almost every module of an application, hence they are cross-cutting concerns. 10 | 11 | 有些需求难以避免地会影响整个流程中的所有系统:比如从技术范畴进行的一些改造(如为了完成 tracing,所有系统增加 trace id,并在 log 中默认携带),比如从业务范畴进行的 i18n 改造。这些改造需求一般天生就是跨系统、跨组、跨部门的,事情一带上“跨”的字眼,就不好搞了。举一个典型的例子,某巨型互联网公司员工抱怨,在当前的微服务和中台架构前提下,做一个需求经常要改 20+ 个模块,苦不堪言,连上线顺序都不一定搞得清楚。当这 20+ 个模块又是跨部门的时候,就更难了。想要推动其它部门做一些短期看起来没啥收益的事,太难了。 12 | 13 | ## 稳定性和灵活性的矛盾 14 | 15 | 对于一个系统来说,追求稳定性,那么必然会在修改和升级上较为消极;追求灵活性,那在功能迭代上一定会较为激进。这两方面的矛盾本来就是难以调和的。追求其中之一,在一定程度上就得放弃另一方面。有很多中台系统被剥离之后,因为用户众多,一旦出现技术上的问题,影响面巨大。 16 | 17 | 中台的建设过程中节奏是最要命的。这其中有一个矛盾点,就是业务线在发展中是快速变化的,快速变化必然就会渴望得到各种资源支持。但是中台大部分是在缓慢建设与推进,两者之间会产生诉求与承接能力不匹配的问题。这块如何做好平衡,就涉及到先做什么、后做什么的问题。 18 | 19 | 一个被完全中台化的业务导致集团内部过分分工,任何前台业务都被认为是中台能力的线性组合。举个例子,有的公司会有接近或超过千人的供应链中台,搜索广告中台,内容中台等等,而多数业务前台少则几个人,多不过几十人。前台团队任何一个人哪怕是全职和一个中台域对接,也无法理解该域的全貌或者是跟得上这个中台的演变。这意味着前台业务完全无法在这些中台相关的领域做创新。本来的创新业务变成无从创新,当初的动力变成了中台的最大的诅咒。有说法说,一个业务靠拖拉拽就能编排出来了,这不是创新是什么? 事实证明这种创新完全无用。没有任何一个投资人会把自己的钱投到一个可以被大公司拖拉拽出来的商业模式的。真正的创新不是现有能力的线性组合。 20 | 21 | 中台自身的场景往往缺乏前瞻设计,是对现有场景的抽象。而当某个创新在一个前线业务线孵化出来之后,中台团队会通过强制收编该能力来扩大自己的能力,同时强迫前台团队下线一个他们研发了很久的创新。这种行为往往造成精英人才的流失,使得本来就受到遏制的前台创新变得更为匮乏。 22 | 23 | ## 中台与前台的模糊业务边界、距离 24 | 25 | 在实际实践时,中台与 FT 的边界往往划得不清不楚。比如用户服务、用户权益、用户在各种子系统中的状态,这些内容可能并不是用户服务本身关心的内容。但往往需求也会提给用户服务,这时候用户服务就只是进行字段存储,而状态机变化则完全在外部。 26 | 27 | 如果对系统内的个别数据不进行管理,那么有其它接入方接入时,就无法解释清楚字段的含义和使用场景。如果不接受这些不相干的数据接入,那么前台流程系统可能会在自己内部重新建立自己的数据系统,这部分系统又极有可能和中台有功能上的重叠。 28 | 29 | 如果想要把这些数据接管过来,那么中台又需要梳理所有业务场景。或者说明需要把所有对数据进行修改的逻辑全部收拢到中台内部,这往往又会产生与中台与前台业务边界的冲突。 30 | 31 | 中台经常以最全的最复杂的实现来应对任何一个简单的应用场景。大量成熟行业和强监管环境下的需求被带入到了创新业务中。在带来了大量的运营复杂性的同时增加了用户(买家,卖家,本地运营)的学习难度。这就是我们常讲的膨胀软件(Bloatware): 巨大,复杂,缓慢,低效。 32 | 33 | # KPI 34 | 35 | “这个业务中台的考核目标是什么?” 36 | 37 | 这块业务是做内容生态、创作者生态,但是当时只有创作、内容生产、内容审核、内容库等等从内容维度的奖励,没有内容的出口。面向 C 端的出口都在其他 BU,那这个中台业务如何进行考核?考核指标该如何制定? 38 | 39 | 要从规模、品质、活跃、消费、互动、收益这六个维度定义十几个指标吗? 40 | 要从月 / 日均活跃账户数量、月 / 日均账户生产文章数量,再加上账号内容在端的月 / 日均播放量、阅读量等等维度进行考核吗? 41 | 42 | 无论从哪个角度来看,都感觉不太合适。这些指标都受到端的影响,没有一个指标仅仅跟中台业务本身相关联。比如有的人提到既然是围绕创作者的生态,那就只看创作者、内容生产就好了,但实际上每一部分都有成本,如果生产出来的内容在不同端效果很差,到底是用户画像的问题,还是算法的问题,还是内容质量的问题?各个业务都要承担 KPI,自然就会打架。 43 | 44 | 另外,以前各端的创作者奖励相当于成本,现在因为都归到中台来承担了,从财务角度只看到成本,那收益和利润该如何算呢?因为出口在各端,不同端的信息流中商业化收入会算到各端业务侧,在内容商业化探索上,也没有想象得那么容易。 45 | 46 | # 阿里中台战略败点分析 47 | 48 | ## 中台应当分门别类,因地制宜,全局中台并不适合大集团 49 | 50 | 阿里集团业务繁杂度远高于超级细胞,中台范围应当细化,不适全局中台。治理小区和治理国家固然有一些类似的点和方法论,但是问题规模变大后,很多小规模下的方法也会变得不再适用,这是一个广泛被认可的观点。超级细胞的业务范围很专一,就是手游,所以它可以很容易就做成游戏工厂。放到阿里这样的大集团,业务种类繁杂,性质大相径庭,何必强融?像淘宝、一淘、天猫,这些其实换汤不换药,那么即便没有中台战略,架构师也知道很多业务逻辑可以复用,要说阿里架构师没有超级细胞的架构师懂?怎么可能。文献[7]中提到的第三阶段其实已经在考虑通过搭建业务平台来建设通用业务能力。而管理层想通过打造一个统一的万能中台来支撑所有集团业务前台(参考图 8),是未经深度思考的表现,从超级细胞小公司到阿里大集团,业务复杂度规模急剧增大,怎么能照搬模式呢。正如没有一个架构师可以在新问题上做出百分之一百与实际匹配的架构一样,也不会有一个统一的万能中台能在保证开闭原则的基础上适配所有业务。 51 | 52 | 总结来说,阿里集团业务繁杂,不能把 BU 当做前台来构建大中台,每个 BU 就是一个独立的公司,有着自己独立的业务,根本“小”不了。而为各个 BU 做的那些通用的东西也是具有局限性的,只能帮助上层 BU 节省部分研发成本。过度贪求高度复用,会陷入“强求陷进”——把不该抽象的东西硬是抽象到了一起,结果就是系统的复杂度并没有降低,而是从多个地方搬到了一个地方。因此,管理层想要的“全集团大中台、小前台”,是一种理想主义,注定难以实现。 53 | 54 | ## 全局中台带来的新问题——依赖单点和热点 55 | 56 | 按上文的理解,中台战略的实际意义更大的是在于提醒所有 BU,要尽量的增强能力复用,为 BU 业务创新营造一个高效低成本的环境。而如果我们把一家大集团的所有主要业务系统都放到一个事业部去管理,就会产生一个新的问题——单点甚至是热点现象。每个 BU 甚至业务线都各自有 KPI,如果某个 BU 发现中台无法支撑自己的场景(因为总会有没考虑的情况),那么势必要求中台团队做支撑,需求一多,还得排队,这和 BU 寻求自身的生存和发展势必是矛盾的。所以,复用能力涉及的业务范围越大,单点问题就越是严重,单点变热点的概率也就越大。即便中台事业部做的再大,哪怕为每个 BU 都搞一个小团队去支持,由于所背的 KPI 和汇报关系并不在所支撑的 BU,实践起来总是会存在信息断层。 57 | 58 | 合理的复用是不会产生热点的,因为正确的抽象聚焦是的领域内的业务,设计思路会无差别的对待所有用户,要么一个用户都没法用,要么所有用户都可以用(无故障的情况下)。就好比数据库一样,任何一款数据库都不会关注数据的业务属性,电商的数据能存,金融的数据必然也能存。引入数据库就是一种数据存取能力复用——数据库属于基础设施,复用性是必要的。 59 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/中台红与黑/中台的诉求.md: -------------------------------------------------------------------------------- 1 | # 业务线扩充 2 | 3 | 软件架构从单体架构,到分布式 SOA,到微服务,到中台架构,这都是业务复杂化的结果,架构好比生产关系,业务是生产力,架构一定要随着业务发展而演化。 4 | 5 | 0 到 1 阶段,只有一条业务线,比如出租车业务,直接根据需求实现即可。从 1 到 n 阶段,业务线逐渐增加,比如快车 / 顺风车。 6 | 7 | 这时系统有两种做法,第一种是新业务线还是单独实现,多个业务线之家是相互独立的,系统结构整体上是”川”字型,如下图所示。但如果业务线类似,它们的核心逻辑(地图 / 调度 / 订单支付)也是类似的,子系统之间有大量的代码复制和多地维护,这是非常低效的。 8 | 9 | 第二种做法是把核心逻辑单独抽取出来,做好通用化,共同服务于所有业务线的需求,此时对于各个业务线系统而言,包括自身的应用层和通用层两部分,定制的东西在应用层解决,共享的东西由通用层提供,再通过编排共享逻辑完成业务流程。系统结构整体上是”山”字型,这个通用层就是山字最底下一横,把各个业务线有机粘合在一起,共享业务逻辑和统一业务规则,实现最大程度的复用。 10 | 11 | 当然搭建山字形是有难度的,什么时候转型为“山”字形?一方面和 n 值有关,比如 n>=3 时,应该要考虑转到山字形,另一个因素和各个业务线的相似度有关,相似度高更适合”山”字形,比如电商的 C2C 和 B2C 业务;差异比较大,适合”川”字形,比如电商业务和互联网金融业务,没必要强行扭在一起。 12 | 13 | 从业务角度看,中台代表通用的基础业务,一个企业基础的业务能力和业务规则是相对稳固和清晰的,各个业务线可以认为具体业务场景,如小程序下单 / 三方外卖等相对复杂和多变,但可以通过组装各个基础业务,快速满足业务场景需求。对于新的业务来说,基础的东西已经差不多有了,只需要少量针对场景的定制开发。总的来说,中台收敛了业务场景,统一了业务规则,比如各个渠道的订单都归到中台的订单服务,遵循类似的订单状态流转和履单过程。 14 | 15 | 基础业务是有限的,业务场景是无限的,特别是在移动互联网和全面数字化转型的大背景下,传统企业需要开拓大量新渠道,搭建中台,可以很好地通过有限的基础业务满足无限的业务场景。 16 | 17 | 从系统角度看,中台相当于商业操作系统,提供标准接口给上层应用,对于传统企业来说,中台之下还有明确的后台,中台很好地把前端应用 (面向互联网) 和企业遗留系统 (面向内部管理) 衔接起来,屏蔽底层系统的复杂性和各种适配工作。 18 | 19 | 从数据角度看,中台收敛业务场景的同时,也收敛了数据 比如自有小程序的订单和外卖订单统一到一个订单库,使用同一套数据模型(具体用到的字段可能略有差异),这为后续的数据中台搭建打下良好基础。 20 | 21 | # 烟囱式平台 22 | 23 | 平台化解决了技术平台的问题,但是每个单元业务的执行都要跨多个领域来完成,复杂度会随之升级。比如说淘宝的宝贝,商品发布规则、交易规则、营销规则散落在各个系统中,进行一个动作时,无法做到靠一个人就能说清楚全局。结果就是一个需求要评估一个月,开发需要几天,测试又需要几天,这已经不是一个流程能够解决的,是一个比较复杂的生态协作问题。 24 | 25 | “大中台”的概念就是从较为复杂的协作生态上来纵向地从服务链路来做资源整合,技术中台注重能力沉淀与整合,业务中台注重链路、效率、成本、流程优化。业务中台在我的眼里变成了规则引擎执行者与定制化服务输出方,规则的输出通过对数据的控制来进行。 26 | 27 | 大企业的很多业务在最初都会经历疯狂的扩张过程,在这个过程中由于各个 BU 自我闭环,导致大厂内部存在着很多重复造轮子的工作。比如同样在内容领域,独立 BU A 做了一款 App,独立 BU B 做了一款 App,都有很多详细功能。但是这些功能在内部的必要性又没有那么强,继续做存在着人力成本的浪费。这个时候我们会通过一个抽象出来的公共业务模块去单独处理。 28 | 29 | 虽然这个集团是某体系当中的巨无霸,但是在内容生态这块其实还是较为薄弱的,需要一个业务中台来支撑内容生态。当时的情况是:好几个事业群都有类似的生态业务在运作。比如南方某事业群有自己的图文内容生态,北方某事业群有自己的视频内容生态,各自的生态又分别为各自客户端业务提供内容生产审核,帐户体系、内容评定标准、奖励机制各不相同。具体到数据体系上,其中两个子集团或成为事业群的业务方都有各自的数据体系,造成的问题是: 30 | 31 | - 账号没有打通,账号评估与分级体系不统一; 32 | - 内容评定标准不统一、品类不统一、标签体系不统一、奖励机制各不相同; 33 | - 审核问题,但凡做内容必须有审核,不同子公司在审核的投入上都很巨大; 34 | - 采购问题,跟 BD 采购流程不同,或签约多个主体; 35 | - 内容生态所涉及到的帐户数据、图文、视频、粉丝互动、内容库、消费数据、内容审核等较容易整合并服务化。 36 | 37 | 从集团角度来看,这就形成了烟囱式的建设。每一个烟囱的能力直接决定了业务的发展速度与业务创新的成本,但实际上业务的快速更新与创新更需要像乐高一样的体系去快速搭建。结合内容生态这个业务来看,根基与出发点是偏业务型的中台建设。实际上我们可以通过一点接入、多点分发的方式来支撑各端业务,做好内容生态供给。在建设过程中对信息、标准、帐户、数据做一系列打通,将业务流、内容流、分发流、数据流、商业流这些相近的单元进行业务中台化。 38 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/大厂中台/阿里.md: -------------------------------------------------------------------------------- 1 | # 阿里巴巴中台 2 | 3 | 阿里巴巴的云发展起来,第一波是我们收了一个公司叫万网公司,做域名服务的。数字化过程中,第一步,客户至少是可以触达的。第二步在互联网、移动互联网这个大的浪潮那些增速远高于这个行业平均增速的客户,给业务带来超越正常速度的增速。 4 | 5 | 第二波是图片、视频,特别是视频的兴起,为我们业务带来了非常大的发展;有一些新的创业的风口,如果能赶上的话,也能获得非常大的发展。 6 | 7 | 第三波就是我们现在正在做的,叫传统企业的数字化;这是非常复杂的一件事情,因为它们行业高度分散。首先要想明白,虽然传统行业是一个比较值得赋能的一个行业,但是如果行业本身在下行周期,我们再多的努力帮助赋能,其实也是很难的。诸葛亮配个阿斗也是搞不起来的。所以我们要看清楚趋势,对那些有非常大的上升周期非常有前景的行业,怎么去帮助他们做赋能,做升级。 8 | 9 | 所以对外的合作时候,我们都要想一下,我们创造的价值到底是什么?我们到底提供什么样的核心价值?技术不是万能的,技术只有有力为战略服务的时候才能够发挥巨大的价值。 10 | 11 | 中台和平台是不一样的。像淘宝这种叫平台,因为你做好了之后,别人是不需要干什么事情的,只需要在上面开店。中台不一样,中台是要被集成的,中台是你的东西要变成别人的一部分,单独个体并不是一个业务,不是一个系统。互联网是一个应用行业,应用行业的业务、产品开发、技术研发,是不分的,因为它只要用成熟的技术就可以了。说实话,只有业务驱动下的产品开发,没有本质上的技术研发。 12 | -------------------------------------------------------------------------------- /03~架构设计方式/中台与平台/开放平台/README.md: -------------------------------------------------------------------------------- 1 | # 开放平台 2 | -------------------------------------------------------------------------------- /03~架构设计方式/技术团队组织/README.md: -------------------------------------------------------------------------------- 1 | # 技术团队管理 2 | -------------------------------------------------------------------------------- /03~架构设计方式/技术团队组织/管理模式/原子化管理.md: -------------------------------------------------------------------------------- 1 | # 原子化管理 2 | 3 | `员工生产力 = 能力 x 意愿 x 生产决策权` 4 | 5 | # 能力 6 | 7 | - 可进一步拆解为“专业能力”和“通用能力”。通用能力例如沟通、理解、抽象等。 8 | 9 | - 专业能力能否很好的发挥出来,还要看匹配程度。职能一般也称为岗位、Job Model,以下都称为职能。 10 | 11 | - 人的专业能力可以通过学习增长,也会随着技术本身发展、市场人才增加贬值。 12 | 13 | # 生产决策权 14 | 15 | - 这里首先要明确一下决策的概念。决策和执行本质上可以互相转换的,每个执行过程中都可能有更细节的决策,我们这里区分的标准是“需要做的事情是否会明显影响生产周期”。如果在一个生产周期能,某件要做的事情在市场上马上招到人,不会因为缺少某个执行者而明显影响生产周期,我们就称为执行。反之,需要进行学习、探索、计划,事情复杂度会明显影响生产周期的,我们就称之为决策。这样区分的原因是因为在后面讨论管理时会发现,团队的管理问题主要是随着决策者的增多而增多的。这里明确概念可以减少后面的疑惑。 16 | 17 | - 决策权是对应着决策场景产生的。例如研发中有系统运维的场景,那么就有相应的决策权。业务变多或者变复杂后决策场景会增加,对应的决策权也会对应着增加。 18 | 19 | - 决策权的类型和职能类型有绑定关系。如前端使用什么样的技术栈的决策权归属于前端工程师这个职能。和前面关联起来可以看到,决策场景、决策权、职能三者之间有对应关系。 20 | 21 | - 决策权通常会影响到利益分配。因为在常见的利益分配机制中,生产决策质量是最重要的考察因素,而能做出什么样的决策也受到决策权本身类型、范围的影响。 22 | 23 | # 意愿 24 | 25 | 需求可拆分成“物质需求”和“精神需求”。笔者尝试过用马斯洛需求层次拆成更细致的需求,后来发现对本文中的场景意义不大,总结成物质需求和精神需求已经足够。员工物质需求是否满足,又可进一步表述为在组织内分配到的利益是否满足期望。利益由利益分配机制决定。例如按一般公司中按照层级划分的薪酬机制,年终期权奖励机制等。 26 | 27 | 利益分配机制由利益分配权所有者决定。利益分配权的最终归属是公司的所有者,但通常层级产生后,会在一定的利益分配机制下授予每个层级一定的利益分配权,例如常见的主管给下级打绩效的方式。 28 | 29 | 一般利益分配机制中的主要因素有三个: 30 | 31 | 1. 其专业能力在市场中的独特程度,我们称为特异性。 32 | 2. 员工所掌握的知识、资源等对组织的重要性,我们称之为不可分离性。 33 | 3. 当前工作产出的结果。 34 | 35 | 在知识型生产中由于产出物的收益是往往延迟的、非一次性的,所以如何界定这类工作到底产生了多少价值是利益分配机制中的难题。利益可分为短期利益和长期利益,如现金工资属于短期,期权属于长期等。 36 | 37 | 通常精神需求建立在物质需求的满足的情况下。精神需求中也受到生产决策权影响。生产决策权的多少会让人感觉是否受到尊重,能力是否得以发挥等。 38 | -------------------------------------------------------------------------------- /03~架构设计方式/技术团队组织/管理模式/组织发展.md: -------------------------------------------------------------------------------- 1 | # 组织发展 2 | 3 | # 为什么要发展? 4 | 5 | 组织为什么要发展?我们可以归纳出两种情况:一、需求变多了,出现了人力问题,联系前面概念我们称为生产决策场景增多。二、需求变复杂了,出现了难度问题,需要更高级的专家帮助。我们称为生产决策复杂度变高。有意思的是,从这两个情况反过来思考,它们却不一定要通过扩张团队的方式解决,也可以通过培训员工提高员工能力的方式解决。于是我们发现在这两个情况下还要增加一个条件——时间和教育成本。如果时间对业务很重要,企业主当然会优先选择招聘能马上解决问题的人,快速创造价值。有了招聘,组织也就随之扩大了。 6 | 7 | 员工扩张后要做的第一件事就是分工。根据扩张的背景不同,分工也有两种选择。一种是做同样类型的事情,只是支持的业务不同。第二种就是做不同类型的事情,这种情况的分工我们称为职能设立。通常在生产决策复杂度变高的情况下容易出现职能设立。例如一开始团队只有一个工程师,什么事情都做,当用户访问量变大了,服务器支撑不住了,出现了运维问题,于是团队新招聘了一个运维工程师专门处理运维问题,原来的工程师则专注于研发。于是便产生了研发和运维两个职能。 8 | 9 | 按照现实中的经验,职能设立也有两种不同的方法:一种是按照生产的工序进行职能设立。例如前端、后端、测试、运维。另一种是按照具体需要的技术设立支撑型的职能,不直接参与业务。例如独立的算法,可以支撑各个职能。这两种分法背后的逻辑分别是:第一种通常是问题本身在工序上就可以分成多个部分,拆分后每个子部分的复杂度都比以前的整体复杂度来说要更小,以此来整体大问题。其中关键在于问题本身是容易拆分组装的,例如业务研发,本来就需要这些工序。对于不易拆分的,第二种就比较适用,它本质上是把问题中的瓶颈部分单独拿出来重点突破,再交由其他职能进行应用。 10 | 11 | 这两种分法并不是完全对立的,对于第一种可拆分的情况,用第二种方式其实也可以支持。例如我们还是一个职能完成所有的业务开发(全栈研发),将其中难点部分通过框架、服务化、甚至产品化的方式提供出来供其应用即可。选择第一种的情况通常是出于职能本身的知识体系、市场能提供的人才类型等因素的综合考虑。相较“支撑”这个概念,我们把直接进行业务研发的职能称为应用型职能,不直接支撑业务、而是通过技术支撑其他职能的称为支撑型职能。支撑型职能背后当然也可以继续有其他职能进行支撑。 12 | 13 | # 管理负担 14 | 15 | 现实中我们看到,对于多条业务或产品的情况,虽然可以让一人负责一个业务,但有时管理者出于经验,也会按照工序划分出流水线式的应用型职能,这可以实现资源的动态调配,也是为未来某个职能决策复杂度上升做准备。 16 | 17 | 不管是哪种情况引入了更多员工,都会给管理者带了更多的管理负担。负担可以分成两个大类: 18 | 19 | - 一是工序上有依赖关系的职能间统筹协调的负担。 20 | - 二是职能内部的负担,例如员工分工、评价生产决策质量等。 21 | 22 | 当负担达到上限时,管理者自然会想到委托部分员工协助管理。如果把对某些员工的管辖权、特别是评价生产决策质量的权利委托给某一员工,不再直接管辖时,层级便产生了。从这里开始,我们将特意地把员工担任的管理者称为中层管理者(不是传统意义上的公司高层中层概念),和组织的所有者区分开(后续称为组织所有者),后面的讨论会用到这两个不同的概念。 23 | 24 | 从前面提到的职能、生产决策权、生产决策场景的对应关系我们看到,职能的设立,本质上就是决策权的分割。分割的原因是:为了解决变复杂或变多的问题使用了分割决策场景的方式,场景分割对应出现了决策权分割。职能按照是否直接支撑业务又可分应用型职能和支撑型职能。应用型对支撑型有依赖。同时应用型职能之间往往还会有工序上的上下游依赖关系。 25 | 26 | 层级的设立,本质上对员工授予了决策权的再分配权(使他可以把任务继续拆分给其他人),和对部分员工的利益分配权(使他可以驱动这些员工)。注意这些利益分配权通常是受限的,组织所有者通常会建立一套全局的利益分配机制,中层管理者的利益分配也要受此机制制约。 27 | 28 | 在对员工分析时我们就看到,职能的匹配度会影响员工的生产力。有依赖关系、上下有关系的职能也会互相影响的决策权,进而影响生产力。更重要的是职能的决策权会间接影响员工的生产质量,进而影响员工分配到的利益。层级的设立更是会对决策权的再分配、和利益分配等对底层员工的决策权和意愿直接产生直接影响,从而影响生产力。 29 | 30 | 管理中千姿百态的问题,其实大多数都是“职能设立”和“层级设立”没做好引发的。常见的两种不良现象:一是出于过去经验或者惯性在做这两个动作,没有深入思考,常见于一些初创组织。二是有一些大组织,投入了过多的精力去强调远景,反而忽略了这两个基本动作。在下一章中,我们就会以这两个动作来演绎员工的行为,更深入地看到它们的影响。 31 | 32 | # 利益分配机制下的博弈 33 | 34 | - 管理者应当尽量设计出让员工利益与集体利益保持一致的制度,不要将员工置于需要在自我和集体中做选择的环境中。 35 | - 在不能保持利益一致的情况下,帮助管理者用风险的角度来看问题,通过补偿、惩罚等将风险控制在一定范围内。 36 | 37 | 员工的行为是为了在当前利益分配机制下为了满足自身的物质、精神需求。那么首先要理解利益分配机制中的依据。如前面提到的员工生产决策质量、员工能力在市场上的定价等。然后分析组织的变化、管理手段的实施会对这些利益分配因素产生什么样的影响,即可理解和推算员工的行为。 38 | 39 | 我们开始以职能设立和层级设立此为例。首先看利益分配机制中的因素:(这里仍是以“通常情况”为例,读者在实际思考时应该以自身所处环境为准。) 40 | 41 | - 专业能力在市场中的稀缺程度。称为特异性指标。 42 | - 员工与公司之间的不可分离程度。称为不可分离性指标。 43 | - 公司对于该能力的需求程度。 44 | - 生产决策质量。 45 | -------------------------------------------------------------------------------- /03~架构设计方式/技术团队组织/组织架构/README.md: -------------------------------------------------------------------------------- 1 | # 技术团队的组织架构 2 | 3 | # Links 4 | 5 | - https://mp.weixin.qq.com/s/lytmTzf5KVDN91XY0hYaMA 现实世界中是没有技术债管理团队这样专门修复和解决技术债的团队的。没有人愿意加入这样的队伍。这种团队每天做的事情就是给其他开发人员收拾烂摊子,谁愿意做这种事情呢? 6 | -------------------------------------------------------------------------------- /03~架构设计方式/架构可视化/C4/README.md: -------------------------------------------------------------------------------- 1 | # C4 可视化架构设计 2 | 3 | 软件系统架构设计的目标不在于设计本身,而在于架构设计意图的传达。图形化有助于在团队间进行高效的信息同步,但不同的图形化方式需要语义一致性和效率间实现平衡。C4 模型通过不同的抽象层级来表达系统的静态结构,并提供了最小集的抽象建模元素,为设计人员提供了一种低认知负载、易于学习和使用的高效建模方式。该工具的作者在多年的咨询中经常发现,很多个人画出来的架构图都是不一样的,但也不是说他们谁画错了,而是每个人的抽象层次不一样。抽象层次这种东西,说起来好像存在,但真要说清楚还挺难,于是作者类比地图,提出了缩放的概念。 4 | 5 | ![地域环境缩放](https://pic1.imgdb.cn/item/636a5dd816f2c2beb16f4301.jpg) 6 | 7 | 上面的四张地图就是想说明,当我们看待真实世界的“架构图”的时候,也是要不停的缩放,在每一个层次刻意忽略一些细节才能表达好当前抽象层次的信息。所以他类比着把架构也提出了四个抽象层次: 8 | 9 | ![C4 层次](https://pic1.imgdb.cn/item/636a5e4516f2c2beb1708a73.jpg) 10 | 11 | 从上到下依次是系统 System、容器 Container、组件 Component 和代码 Code。 12 | 13 | # 四张核心图 14 | 15 | ## 系统上下文图 16 | 17 | ![系统上下文图](https://pic1.imgdb.cn/item/636a5e8416f2c2beb1714b73.jpg) 18 | 19 | 如上图所示,这个图表达的是你所开发的系统和它的用户以及它所依赖的系统之间的关系。从这个图上我们已经看出来 C4 图形的几个关键图形: 20 | 21 | ![C4 关键图形](https://pic1.imgdb.cn/item/636a5ec016f2c2beb1720447.jpg) 22 | 23 | C4 说穿了就是几个要素:关系——带箭头的线、元素——方块和角色、关系描述——线上的文字、元素的描述——方块和角色里的文字、元素的标记——方块和角色的颜色、虚线框(在 C4 里面虚线框的表达力被极大的限制了,我觉得可以给虚线框更大的扩展空间)通过在不同的抽象层次上,重新定义方块和虚线框的含义来限制我们只能在一个抽象层次上表达,从而避免在表达的时候产生抽象层次混乱的问题。 24 | 25 | 那么在系统上下文图里,方块指代的是软件系统,蓝色的表示我们聚焦的系统,也就是我开发的系统(也可能是我分析的系统,取决于我是谁),灰色表示我们直接依赖的系统,虚线框表示的是企业的边界。通过这些图形化的元素表达我们可以看出来各个系统彼此之间的关系。 26 | 27 | ## 容器图 28 | 29 | ![容器图](https://pic1.imgdb.cn/item/636a602516f2c2beb17741c2.jpg) 30 | 31 | 当我们放大一个系统,就会看到容器,如上图所示,C4 模型认为系统是由容器组成的。容器是我个人认为,C4 模型最大的创举,尤其是在这个单体架构快速崩塌的时代。所谓容器,既不是 Docker 的容器,也不是 JavaEE 里的容器,是借用了进程模型,每一个容器都是指有自己独立的进程空间的一种存在。不管是在服务器上的单独进程空间,还是在浏览器里的单独进程空间,只要是单独的进程空间就可以看作一个容器。当然如果你容器化做得好,Docker 的 Container 和这个 Container 可以一一对应。有了这个概念的存在我们就可以更清晰的去表达我们的架构,而不是总是用一些模糊的东西。 32 | 33 | ## 组件图 34 | 35 | ![组件图](https://pic1.imgdb.cn/item/636a606516f2c2beb1780cfa.jpg) 36 | 37 | 当我们放大一个容器,我们就会看到组件,如上图所示。组件在这里面很好的把接口和它的实现类打包成一个概念来表达关系。我个人觉得有时候一些存在于代码中,但又不是接口的某些东西,比如 Service、Controller、Repository 之类也可以用组件图来表达,如果你学了一些没有明确抽象层次的架构知识或者一些单体时代的遗留经验的时候,你可以画出来一些组件图,来印证自己的理解,如下图,我画的我对 DDD 战术设计里面的一些概念的理解: 38 | 39 | ![组件图](https://pic1.imgdb.cn/item/636a610b16f2c2beb17a5b7e.jpg) 40 | 41 | ## 代码图 42 | 43 | 代码图没什么可说的,就是 UML 里的类图之类很细节的图。一般是不画的,都是代码生成出来。除非非常重要的且还没有写出代码的组件才画代码图。 44 | 45 | 以上就是 C4 的核心图,我们可以看到四种不同的抽象层次的定义会让我们更容易固定住我们讨论的层次,这点上我觉得 C4 是非常有价值的。 46 | 47 | # 三张扩展图 48 | 49 | 架构设计设计要考虑的维度很多,仅四张核心图是不够的,所以作者又提供了三张扩展图,可以让我们关注更多的维度。 50 | 51 | ## 系统景观图 52 | 53 | ![系统景观图](https://pic1.imgdb.cn/item/636a623916f2c2beb17f210a.jpg) 54 | 55 | 和它的直接关系,连一些间接相关的系统都会标示出来,那些系统的用户以及用户之间的关系也会标示出来,只是内部的用户会用灰色标记。 56 | 57 | 这个图有什么用呢?在我们分析一个企业的时候,我们需要一个工具帮助我们把一家公司给挖个底掉,做到完全穷尽,才能看到企业的全景图从而理解局部的正确定位以做好局部设计为全局优化服务。之前我试过以四色建模的红卡、事件风暴的事件两种工具教人去掌握这种能力,当学员是程序员的时候都无法快速掌握这种顺藤摸瓜的分析技巧,毕竟跟程序员的思维还是有些差异的。但是我用了系统景观图之后,学员毫不费力的就掌握了这种分析能力。所以我后来都是用这个图来教程序员探索企业的数字化全景图,效果极好,推荐给大家。 58 | 59 | ## 动态图 60 | 61 | ![动态图](https://pic1.imgdb.cn/item/636a629716f2c2beb1808aa1.jpg) 62 | 63 | 动态图不同于其他图都是表达静态关系的,它是用来表达动态关系的,也就是不同的元素之间是如何调用来完成一个业务的。所以动态图不仅仅在一个层面上可以工作,它在系统级、容器级和组件级都可以画,表达的目标是不一样的。 64 | 65 | 我之前曾经写过名为《像机器一样思考》的一系列文章,里面我也发明了类似的图,不同于他的关系线上标注的是调用的方法、函数,我更关注的是数据,使用效果也很好。 66 | 67 | 什么时候是用动态图呢?举个小例子,我之前做一个内部的小系统,只有一个有经验的工程师带着 10 多个毕业生,我便要求他们在开始工作之前都画出动态图来,交由有经验的工程师去评估他们的思路是否正确,如果有问题,就在开始之前就扼杀掉了烂设计。不管是毕业生还是初级工程师,改代码的能力都比写代码的能力要差很多,所以将烂设计扼杀在实现之前还是有帮助的。 68 | 69 | ## 部署图 70 | 71 | ![部署图](https://pic1.imgdb.cn/item/636a62c216f2c2beb181284d.jpg) 72 | 73 | 前面的几张图都是站在开发的角度思考,但是一个没有充分思考过部署的架构很容易变成一个运维的灾难。所以作者提供了一个部署图。考虑到 DevOps 运动如火如荼,这个图可以变成很好的 Dev 和 Ops 之间沟通的桥梁。我们在实操中发现,Dev 和 Ops 关注点的不同、语言的不一致,在这张图上表现得非常清楚。 74 | 75 | 图上最大的的实线框不同于虚线框,它表达的是数据中心,当你开始考虑异地载备的时候它就有了意义。数据的同步、实例的数量都会影响你部署图的内容。部署图基本都是容器级的,它会很好的表达出来容器到底部署了几个实例,部署在什么样的操作系统上,一个节点部署了几个容器之类,我们在实际使用中,发现需要考虑的信息太多,自己就抽象出了类似于亚马逊上实例规格的 Small、Large 之类的术语来表达机器配置,增进了开发和运维之间的交流准确性。 76 | 77 | # Links 78 | 79 | - https://www.jianshu.com/p/1e496225b6b6 80 | -------------------------------------------------------------------------------- /03~架构设计方式/架构可视化/C4/架构可视化的坏味道.md: -------------------------------------------------------------------------------- 1 | # 架构可视化的坏味道 2 | 3 | # Links 4 | 5 | - https://www.jianshu.com/p/f16aae86713a 架构可视化的坏味道 6 | -------------------------------------------------------------------------------- /03~架构设计方式/架构可视化/README.md: -------------------------------------------------------------------------------- 1 | # 软件架构可视化 2 | 3 | 软件系统架构设计的目标不在于设计本身,而在于架构设计意图的传达;系统架构图是为了抽象的表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。如果不能清晰、一致的在干系人间进行设计意图的同步,即使再好的设计也只是空中楼阁。软件架构设计本质上也是一种抽象和建模的过程,软件架构设计模型的表达有多种方式:图形化、语言和文字。绝大部分场景下,图形化在架构设计的表现力层面效果更佳。因此,对于软件系统架构进行可视化表达是有价值,且是必要的:解决沟通障碍、达成共识、减少歧义。 4 | 5 | 软件架构可视化的方式有多种,不同的团队有不同的实践方式,最为常见的由如下几种: 6 | 7 | - 线框图:通过线框图和连线表达架构元素及之间的关系 8 | - UML:统一建模语言,表达系统的静态结构和动态行为 9 | - 草图:非正式的图形 10 | 11 | 不同的可视化方式各有优劣,以下部分将对不同的表现形式进行说明 12 | -------------------------------------------------------------------------------- /03~架构设计方式/架构可视化/techtribesjs.puml: -------------------------------------------------------------------------------- 1 | @startuml "techtribesjs" 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 3 | ' uncomment the following line and comment the first to use locally 4 | ' !include C4_Container.puml 5 | 6 | LAYOUT_TOP_DOWN() 7 | 'LAYOUT_AS_SKETCH() 8 | LAYOUT_WITH_LEGEND() 9 | 10 | 11 | Person_Ext(anonymous_user, "Anonymous User") 12 | Person(aggregated_user, "Aggregated User") 13 | Person(administration_user, "Administration User") 14 | 15 | System_Boundary(c1, "techtribes.js"){ 16 | 17 | Container(web_app, "Web Application", "Java, Spring MVC, Tomcat 7.x", "Allows users to view people, tribes, content, events, jobs, etc. from the local tech, digital and IT sector") 18 | 19 | ContainerDb(rel_db, "Relational Database", "MySQL 5.5.x", "Stores people, tribes, tribe membership, talks, events, jobs, badges, GitHub repos, etc.") 20 | 21 | Container(filesystem, "File System", "FAT32", "Stores search indexes") 22 | 23 | ContainerDb(nosql, "NoSQL Data Store", "MongoDB 2.2.x", "Stores from RSS/Atom feeds (blog posts) and tweets") 24 | 25 | Container(updater, "Updater", "Java 7 Console App", "Updates profiles, tweets, GitHub repos and content on a scheduled basis") 26 | } 27 | 28 | System_Ext(twitter, "Twitter") 29 | System_Ext(github, "GitHub") 30 | System_Ext(blogs, "Blogs") 31 | 32 | 33 | Rel(anonymous_user, web_app, "Uses", "HTTPS") 34 | Rel(aggregated_user, web_app, "Uses", "HTTPS") 35 | Rel(administration_user, web_app, "Uses", "HTTPS") 36 | 37 | Rel(web_app, rel_db, "Reads from and writes to", "SQL/JDBC, port 3306") 38 | Rel(web_app, filesystem, "Reads from") 39 | Rel(web_app, nosql, "Reads from", "MongoDB wire protocol, port 27017") 40 | 41 | Rel_U(updater, rel_db, "Reads from and writes data to", "SQL/JDBC, port 3306") 42 | Rel_U(updater, filesystem, "Writes to") 43 | Rel_U(updater, nosql, "Reads from and writes to", "MongoDB wire protocol, port 27017") 44 | 45 | Rel(updater, twitter, "Gets profile information and tweets from", "HTTPS") 46 | Rel(updater, github, "Gets information about public code repositories from", "HTTPS") 47 | Rel(updater, blogs, "Gets content using RSS and Atom feeds from", "HTTP") 48 | 49 | Lay_R(rel_db, filesystem) 50 | 51 | @enduml -------------------------------------------------------------------------------- /03~架构设计方式/架构可视化/架构图分类.md: -------------------------------------------------------------------------------- 1 | # 架构图分类 2 | 3 | # 线框图 4 | 5 | 线框图是最为通用的可视化表达方式之一,架构师或设计人员大量的架构图,比如技术架构、功能架构、数据架构、逻辑架构等等都通过线框图的形式表达。该种可视化方式的优势是: 6 | 7 | - 建模工具多样化:你可以基于 Viso、Drawio、PPT 等任何一款支持线框图的软件进行建模工作 8 | - 学习成本低:设计人员几乎不需要进行专门的建模语言以及建模工具的学习,门槛较低 9 | 10 | 但,基于线框图表达软件系统架构存在的问题也非常明显:语义的一致性问题。你可能自己画过很多软件系统架构图,也可能参与评审过其他团队的架构图,我相信,对你而言并不是的所有的图都是“清晰且易于理解的”。举个简单的场景,如果我们在百度搜索 “架构图” ,你可能得到以下结果: 11 | 12 | ![各式各样架构图](https://pic1.imgdb.cn/item/636a5c2816f2c2beb16ab3dc.jpg) 13 | 14 | 不同形状和颜色的图形元素、不同形状和颜色的连线、不同的意图。我们可以看出:线框图虽然简单,但其其图形化的语义一致性是大问题。虽然都是通过线框表达软件系统架构,但不同的人可能使用不同的元素、不同的颜色、不同的连线和分层等等,线框自由表达的灵活性和图形化语义的一致性存在潜在冲突,最终都会阻碍架构设计意图传达。 15 | 16 | # UML 17 | 18 | UML 是统一建模语言,相比于线框图而言,其优势是在软件建模层面提供了一致性的建模元语言。简单来说,UML 提供了大家达成一致的(UML 支持扩展的场景除外)建模元素。如果团队成员比较熟悉 UML,那么通过 UML 表达的系统架构图天然具有认知一致性。丰富灵活的建模元语言在提升语义一致性的同时,也必然会导致复杂性的上升。掌握 UML 具有一定的学习成本,而熟练的应用对研发人员也提出了更高的要求。基于 Simon Brown 给出的数据,实际情况只有少数团队真正使用 UML。不论是 UML 的复杂度和学习成本原因,还是敏捷化下对 UML 的排斥,很多团队都放弃了 UML。 19 | 20 | 我们不能否认 UML 的价值,基于统一建模语言能够更有效的进行架构设计的信息传递和沟通,也能基于 UML 提供的详细的模型图元素进行充分的设计表达。团队中是否要基于 UML 进行沟通需要权衡,虽然 UML 不能表达你所要传达的全部的架构信息,但其在某些维度的表达相对比较适合。 21 | 22 | - 表达流程和工作流可以采用 UML 活动图 23 | - 表达运行时的交互可以采用 UML 时序图 24 | - 表达领域模型或者设计模式可以采用 UML 类图 25 | - 表达状态转换可以采用 UML 状态机 26 | - 表达系统的部署结构可以使用 UML 部署图 27 | 28 | # 草图 29 | 30 | 架构可视化另一个非常常见的方式是:草图。草图是一种非正式的、易于快速沟通的图形化方式。团队基于特定的场景,可以通过草图的形式进行快速的沟通,以便高效的在干系人间拉齐关键信息。但,草图的劣势与线框图一样:语义一致性低。 31 | 32 | 我们可以在白板上 “随心所欲” 的画各式各样的草图,草图上的元素、连线,又或者布局都可能是涌现式的、临时性的,这些草图的价值在于 “会话周期内的高效沟通”。如果干系人没有完全参与到草图的讨论,又或是后置查看,大概也很难精准捕获这些草图所要表达的设计意图。 33 | 34 | ![C4](https://pic1.imgdb.cn/item/636a5cfb16f2c2beb16c91a2.jpg) 35 | 36 | # 场景视图 37 | 38 | 场景视图用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示。 39 | 40 | # 逻辑视图 41 | 42 | # Links 43 | 44 | - https://parg.co/8mN 45 | - https://mp.weixin.qq.com/s/qPcZSA-kKZhFzmZeNBPaTg 46 | - https://mp.weixin.qq.com/s/0gghnrdaQgOgRmciSOraGw 47 | -------------------------------------------------------------------------------- /03~架构设计方式/架构可视化/流程图/Flowchart.md: -------------------------------------------------------------------------------- 1 | # Flowchart 2 | 3 | 流程图的语法大致分为两部分,定义元素与连接元素。定义元素即: 4 | 5 | ```s 6 | tag=>type: content:>url 7 | ``` 8 | 9 | tag 标签,用于连接元素时使用;type 即该标签的类型。共有 6 种类型如下: 10 | 11 | ```s 12 | start 13 | end 14 | operation 15 | subroutine 16 | condition 17 | inputoutput 18 | ``` 19 | 20 | content 指流程语句中放置的内容,注意 `type:` 与 content 之间一定要有一个空格。url 则表示链接,与流程语句绑定。连接元素则主要是 `->` 符号,如: 21 | 22 | ```s 23 | c2(yes)->io->e 24 | c2(no)->op2->e 25 | ``` 26 | 27 | # 实例 28 | 29 | ```s 30 | st=>start: Start|past:>http://www.baidu.com 31 | e=>end: End:>http://www.baidu.com 32 | op1=>operation: My Operation|past 33 | op2=>operation: Stuff|current 34 | sub1=>subroutine: My Subroutine|invalid 35 | cond=>condition: Yes or No?|approved:>http://www.baidu.com 36 | c2=>condition: Good idea|rejected 37 | io=>inputoutput: catch something...|request 38 | 39 | st->op1(right)->cond 40 | cond(yes, right)->c2 41 | cond(no)->sub1(left)->op1 42 | c2(yes)->io->e 43 | c2(no)->op2->e 44 | ``` 45 | 46 | ![示意图](https://assets.ng-tech.icu/item/20230427200554.png) 47 | 48 | ```s 49 | st=>start: Start 50 | e=>end: End 51 | cond=>condition: Option 52 | op1=>operation: solution_1 53 | op2=>operation: solution_2 54 | 55 | st->cond 56 | cond(yes)->op1->e 57 | cond(no)->op2->e 58 | ``` 59 | 60 | ![Flowchart 示意图](https://assets.ng-tech.icu/item/20230427200534.png) 61 | 62 | ```s 63 | st=>start: Start 64 | e=>end: Why are you worried? 65 | cond1=>condition: Do you have a problem? 66 | cond2=>condition: Can you solve it? 67 | op=>operation: Since you can't solve it, 68 | 69 | st->cond1 70 | cond1(yes)->cond2 71 | cond1(no)->e 72 | cond2(yes)->e 73 | cond2(no)->op->e 74 | ``` 75 | 76 | ![Flowchart 示意图](https://assets.ng-tech.icu/item/20230427200517.png) 77 | -------------------------------------------------------------------------------- /03~架构设计方式/架构域与推导/README.md: -------------------------------------------------------------------------------- 1 | # 架构域划分 2 | 3 | 在笔者的知识体系中,实际上将架构分为业务架构、应用架构、云基础架构这几大类,业务架构主要着眼于控制业务的复杂性,基础架构着眼于解决分布式系统中存在的一系列问题。无论何种架构,都希望能实现系统的可变的同时保障业务的高可用。 4 | 5 | # 架构域迭代与演化 6 | 7 | 对复杂的系统,特别是前人没有做过的新系统,通常难以一下子设计出合适的架构。在架构设计的初期,通常都要经历一个不断探索的阶段。在架构设计过程中,架构分解是必不可少的关键步骤。如何进行架构分解,从哪里入手开始进行分解?这些需要一套架构分解的过程模型和过程方法来指导分解。每个架构域的分解过程,都是一个迭代过程。从无到有、从粗到细、从模糊到清晰,一步步精细化、丰富架构。迭代的过程就是一个否定之否定的过程,随着分解的逐步推进或系统的架构演化,后面的分解除了会识别出新的架构元素,也可能会对先前识别出的架构作出调整。整个架构分解的迭代过程,通过画架构图的方式是种非常直观的表现形式。 8 | 9 | ![架构域迭代演化](https://s2.ax1x.com/2019/09/11/nwffq1.png) 10 | -------------------------------------------------------------------------------- /03~架构设计方式/架构域与推导/业务模型推导.md: -------------------------------------------------------------------------------- 1 | # 自顶向下构建架构 2 | 3 | 在架构设计中,往往提出问题难于解决问题,Brooks 的《The design of design》中介绍到:The hardest part of design is deciding what to design. 团队中常见典型矛盾的就是产品团队和研发团队的矛盾。作为研发团队,我们常吐槽产品团队的需求不合理的时候,不懂技术等。其实我们可以试想把自己的工作在往前移一下,不仅仅是去设计架构实现产品的需求,而是去实现客户的需求,甚至发现潜在的需求。这时我们就变成了在设计上提出问题的人,你会发现其实提出问题,很多时候需要同样深入的思考。设计一个好的问题,甚至比解决问题更难。 4 | 5 | 首先定义问题,而定义问题中最重要的是定义客户的问题。定义问题,特别主要识别出关键问题,关键问题是对客户有体感,能够解决客户痛点,通过一定的数据化来衡量识别出来,关键问题要优先给出解决方案。问题定义务必加入时间维度,把手段/方案和问题定义区分开来;需要对问题进行升层思考后再进行升维思考,从而真正抓到问题的本质,理清和挖掘清楚需求;要善用第一性原理思维进行分析思考问题。 6 | 7 | 问题解决原则:先解决客户的问题(使命),然后才能解决自己的问题(愿景);务必记住不是强调我们怎么样,而是我们能为客户具体解决什么问题,然后才是我们变成什么,从而怎么样去更好得服务客户。我们应当善用多种方法对客户问题进行分析,转换成我们产品或者平台需要提供的能力,比如仓储系统 WMS 可以提供哪些商业能力。 8 | 9 | 对我们的现有的流程和能力模型进行梳理,找到需要提升的地方,升层思考和升维思考真正明确提升部分。定义指标,并能够对指标进行拆解,然后进行数学建模,将抽象出来的能力诉求转换成技术挑战,此步对于技术人员来说相当于找到了靶子,可以进行方案的设计了,需要结合自底向上的架构推导方式。 10 | 11 | 创新可以是业务创新,也可以是产品创新,也可以是技术创新,也可以是运营创新,升层思考、升维思考,使用第一性原理思维、生物学(进化论--进化=变异+选择+隔离、熵增定律、分形和涌现)思维等哲科思维可以帮助我们在业务,产品,技术上发现不同的创新可能。可以说哲科思维是架构师的灵魂思维。 12 | 13 | ![](https://assets.ng-tech.icu/item/20230427181904.png) 14 | 15 | # 自底向上推导应用架构 16 | 17 | 先根据业务流程,分解出系统时序图,根据时序图开始对模块进行归纳,从而得到粒度更大的模块,模块的组合/聚合构建整个系统架构。基本上应用逻辑架构的推导有 4 个子路径,他们分别是: 18 | 19 | - 业务概念架构:业务概念架构来自于业务概念模型和业务流程。 20 | - 系统模型:来自于业务概念模型。 21 | - 系统流程:来自业务流程。 22 | - 非功能性的系统支撑:来自对性能,稳定性,成本的需要。 23 | 24 | 效率,稳定性,性能是最影响逻辑架构落地成物理架构的三大主要因素,所以从逻辑架构到物理架构,一定需要先对效率、稳定性和性能做出明确的量化要求。如果是产品方案已经明确,程序员需要理解这个业务需求,并根据产品方案推导出架构,此时一般使用自底向上的方法,而领域建模就是这种自底向上的分析方法。 25 | 26 | 自底向上重度依赖于演绎和归纳,演绎,演绎就是逻辑推导,越是底层的,越需要演绎: 27 | 28 | - 从用例到业务模型就属于演绎 29 | - 从业务模型到系统模型也属于演绎 30 | - 根据目前的问题,推导出要实施某种稳定性措施,这是也是演绎 31 | 32 | 归纳,这里的归纳是根据事物的某个维度来进行归类,越是高层的,越需要归纳: 33 | 34 | - 问题空间模块划分属于归纳 35 | - 逻辑架构中有部分也属于归纳 36 | - 根据一堆稳定性问题,归纳出,事前,事中,事后都需要做对应的操作,是就是根据时间维度来进行归纳。 37 | -------------------------------------------------------------------------------- /03~架构设计方式/架构域与推导/架构域划分.md: -------------------------------------------------------------------------------- 1 | # 业务架构/解决方案架构 2 | 3 | 核心是解决业务带来的系统复杂性,了解客户/业务方的痛点,项目定义,现有环境;梳理高阶需求和非功能性需求,进行问题域划分与领域建模等工作;沟通,方案建议,多次迭代,交付总体架构。 4 | 5 | 业务架构就是在业务需求初期,将模糊的需求描述转变成清晰的问题域,梳理出清晰的业务流程,为产品架构提供输入。问题域是指自己的产品能够解决的所有问题的空间集合。从核心需求出发,将所有当前需要解决、未来可能要解决的问题放入产品框架的范围。能够帮助我们的产品拥有更高的可拓展性,在后续具备迭代和优化的空间。在经过问题域的罗列后,我们应该能够得到一个模糊的产品方向和功能范围。把这些问题域的答案抽象总结成一个确定的产品需求。根据核心需求和问题域的答案,梳理出业务流程。 6 | 7 | 业务架构包括业务规划、业务模块、业务流程,对整个系统的业务进行拆分,对领域模型进行设计,把现实的业务转化成抽象对象。没有最优的架构,只有最合适的架构,一切系统设计原则都要以解决业务问题为最终目标,脱离实际业务的技术情怀架构往往是空中楼阁。业务架构必须与其面向的实际应用场景相匹配,由于每个产品或项目的业务场景均有所不同,所以每次做新的软件开发前,必须设计软件架构。试图不经分析直接套用先前的架构方案,十有八九会让当前的系统在某个点上报出大问题导致推翻重建,更不要说直接拿别人的现成架构方案了。 8 | 9 | 例如,A 业务中有套 A 系统,恰巧 B 业务需要解决类似 A 业务的场景。此时很多情况,B 业务的人员会考虑把 A 系统直接拿过来,以为做一些简单的修改,就能在 B 业务中落地。结果在系统落地的过程中,很多功能模块不能直接使用,都要重新按照业务进行修改。最终的结果是,A 系统经过不断的重写修改变成了 B 系统。上述的案例正是由于业务架构没有做到位,没有做好软件架构的分析和设计,所以我们很难看出两个系统有多少差别,也无法确定用一个业务系统去覆盖另一个业务系统的可行性有多大。相反,对 A 和 B 业务领域进行业务架构梳理,我们就能清楚发现两者的一致与区别,就能有效的评估系统覆盖的可行性和合理性。 10 | 11 | 经过业务架构阶段之后,需要输出的产物包括:企业战略方向图、问题域列表、业务流程图。 12 | 13 | # 应用架构 14 | 15 | 根据业务场景的需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性要求(性能、安全、稳定性等)。应用架构自顶向下又会分别着眼于产品的功能模块划分、应用系统的划分以及不同功能模块、子系统所需要的技术选择。 16 | 17 | 功能模块是用户能够完成一个操作的最小粒度的完整功能。比如一个展示可购买商品的列表页、一个修改用户密码的功能。在功能模块设计过程中,需要确保用户能通过一个功能模块完整的完成一项工作,而不是半个工作。从产品的角度来看,应用架构就是将这些不同用途的功能模块围绕特定的业务目标进行分类整合。功能模块是根据其相互之间的关系来组织的。一个产品中不同的功能模块之间的关系分直接关系和间接关系。只有直接关系的功能模块才会被组织到一起,形成一个子系统。那些存在间接关系的模块,会在不同的层级通过直接关系的模块产生联系。当具有直接关系的功能模块组合成一个子系统后,解决相同问题域的子系统就形成一个功能层级。功能层级按照接近用户实操的距离程度进行从上到下,或者从左至右的划分,这就形成了应用架构的功能分层。 18 | 19 | 应用系统的划分着眼于实现我们的功能模块应该分哪些应用系统,应用系统间是如何集成的。在业务架构的基础上,按照解决的业务问题域,划分出不同的功能模块,再根据功能模块间的关系,组合成子系统。应用架构在产品架构的基础上考虑两个事情:第一、考虑的是子系统间的关系。第二、考虑将可复用的组件或模块进行下沉,沉淀到平台层,为业务组件提供统一的支撑。 20 | 21 | 应用的技术架构是应接应用架构的技术需求,并根据识别的技术需求,进行技术选型,把各个关键技术和技术之间的关系描述清楚。技术架构解决的问题包括:如何进行纯技术层面的分层、开发框架的选择、开发语言的选择、涉及非功能性需求的技术选择。由于应用架构体系是分层的,那么对于的技术架构体系自然也是分层的。大的分层有微服务架构分层模型,小的分层则是单个应用的技术分层框架。大的技术体系考虑清楚后,剩下的问题就是根据实际业务场景来选择具体的技术点。各个技术点的分析、方案选择,最终形成关键技术清单,关键技术清单考虑应用架构本身的分层逻辑,最终形成一个完成的技术架构图。 22 | 23 | # 数据架构 24 | 25 | 专注于构建数据中台,统一数据定义规范,标准化数据表达,形成有效易维护的数据资产。打造统一的大数据处理平台,包括数据可视化运营平台、数据共享平台、数据权限管理平台等。 26 | 27 | 企业架构由业务架构驱动,从业务架构分析业务流程、定义数据架构,流程和数据结合定义产品架构。这中间,数据架构起着至关重要的作用。企业 IT 系统的价值并不在于选取的技术有多先进,使用的硬件有多强大。而是企业业务数据的处理和存储。一家公司最宝贵的资产无疑就是数据。毫无疑问,在当今大数据的时代背景下,缺少数据资产的建设和使用,就失去与同行业争夺竞争的机会。 28 | 29 | 数据架构主要解决三个问题: 30 | 31 | - 系统需要什么样的数据:数据是对客观事物的真实表现,企业业务过程中的所有对象的状况都可以用数据记录下来。业务运营过程中有两条重要的线索:流程和数据。业务流程离不开数据流转,业务运营状况通过数据反映。基于业务架构的端到端的流程建模过程中,会衍生出对应的业务数据对象,需要与数据架构模型对接。流程模型和数据模型对接后落实到应用层面,就形成了产品架构。数据架构中的数据包含静态数据和动态数据。相对静态部分如元数据、业务对象数据模型、主数据、共享数据。相对动态部分如数据流转、ETL、数据全生命周期管控治理。 32 | 33 | - 如何存储这些数据:数据架构是为了建立一个共享、通用、一致的数据基础平台,解决企业信息孤岛。如何存储业务数据,需要结合自身需求,采取合适的数据分布策略。通常,数据存储的分布策略有两种:一种是集中式存储,一种是分布式存储。集中式存储就是讲数据集中存放于总部数据中心,所有的下属机构或子公司不放置和维护数据,都想总部数据中心进行访问。分布式存储就是数据分布存放于总部、分支机构或者子公司,每个分布节点需要维护和管理自己的数据。分布式的数据存储架构中,还需要考虑每个分布式节点的数据与总部节点数据进行同步、备份,做到数据资产的安全、可靠。 34 | 35 | - 如何进行数据架构设计:数据源自于企业的业务流程,从业务流程中我们可以找出领域对象,基于领域对象进行分析,就能得到对象的属性。根据业务关系进而抽取领取对象之间的关系。因此,领域建模是一种对数据架构很有帮助的建模思想。通过领域建模,我们不仅能清晰的反映企业的业务域,还能清晰的描绘出一幅企业的数据模型。数据模型最常用的视图就是 ER 图,它主要描述企业数据实体、属性和关系。 36 | 37 | # 中间件架构 38 | 39 | 专注于中间件系统的构建,需要解决服务器负载,分布式服务的注册和发现,消息系统,缓存系统,分布式数据库等问题,同时架构师要在 CAP 之间进行权衡。 40 | 41 | # 运维架构 42 | 43 | 负责运维系统的规划、选型、部署上线,建立规范化的运维体系。 44 | 45 | # 物理架构 46 | 47 | 物理架构关注软件元件是如何放到硬件上的,专注于基础设施,某种软硬件体系,甚至云平台,包括机房搭建、网络拓扑结构,网络分流器、代理服务器、Web 服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。 48 | -------------------------------------------------------------------------------- /03~架构设计方式/架构描述框架/DODAF.md: -------------------------------------------------------------------------------- 1 | # DODAF 2 | 3 | DODAF 是美国国防部架构框架,是一个控制“EA 开发、维护和决策生成”的组织机制,是统一组织“团队资源、描述和控制 EA 活动”的总体结构。 4 | 5 | DODAF 涵盖 DoD 的所有业务领域,定义了表示、描述和集成 DoD 范围内众多架构的标准方法,确保架构描述可比较、评估,提供了对 FoS (系统族)和 SoS(体系)进行理解、比较、集成和互操作共同的架构基础,提供开发和表达架构描述的规则和指南,但不指导如何实现。 6 | 7 | DODAF 核心是 8 个视点和 52 个模型。 8 | 9 | ![](https://assets.ng-tech.icu/item/20230427181840.png) 10 | -------------------------------------------------------------------------------- /03~架构设计方式/架构描述框架/ITSA.md: -------------------------------------------------------------------------------- 1 | # ITSA 2 | 3 | ITSA 诞生于 1986 年的惠普,世界最早的企业架构框架(IT 战略与架构)。建模原则就是“Everything you need, and nothing you don’t”-只放你要的东西。 4 | -------------------------------------------------------------------------------- /03~架构设计方式/架构描述框架/README.md: -------------------------------------------------------------------------------- 1 | # TOGAF 2 | 3 | TOGAF 是 The Open Group Architecture Framework 的缩写,它由 The Open Group 开发,The Open Group 是一个非盈利的技术行业联盟,它不断更新和重申 TOGAF。 4 | 5 | TOGAF 强调商业目标作为架构的驱动力,并提供了一个最佳实践的储藏库,其中包括 TOGAF 架构开发方法(ADM)、TOGAF 架构内容框架、TOGAF 参考模型、架构开发方法(ADM)指引和技术、企业连续统一体和 TOGAF 能力框架。 6 | 7 | # ADM 8 | 9 | ADM 是一个迭代的步骤顺序以发展企业范围的架构的方法。 10 | 11 | ![](https://assets.ng-tech.icu/item/20230427181823.png) 12 | -------------------------------------------------------------------------------- /03~架构设计方式/架构描述框架/TOGAF.md: -------------------------------------------------------------------------------- 1 | # TOGAF 2 | 3 | TOGAF 是 The Open Group Architecture Framework 的缩写,它由 The Open Group 开发,The Open Group 是一个非盈利的技术行业联盟,它不断更新和重申 TOGAF。 4 | 5 | TOGAF 强调商业目标作为架构的驱动力,并提供了一个最佳实践的储藏库,其中包括 TOGAF 架构开发方法(ADM)、TOGAF 架构内容框架、TOGAF 参考模型、架构开发方法(ADM)指引和技术、企业连续统一体和 TOGAF 能力框架。 6 | 7 | # ADM 8 | 9 | ADM 是一个迭代的步骤顺序以发展企业范围的架构的方法。 10 | 11 | ![](https://assets.ng-tech.icu/item/20230427181737.png) 12 | -------------------------------------------------------------------------------- /03~架构设计方式/架构描述框架/TOGAF/README.md: -------------------------------------------------------------------------------- 1 | # TOGAF 2 | 3 | TOGAF 是 The Open Group Architecture Framework 的缩写,它由 The Open Group 开发,The Open Group 是一个非盈利的技术行业联盟。 4 | 5 | ![](https://assets.ng-tech.icu/item/20230427181632.png) 6 | 7 | # 背景 8 | 9 | 作为计算机体系结构的一个子集,企业架构作为一个领域可以追溯到 20 世纪 60 年代中期。IBM 在其他公司和大学中率先采用了一些明确的方式来构建企业架构,因为知道在网络上运行的所有部分都很复杂。 10 | 11 | 在接下来的几十年中,技术只会变得更加复杂:今天,大多数公司,无论规模大小或产品,都利用互联网使其业务流程更简单,更快捷,有时更透明。今天,企业体系结构是理解各种硬件和软件选项的必要过程,可以在内部部署和云中部署,并确保跨多个平台共享数据时的安全性。 12 | 13 | TOGAF 最初是在 1995 年开发的。到目前为止在企业架构领域很常见,新版本或模型提供了改进的迭代和理论。同样,TOGAF 从美国国防部自己的 EAF 中获得了很多灵感,被称为信息管理技术架构框架(简称 TAFIM)。有趣的是,USDoD 在 TOGAF 出现的几年内停止使用 TAFIM。尽管如此,20 多年后的今天,TOGAF 的实施和成功仍在全球范围内延续。 14 | 15 | # 方法 16 | 17 | Open Group 将 TOGAF 定义为`企业架构的全球标准`,该框架旨在通过四个目标帮助企业组织和解决所有关键业务需求: 18 | 19 | - 确保从关键利益相关方到团队成员的所有用户都使用相同的语言。这有助于每个人以相同的方式理解框架,内容和目标,并让整个企业在同一页面上打破任何沟通障碍。 20 | - 避免被“锁定”到企业架构的专有解决方案。只要该公司在内部使用 TOGAF 而不是用于商业目的,该框架就是免费的。 21 | - 节省时间和金钱,更有效地利用资源。 22 | - 实现可观的投资回报(ROI)。 23 | 24 | # 三大支柱 25 | 26 | 如果四个目标是使用 TOGAF 的理论结果,那么三个支柱就是实现目标的方法。这些支柱有助于创建一个系统化的流程,以组织和使软件技术以与治理和业务目标相一致的结构化方式使用。由于软件开发依赖于 IT 内外不同业务部门之间的协作,因此 TOGAF 使用同一种语言的目标鼓励并协助各个利益相关方进入同一页面,这在商业环境中可能不会发生。 27 | 28 | ## 企业架构域 29 | 30 | 这些将整个企业划分为四个关键领域,有时缩写为 BDAT 领域: 31 | 32 | - 业务架构,定义业务战略和组织,关键业务流程以及治理和标准。 33 | - 应用程序体系结构,为部署各个系统提供蓝图,包括应用程序系统之间的交互以及与基本业务流程的关系。 34 | - 数据架构,记录逻辑和物理数据资产的结构以及任何相关的数据管理资源。 35 | - 技术架构(也称为技术架构),它描述了支持关键任务应用部署所需的硬件,软件和网络基础架构。 36 | 37 | ## 架构开发模型(ADM) 38 | 39 | 这个迭代循环使用性能工程来开发实际的企业架构。重要的是,它可以根据企业的需求进行定制,因此它不是一种万能的方法。一旦架构开发出来,企业就可以将其推广到所有团队或部门进行反复循环,从而确保最小的错误,并进一步帮助公司进行紧密联系。 40 | 41 | ## 企业连续 42 | 43 | 此分类系统跟踪范围内的体系结构解决方案,从通用的行业标准选项开始,并包括定制的企业特定解决方案。 44 | -------------------------------------------------------------------------------- /03~架构设计方式/架构描述框架/Zachman.md: -------------------------------------------------------------------------------- 1 | # Zachman 2 | 3 | 第一个最有影响力的框架方法论就是 Zachman 框架,它是 John Zachman 首次在 1987 年提出的。 4 | Zachman 框架模型分两个维度:横向维度采用 6W(what、how、where、who、when、why)进行组织,纵向维度反映了 IT 架构层次,从上到下(Top-Down),分别为范围模型、企业模型、系统模型、技术模型、详细模型、功能模型。横向结合 6W,Zachman 框架分别由数据、功能、网络、人员、时间、动机分别对应回答 What、How、Where、Who、When 与 Why 这六个问题。 5 | 6 | ![](https://assets.ng-tech.icu/item/20230427181807.png) 7 | -------------------------------------------------------------------------------- /04~服务化架构/README.link: -------------------------------------------------------------------------------- 1 | https://github.com/wx-chevalier/MicroCN-Notes?q= 2 | -------------------------------------------------------------------------------- /99~参考资料/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/System-Architecture-Notes/4f033cf5883b8fc600f863505b8f80d7f0ccdc6b/99~参考资料/.DS_Store -------------------------------------------------------------------------------- /99~参考资料/Awesome System Design Articles/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://github.com/ashishps1/awesome-system-design-resources#awesome-system-design-articles) 2 | -------------------------------------------------------------------------------- /INTRODUCTION.md: -------------------------------------------------------------------------------- 1 | # Introduction(导读) 2 | 3 | # 软件架构编年史 4 | 5 | - 20 世纪 50 年代 6 | - **非结构化编程** 7 | - ~1951 – **汇编** 8 | - 20 世纪 60 年代 9 | - **结构化编程** 10 | - **分层**: 用户界面、业务逻辑数据存储都在**一层**。 11 | - ~1958 – Algol 12 | - 20 世纪 70 年代 13 | - **过程式/函数式编程** 14 | - ~1970 – Pascal 15 | - ~1972 – C 16 | - [1979](http://heim.ifi.uio.no/~trygver/1979/mvc-2/1979-12-MVC.pdf) – **MVC 模式(Model-View-Controller)** 17 | - 20 世纪 80 年代 18 | - **面向对象编程** (但其思想在 [20 世纪 60 年代](http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en)晚期已经第一次提出) 19 | - **分层**: **两层**,第一层是用户界面,第二层是业务逻辑和数据存储 20 | - ~1980 – C++ 21 | - **CORBA** – 通用物件请求代理架构(尽管[1991 年](https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture%23Versions_history)才推出第一个稳定版,但最早使用可以追溯到 [20 世纪 80 年代](https://en.wikipedia.org/wiki/TIBCO_Software)) 22 | - ~1986 – Erlang 23 | - ~1987 – Perl 24 | - [1987](https://www.lri.fr/~mbl/ENS/FONDIHM/2013/papers/Coutaz-Interact87.pdf) – PAC 即 **HMVC 模式(Hierarchical Model-View-Controller)** 25 | - [1988](https://drive.google.com/file/d/0BwhCYaYDn8EgNzAzZjA5ZmItNjU3NS00MzQ5LTkwYjMtMDJhNDU5ZTM0MTlh/view) – **LSP(里氏替换原则)** (~SO**L**ID) 26 | - 20 世纪 90 年代 27 | - **分层**: **三层**,第一层是用户界面,第二层是业务逻辑(以及浏览器作为客户端时的用户界面展现逻辑),第三层是数据存储 28 | - ~1991 – **消息总线** 29 | - ~1991 – Python 30 | - [1992](https://www.amazon.com/Object-Oriented-Software-Engineering-Driven-Approach/dp/0201403471) – **EBI 架构**(Entity-Boundary-Interactor) 即 EBC 或 EIC 31 | - ~1993 – Ruby 32 | - ~1995 – Delphi, Java, Javascript, PHP 33 | - [1996](http://www.wildcrest.com/Potel/Portfolio/mvp.pdf) – **MVP 模式(Model-View-Presenter)** 34 | - [1996](http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod) – **OCP**, **ISP**, **DIP** (~S**O**L**ID**), REP, CRP, CCP, ADP 35 | - [1997](http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod) – SDP, SAP 36 | - ~[1997](http://www.cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf) – **面向方面编程** 37 | - ~1997 – Web 服务 38 | - ~[1997](http://shop.oreilly.com/product/9780596006754.do) – **ESB** – 企业服务总线 (尽管创造该术语的书籍 2004 年才出版,但这个概念早已被使用) 39 | - 21 世纪 00 年代 40 | - [2002](http://a.co/7S3sJ2J) – **SRP** (~**S**OLID) 41 | - [2003](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) – **领域驱动设计** 42 | - [2005](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/) – **MVVM 模式(Model-View-ViewModel)** 43 | - [2005](http://alistair.cockburn.us/Hexagonal%2Barchitecture) – **端口和适配器架构**即六边形架构 44 | - [2006](https://youtu.be/JHGkaShoyNs%3Ft%3D1m17s)? – **CQRS 与 ES** (命令查询职责分离与事件溯源) 45 | - [2008](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/) – **洋葱架构** 46 | - [2009](https://medium.com/s-c-a-l-e/talking-microservices-with-the-man-who-made-netflix-s-cloud-famous-1032689afed3) – **微服务**(Netflix) 47 | - 21 世纪 10 年代 48 | - [2010](https://www.amazon.co.uk/Lean-Architecture-Agile-Software-Development/dp/0470684208) – **DCI 架构**(Data-Context-Interaction) 49 | - [2012](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) – **整洁架构** 50 | - [2014](http://www.codingthearchitecture.com/2014/08/24/c4_model_poster.html) – C4 模型 51 | --------------------------------------------------------------------------------