<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>阅读 on Nekonull's Garden</title><link>https://nekonull.me/tags/%E9%98%85%E8%AF%BB/</link><description>Recent content in 阅读 on Nekonull's Garden</description><generator>Hugo</generator><language>zh-CN</language><copyright>CC-BY-SA-4.0</copyright><lastBuildDate>Sun, 05 Jan 2025 23:38:12 +0800</lastBuildDate><atom:link href="https://nekonull.me/tags/%E9%98%85%E8%AF%BB/index.xml" rel="self" type="application/rss+xml"/><item><title>读书记录《悟道领域驱动设计》</title><link>https://nekonull.me/share/book-ddd/</link><pubDate>Sun, 05 Jan 2025 23:38:12 +0800</pubDate><guid>https://nekonull.me/share/book-ddd/</guid><description>&lt;ul&gt;
&lt;li&gt;ch2 应用架构
&lt;ul&gt;
&lt;li&gt;贫血模型 vs 充血模型
&lt;ul&gt;
&lt;li&gt;贫血模型：类只是数据容器，没有行为（例如 pojo）&lt;/li&gt;
&lt;li&gt;充血模型：类有属性（数据），也有方法（行为）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;项目结构
&lt;ul&gt;
&lt;li&gt;接口层：对外暴露api/消息consumer&lt;/li&gt;
&lt;li&gt;应用服务层：协调领域模型完成业务逻辑&lt;/li&gt;
&lt;li&gt;基础设施层：写db/缓存/调外部服务&lt;/li&gt;
&lt;li&gt;领域层：领域内的具体逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;查询的两种实现
&lt;ul&gt;
&lt;li&gt;1 读数据后加载领域模型（聚合根），聚合根转换为查询结果 view&lt;/li&gt;
&lt;li&gt;2 直接用实际存储的数据模型，跳过领域对象加载，直接用把数据模型转换为查询结果 view&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch3 实体和值对象
&lt;ul&gt;
&lt;li&gt;实体：有唯一标识的领域模型
&lt;ul&gt;
&lt;li&gt;e.g. 内容发布系统的文章&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;值对象：没有唯一标识，需要的时候随时构造，值一样就认为是相同的对象
&lt;ul&gt;
&lt;li&gt;e.g. 内容发布系统的文章标题&lt;/li&gt;
&lt;li&gt;DP：Domain Primitive 领域内的基本数据类型，值对象的一种
&lt;ul&gt;
&lt;li&gt;e.g. 金融系统中的金额（money）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch4 聚合与聚合根
&lt;ul&gt;
&lt;li&gt;聚合：一组对象组成的对象树
&lt;ul&gt;
&lt;li&gt;单个实体也是聚合&lt;/li&gt;
&lt;li&gt;聚合是一致性的边界；聚合内强一致，跨聚合最终一致
&lt;ul&gt;
&lt;li&gt;一个事务只更新单个聚合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;聚合根：这个对象树的入口
&lt;ul&gt;
&lt;li&gt;单个实体也可以是聚合根&lt;/li&gt;
&lt;li&gt;外部对象只能引用聚合根（通常通过持有一个id）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拆分聚合
&lt;ul&gt;
&lt;li&gt;一个实体被多个聚合根引用，应该被提升为聚合根&lt;/li&gt;
&lt;li&gt;1:N 的集合属性（Role-Resource Binding），因为存在双向查找的需要（Role找Resource，Resource找Role），通常也可能被提升为聚合根&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch5 Factory, Repo, 领域服务
&lt;ul&gt;
&lt;li&gt;Factory：从无到有创建领域对象
&lt;ul&gt;
&lt;li&gt;需要分配 id&lt;/li&gt;
&lt;li&gt;通常用 builder 模式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Repo：保存和加载聚合根，和实际的存储解耦
&lt;ul&gt;
&lt;li&gt;save：聚合根持久化到数据库
&lt;ul&gt;
&lt;li&gt;注意事务控制，所有实际的写操作应该在一个事务内执行（例如文章写一个表，文章内容写另一个表）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;load：从数据库查询数据对象，组装回聚合根&lt;/li&gt;
&lt;li&gt;推荐实现行级 repo，一个聚合根对应数据库中一行数据
&lt;ul&gt;
&lt;li&gt;如果存在查多行的需求，如 queryList，应该用 CQRS 分离出去&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;领域服务：包含领域中不适合放在实体/值对象的业务操作，应该是无状态的
&lt;ul&gt;
&lt;li&gt;例子：导出数据为 excel&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch6 设计模式
&lt;ul&gt;
&lt;li&gt;责任链：多个handler逐个执行&lt;/li&gt;
&lt;li&gt;策略：从多种算法实现中选择一个（根据业务类型匹配到策略）&lt;/li&gt;
&lt;li&gt;桥接&lt;/li&gt;
&lt;li&gt;规约：验证复杂规则&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch7 防腐层 ACL
&lt;ul&gt;
&lt;li&gt;避免外部系统变更影响到当前系统（字段名变化、包名变化&amp;hellip;）&lt;/li&gt;
&lt;li&gt;实现：适配器模式
&lt;ul&gt;
&lt;li&gt;出入参数应该是本地值对象，或者基本数据类型&lt;/li&gt;
&lt;li&gt;外部异常/错误码应该转换为本地异常/错误码&lt;/li&gt;
&lt;li&gt;只返回实际需要的字段&lt;/li&gt;
&lt;li&gt;一般在应用服务/领域服务调用防腐层，不要在实体和值对象使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch8 领域事件
&lt;ul&gt;
&lt;li&gt;幂等实现：数据库唯一索引 / 状态机&lt;/li&gt;
&lt;li&gt;事件建模：用领域语言（例如账户被激活）；建模为值对象/贫血对象（不可变的）&lt;/li&gt;
&lt;li&gt;应用
&lt;ul&gt;
&lt;li&gt;触发其他领域/聚合的行为&lt;/li&gt;
&lt;li&gt;记录状态变化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;事件消息体：实体id，事件id，事件类型，发生事件
&lt;ul&gt;
&lt;li&gt;也可选包含具体数据，减少查询需要，例如用户变更手机号事件带上新的手机号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;生成事件
&lt;ul&gt;
&lt;li&gt;应用层创建：推荐，直接在应用层生成，然后调用基础服务发布&lt;/li&gt;
&lt;li&gt;聚合根创建：聚合根内生成事件，存储在聚合根内一个临时位置，应用层调用方法从聚合根取得，然后调用基础服务进行发布&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;发布事件
&lt;ul&gt;
&lt;li&gt;问题：保存聚合根和发布领域事件应该是一个事务，不能一个失败一个成功；但是引入分布式事务会造成复杂度上升&lt;/li&gt;
&lt;li&gt;解决：用一个db里事件表，repo save的时候不仅写聚合根，还写事件表；然后用事件表变更触发mq
&lt;ul&gt;
&lt;li&gt;轮询补偿：一个外部定时任务，检查事件表中状态=未发布的事件，读出来发mq，然后更新状态为已发布&lt;/li&gt;
&lt;li&gt;拖尾：用一个db拖尾组件监听db变更，自动发布到mq&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;订阅事件：事件 consumer 作为一个新的接口层，调用应用层服务
&lt;ul&gt;
&lt;li&gt;注意幂等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch9 CQRS
&lt;ul&gt;
&lt;li&gt;问题：查询的时候加载聚合根可能没必要（例如只读取一部分字段）；但是修改的时候必须要加载完整聚合根&lt;/li&gt;
&lt;li&gt;CQRS 应用层分为 查询 query 和 修改 command 两部分，分别用不同的模型处理
&lt;ul&gt;
&lt;li&gt;command 依然加载完整聚合根&lt;/li&gt;
&lt;li&gt;query 直接用数据模型（db数据）进行查询，转换为需要的返回 view&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现
&lt;ul&gt;
&lt;li&gt;同数据源：比较简单，query 不用 repo.load 而是直接读db&lt;/li&gt;
&lt;li&gt;异数据源：复杂，可能导致不一致
&lt;ul&gt;
&lt;li&gt;需求：主数据存储 mysql，用 es 做文本搜索&lt;/li&gt;
&lt;li&gt;实现：应用层发布领域事件，db日志拖尾&lt;/li&gt;
&lt;li&gt;查询可以是一个贫血模型，因为没有复杂逻辑且只读数据不修改&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点：复杂度高、可能导致数据一致性问题、增加学习成本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch10 事件溯源
&lt;ul&gt;
&lt;li&gt;想法：存储领域事件，读取时通过重放领域事件得到最新聚合状态
&lt;ul&gt;
&lt;li&gt;优点：完整业务跟踪能力；可以回滚到任意时刻的聚合根&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现1：最原始的实现，直接存领域事件，读时回放
&lt;ul&gt;
&lt;li&gt;只有一个事件表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现2：事件多查起来慢，因此加快照，回放时用 上次快照 + 上次快照后的事件
&lt;ul&gt;
&lt;li&gt;事件表 + 快照表，save的时候可能需要生成快照&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现3：快照表用拉链表实现，存储所有事件+聚合根所有版本（含有效期）
&lt;ul&gt;
&lt;li&gt;拉链表：含有开始时间和结束时间，表明这一行数据在此时间段内有效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch11 一致性
&lt;ul&gt;
&lt;li&gt;聚合内一致性
&lt;ul&gt;
&lt;li&gt;事务应该在 repo 实现，不应该在应用层实现，否则会造成事务过大&lt;/li&gt;
&lt;li&gt;用乐观锁避免并发更新问题&lt;/li&gt;
&lt;li&gt;repo save 失败，需要在应用层重试，来确保聚合数据是最新状态（即重新 load）
&lt;ul&gt;
&lt;li&gt;因为会重新调用外部接口，依赖的外部接口应该幂等&lt;/li&gt;
&lt;li&gt;不适合频繁更新的热点数据（可能导致频繁重试）&lt;/li&gt;
&lt;li&gt;重试次数规划好，一般一次就够了，多次的话应该考虑其他架构&lt;/li&gt;
&lt;li&gt;重试次数/触发原因可配置，一般只有乐观锁失败再重试，其他错误不应该重试&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;读写性能问题
&lt;ul&gt;
&lt;li&gt;一般业务做不到那么大&lt;/li&gt;
&lt;li&gt;读多写少：读写分离、缓存、复杂查询维护单独的读数据源、分库分表&lt;/li&gt;
&lt;li&gt;写之前的确需要完整加载聚合，写慢点就忍吧&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;跨聚合一致性：实际上是分布式事务
&lt;ul&gt;
&lt;li&gt;二阶段提交：prepare, commit；commit 可能失败，此时rollback
&lt;ul&gt;
&lt;li&gt;代价比较大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;本地消息表+发布领域事件：见 ch8&lt;/li&gt;
&lt;li&gt;最大努力通知：上游不断发起通知调用给下游，直到下游确认
&lt;ul&gt;
&lt;li&gt;适用于对可靠性要求不高&lt;/li&gt;
&lt;li&gt;发起者需要提供查单接口，供接收者主动查询状态&lt;/li&gt;
&lt;li&gt;发起者需要实现重复通知，接收者自己保证幂等&lt;/li&gt;
&lt;li&gt;发通知间隔应该指数退避，且限制最大次数，避免无限发送&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TCC：try, confirm, cancel；try成功了confirm必须成功
&lt;ul&gt;
&lt;li&gt;注意点1 幂等：confirm/cancel幂等&lt;/li&gt;
&lt;li&gt;注意点2 空回滚：没有调用try就调用了cancel；收到cancel查资源状态，没try过应该直接返回&lt;/li&gt;
&lt;li&gt;注意点3 事务悬挂：先执行cancel再执行try；收到try查资源状态，被cancel过也直接返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;saga：正向perform，补偿compensate
&lt;ul&gt;
&lt;li&gt;注意点1 隔离性：正向操作完成后，其他事务已经能观察到了，负向回滚之后可能影响到其他事务（用户看到订单消失）
&lt;ul&gt;
&lt;li&gt;考虑引入一个中间状态例如 pending&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注意点2 幂等：perform/compensate 都应该幂等&lt;/li&gt;
&lt;li&gt;注意点3 空回滚：没有perform就compensate；补偿前用业务主键查是否有perform，没有直接返回，并记录已回滚&lt;/li&gt;
&lt;li&gt;注意点4 事务悬挂：先compensate再perform；perform前查询是否compensate过，有则报错&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;选型
&lt;ul&gt;
&lt;li&gt;不要求实时，只要求最终一致：本地消息表、最大努力通知&lt;/li&gt;
&lt;li&gt;要求实时：TCC&lt;/li&gt;
&lt;li&gt;长事务、涉及外部/遗留系统：saga&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch12 战略设计
&lt;ul&gt;
&lt;li&gt;概念
&lt;ul&gt;
&lt;li&gt;限界上下文：一个特定业务内的概念、规则、流程
&lt;ul&gt;
&lt;li&gt;如电商系统中的订单、商品、营销、物流&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;上下文映射：不同限界上下文之间的协作关系&lt;/li&gt;
&lt;li&gt;子域：关联性强的限界上下文形成的大的业务概念
&lt;ul&gt;
&lt;li&gt;主播+直播 -&amp;gt; 视频直播子域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;划分限界上下文：按照业务边界&lt;/li&gt;
&lt;li&gt;上下文映射
&lt;ul&gt;
&lt;li&gt;共享内核：存在共享的代码、领域模型、基础设施等&lt;/li&gt;
&lt;li&gt;客户 供应商：客户（下游）给供应商（上游）提要求，如加接口、加字段&lt;/li&gt;
&lt;li&gt;跟随者：上游不响应下游要求，下游得自己做&lt;/li&gt;
&lt;li&gt;各行其道：上下游完全不关联&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;子域类型
&lt;ul&gt;
&lt;li&gt;核心子域：业务系统最重要的部分，业务价值高&lt;/li&gt;
&lt;li&gt;支撑子域：起到支撑作用，但是没有成熟/通用方案，需要自己构建&lt;/li&gt;
&lt;li&gt;通用子域：有通用性，存在成熟/通用方案，可以通过采购/开源获得&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch13 领域建模
&lt;ul&gt;
&lt;li&gt;事件风暴法：收集所有领域事件，归纳领域模型&lt;/li&gt;
&lt;li&gt;收集内容
&lt;ul&gt;
&lt;li&gt;领域事件&lt;/li&gt;
&lt;li&gt;命令：触发领域事件&lt;/li&gt;
&lt;li&gt;actor：命令的人为发起者&lt;/li&gt;
&lt;li&gt;策略：命令的规则发起者，满足某种条件自动触发；定时任务也算&lt;/li&gt;
&lt;li&gt;外部系统：也是命令的发起者&lt;/li&gt;
&lt;li&gt;聚合&lt;/li&gt;
&lt;li&gt;读模型：actor 发起命令前读的数据（例如审核员查看内容），需要这些数据辅助决策&lt;/li&gt;
&lt;li&gt;热点：待定问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;建模流程
&lt;ul&gt;
&lt;li&gt;列举领域事件&lt;/li&gt;
&lt;li&gt;按业务流程排序领域事件；无法连接的说明可能遗漏了&lt;/li&gt;
&lt;li&gt;补充命令&lt;/li&gt;
&lt;li&gt;补充发起者&lt;/li&gt;
&lt;li&gt;提取聚合：同一个聚合的领域事件归类到一起&lt;/li&gt;
&lt;li&gt;补充读模型&lt;/li&gt;
&lt;li&gt;划分限界上下文，标注映射关系；注意需要标记上下游&lt;/li&gt;
&lt;li&gt;划分子域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch14 研发效能
&lt;ul&gt;
&lt;li&gt;maven 脚手架&lt;/li&gt;
&lt;li&gt;响应封装 graceful response&lt;/li&gt;
&lt;li&gt;对象转换 mapstruct&lt;/li&gt;
&lt;li&gt;静态分析&lt;/li&gt;
&lt;li&gt;低代码&lt;/li&gt;
&lt;li&gt;持续集成/持续交付/持续部署&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch15 测试驱动开发 tdd
&lt;ul&gt;
&lt;li&gt;红绿循环：写测试、测试失败、写代码、测试通过、重构、测试通过&lt;/li&gt;
&lt;li&gt;贫血模式的 tdd
&lt;ul&gt;
&lt;li&gt;dao：生成的，一般不用测&lt;/li&gt;
&lt;li&gt;service：对基础设施的调用应该 mock&lt;/li&gt;
&lt;li&gt;controller：mockmvc 直接测试 http 请求，验证响应（如返回码）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ddd 中的 tdd
&lt;ul&gt;
&lt;li&gt;实体：测全分支；不应该依赖启动容器和基础设施&lt;/li&gt;
&lt;li&gt;值对象：覆盖业务规则&lt;/li&gt;
&lt;li&gt;factory&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch16 敏捷开发
&lt;ul&gt;
&lt;li&gt;scrum&lt;/li&gt;
&lt;li&gt;看板&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch17 架构可视化
&lt;ul&gt;
&lt;li&gt;c4 模型
&lt;ul&gt;
&lt;li&gt;系统上下文图：只展示核心系统、支持元素（如外部依赖、用户）&lt;/li&gt;
&lt;li&gt;容器图：展示主要数据选型和个容器的职责分工
&lt;ul&gt;
&lt;li&gt;容器：可独立运行/部署的单元（如后台单体、缓存、数据库）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;组件图：展示可执行容器内部分工，指导开发&lt;/li&gt;
&lt;li&gt;代码图：UML/ER图，不推荐画（变更频繁）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其他图
&lt;ul&gt;
&lt;li&gt;系统全景图：展示关联的所有系统&lt;/li&gt;
&lt;li&gt;动态图：展示元素在运行时如何协作，用箭头和编号表示顺序&lt;/li&gt;
&lt;li&gt;部署图：说明部署方案，含有实例数量、机房等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch18 重构
&lt;ul&gt;
&lt;li&gt;模式
&lt;ul&gt;
&lt;li&gt;修缮者：实现新方法，保留老方法，用开关切换，无问题后删除老方法&lt;/li&gt;
&lt;li&gt;绞杀者：设计新系统，用一个门面承接流量，逐渐在新系统实现功能，并切老系统的流量到新系统，直到老系统完全无流量&lt;/li&gt;
&lt;li&gt;推翻重建：彻底放弃老系统&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;流程
&lt;ul&gt;
&lt;li&gt;启动
&lt;ul&gt;
&lt;li&gt;必要性评估：考虑性能、可靠性、技术栈、业务支持、研发效率、运营效率&lt;/li&gt;
&lt;li&gt;环境因素：企业战略、组织架构稳定性、文化氛围、管理风格&lt;/li&gt;
&lt;li&gt;效益和风险分析&lt;/li&gt;
&lt;li&gt;可行性分析：天时（和企业战略一致）、地利（已具备环境条件）、人和（团队愿意支持思想一致）&lt;/li&gt;
&lt;li&gt;干系人识别：团队成员、管理者、职能部门负责人、最终用户&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;规划
&lt;ul&gt;
&lt;li&gt;确认重构范围&lt;/li&gt;
&lt;li&gt;工作任务分解：任务分给唯一的某个人完成，可以向其他组员寻求帮助，但是负责人只有一个&lt;/li&gt;
&lt;li&gt;工期估算和进度计划&lt;/li&gt;
&lt;li&gt;沟通计划：管理层、项目成员、外部团队&lt;/li&gt;
&lt;li&gt;人力资源计划：人力不足需要申请人力、提前培训&lt;/li&gt;
&lt;li&gt;质量管理计划：业务流程梳理用力、数据一致性对比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;执行
&lt;ul&gt;
&lt;li&gt;组建团队&lt;/li&gt;
&lt;li&gt;梳理现有业务逻辑：读代码、读历史文档、头脑风暴&lt;/li&gt;
&lt;li&gt;整理用例并评审&lt;/li&gt;
&lt;li&gt;实施开发：DDD、敏捷、测试驱动、CICD&lt;/li&gt;
&lt;li&gt;数据迁移&lt;/li&gt;
&lt;li&gt;灰度切量&lt;/li&gt;
&lt;li&gt;老系统下线：老系统可能还有调用者&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;监控
&lt;ul&gt;
&lt;li&gt;进度&lt;/li&gt;
&lt;li&gt;质量&lt;/li&gt;
&lt;li&gt;待办项&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收尾
&lt;ul&gt;
&lt;li&gt;沉淀过程资产：架构图、FAQ、接口文档&lt;/li&gt;
&lt;li&gt;推动新系统普及：通知调用方&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数据迁移的实现
&lt;ul&gt;
&lt;li&gt;方案1：双写
&lt;ul&gt;
&lt;li&gt;步骤1：老系统开始写入新数据源；新系统需要支持写老数据源&lt;/li&gt;
&lt;li&gt;步骤2：历史数据从老数据源全量迁移到新数据源；检查新老数据一致性&lt;/li&gt;
&lt;li&gt;步骤3：读验证，灰度部分读流量到新系统&lt;/li&gt;
&lt;li&gt;步骤4：写验证，灰度部分写流量到新系统（新系统依然写老数据源，且定期校验）&lt;/li&gt;
&lt;li&gt;步骤5：全部流量切换到新系统，新系统双写老数据源&lt;/li&gt;
&lt;li&gt;步骤6：新系统稳定后，关闭新系统写老数据源开关，老系统下线&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;方案2：双向数据同步
&lt;ul&gt;
&lt;li&gt;正向：老-&amp;gt;新：传输老系统的所有数据&lt;/li&gt;
&lt;li&gt;反向：新-&amp;gt;老：只传输新系统生成的数据（对来自老系统的数据有特殊标记）&lt;/li&gt;
&lt;li&gt;步骤：正向链路一直打开，全量数据从老系统迁移到新系统，且保证增量同步；写验证前打开反向链路，直到老系统下线&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ch19 布道领域驱动设计
&lt;ul&gt;
&lt;li&gt;编码指南&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>读书记录《时间贫困》</title><link>https://nekonull.me/share/book-time-poverty/</link><pubDate>Mon, 12 Aug 2024 21:36:00 +0800</pubDate><guid>https://nekonull.me/share/book-time-poverty/</guid><description>&lt;p&gt;书名：《时间贫困》&lt;/p&gt;
&lt;p&gt;评价：7/10；很短的书，快的话一小时就能读完；了解到了一些和自己认知之外但符合自己真实感受的观点（e.g. 完全躺平未必会快乐）；可能会试试书中描述的行动。&lt;/p&gt;
&lt;p&gt;版本：微信读书&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;观点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可支配时间长!=幸福&lt;/strong&gt;：可支配时间太少（如每天少于2小时）会带来压力，从而降低幸福感；可支配时间太多（如每天超过5小时）会让人缺乏目标感，从而降低幸福感；排除时间太少和太多的极端情况，幸福与你所拥有的可支配时间的长短无关，而是取决于你如何利用自己拥有的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间充裕与否会影响自信&lt;/strong&gt;： 时间很多时，我们倾向于积极聚焦。时间充裕总体上会提升我们的信心，让我们对有信心实现的一切感到乐观而兴奋。只要有足够的时间，我们将前途无量。但在时间有限时（现实往往如此）​，我们就会悲观地倾向于预防聚焦。当所剩时间不多时，我们满脑子都是失败的可能性，从而会降低目标来匹配不足的信心。深陷时间贫困时，我们只是在勉强度日。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更自信的人认为自己也拥有更多时间&lt;/strong&gt;：当人们感受到更强的自我效能感时，他们也会认为自己拥有更多时间。这一发现意义重大，因为它意味着你可以有意识且有效地操纵你的时间宽裕度。通过采取一些手段增强你的自信心，你就会大大缓解时间贫困的困境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快乐不意味着远离工作&lt;/strong&gt;：快乐并不意味着要远离工作，因为（正如我们所知）工作是有意义的。关键在于，对时间的思考会促使我们把时间花在那些能带来个人成就感的活动上。我在那些认为工作有意义的人中重新进行了第一项研究，结果证明对时间的思考激励着他们在工作中做得更多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;亲密关系是快乐的必要条件&lt;/strong&gt;：这一重要的数据表明，尽管没有一个变量是快乐的充分条件，但亲密关系是快乐的必要条件。换句话说，有朋友并不能百分之百保证你会快乐，但要想变得快乐，你需要朋友。 只有当我们有了归属感（爱与被爱）​，追求个人成就和自我实现所付出的个人努力才是值得的。如果你想攀登事业的阶梯，这很好，但前提是不要为此牺牲你与生活中所有人的联系。如果你到达顶峰时没有人和你一起庆祝，那么你也不会有多大的成就感。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;花钱买时间可能是有意义的&lt;/strong&gt;：有研究确实警示我们，与购买更好的体验相比，购买物品带来的快乐感明显更少且不持久。此外，阿什利团队的分析结果表明，外包家务带来的积极影响并不取决于收入水平。花钱买时间可以使大多数人受益。无论你有多少钱，时间对每个人来说都一样宝贵。 如果你花点儿钱来给自己腾出一些时间，你就可以用这些时间去做对你来说真正重要的事情了。你可以更有效地使用买来的时间，把它们花在那些更快乐、更有意义的活动上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;户外让人更快乐&lt;/strong&gt;：有无数个例子证明：人们在户外更快乐。此外，这种快乐程度的提升并不取决于天气（尽管人们在阳光明媚的温暖天气中的确更快乐）​、他们正在做的事（尽管有些特别快乐的活动只能在户外进行，如打理花园或赏鸟）或环境（尽管人们在大自然或绿色空间中比在城市更快乐）​，而是只需要到户外即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;价值观+优势+爱好=更满意的工作时间&lt;/strong&gt;：越来越多的证据表明，即使你从事的工作并不完美（实际上，没有一份工作是完美的）​，但如果你将工作同你的价值观（你在意的事物）​、你的优势（你擅长的事物）以及你的爱好（你喜欢的事物）结合起来，你就会更有动力，在工作上也会表现得更加出色，对工作和生活的总体满意度也会提高&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作场所也能交朋友&lt;/strong&gt;：如果你在工作时间内开展一些真实的人际交往活动，你的工作时间会变得更快乐、更有意义。花时间在工作中结交朋友是值得的。我们清醒的大部分时间都在办公室度过，如果把这些时间都花在痛苦的工作上，那就是一种浪费。你必须摒弃这样一种观念：在工作场合中不能展现出真实的自我。 如果办公室有个你想见的人，那么你就会有上班的动力，这个人也会帮助你在工作时间获得更多的快乐和成就感。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同年龄对平凡快乐的感知不同&lt;/strong&gt;：对年轻人来说，不平凡的经历比平凡的经历更让他们感到快乐；而对年长者来说，平凡的经历给他们带来的快乐不亚于不平凡的经历。换句话说，从统计学上来看，年长者从平凡的经历和不平凡的经历中获得的快乐几乎一样多。 随着年龄的增长，人们从平凡的经历中获得的快乐会越来越多，自然而然也会意识到自己所剩的生命是有限的。当人们意识到自己的时间宝贵时，就会更享受简单时刻的快乐。这些结果表明，尽管我比阿米特大不了多少，但我正在迈向人生的下一个阶段。这也就解释了我和阿米特的快乐周末为何不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快乐时间是有限的&lt;/strong&gt;：一旦你认识到剩下的时间是有限的，你在这段时间里会更快乐。虽然在注意到时间如此有限后，你可能会感到不安，但你会更加关注并更容易发现那些简单的快乐。认识到“快乐的时光是短暂的”不仅有助于你度过艰难时期，还会提醒你停下脚步，这样你就不会错过一路上的美好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应该优先考虑带给你快乐的事&lt;/strong&gt;：研究者阿纳·凯南和拉恩·基维茨也观察到这个现象。他们巧妙地称其为“远视现象”​，即一种过于有远见的、只选未来不顾现在的倾向。这是过度自控的问题。他们指出，把苹果当零食确实比巧克力蛋糕更健康，但倘若每次都选苹果，你就永远尝不到美味的巧克力了。如果总是选择“该做之事”而非“想做之事”​，你就永远不会有享受的机会。年复一年，你只干正事，但回首往事时，你可能会因为错过了生活中应有的快乐而感到非常遗憾，比如白亚麻餐布上的羊角面包。 所以，在有限的时间里，确定、承诺并优先安排能带给你快乐的活动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拆分喜欢的活动以避免享乐适应&lt;/strong&gt;：随着时间的推移，我们会习惯现有事物，所以我们在一项活动开始时会特别敏感。这是我们最专注的时候，体验也最为强烈。因此，为了利用享乐适应，你应该把你喜欢的活动拆分开——创造更多开始，防止厌倦滋生。美好事物的延续也会带给你更多期待。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;行动&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间追踪&lt;/strong&gt;：记录自己的时间分配和快乐程度，找出快乐和不快乐的活动中存在的共同点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算剩余时间&lt;/strong&gt;：计算一下涉及其他人的活动中，将来可能做这项活动的大概次数，也就是他们还剩下多少次机会，以及现在已经用尽了多少比例的机会&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每周留出独处和思考的一小时&lt;/strong&gt;：舒式一小时比起之前提过的消除干扰所带来的快乐更有意义。正是在这样一段时间里，你可以更深入地处理问题、更恣意地创新、更有效地制定战略，以应对需要关注的重要决策&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>读书记录《软件设计的哲学（第2版）》</title><link>https://nekonull.me/share/book-philosophy-software-design-2nd/</link><pubDate>Sun, 28 Jul 2024 21:34:58 +0800</pubDate><guid>https://nekonull.me/share/book-philosophy-software-design-2nd/</guid><description>&lt;p&gt;书名：A Philosophy of Software Design (2nd Edition)&lt;/p&gt;
&lt;p&gt;评价：8.5/10；一开始是看到封面感觉很棒，于是就找来读了下；不是很长，三四个小时就能读完；虽然内容比较基本，但是能系统化的重新复习下也挺好的&lt;/p&gt;
&lt;p&gt;版本：anna&amp;rsquo;s archive，llm翻译为中文&lt;/p&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;软件设计的原则&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;复杂性的管理
&lt;ul&gt;
&lt;li&gt;复杂性源自依赖和晦涩的累积；随着复杂性增加，它导致变更放大、高认知负荷以及未知的未知因素
&lt;ul&gt;
&lt;li&gt;变更放大：一个看似简单的改动需要在多处修改代码&lt;/li&gt;
&lt;li&gt;认知负荷：为了进行更改，开发者必须积累大量信息。&lt;/li&gt;
&lt;li&gt;未知的未知：尚不清楚需要修改哪些代码，或者为了进行这些修改必须考虑哪些信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;因此，实现每个新功能需要更多的代码修改。此外，开发人员花费更多时间获取足够的信息以安全地进行更改，在最糟糕的情况下，他们甚至无法找到所需的所有信息。底线是，复杂性使得修改现有代码库变得困难且充满风险。&lt;/li&gt;
&lt;li&gt;向下转移复杂性最有意义的情况是：（a）被转移的复杂性与类的现有功能紧密相关，（b）转移复杂性将导致应用程序其他部分的简化，以及（c）转移复杂性简化了类的接口。记住，目标是最小化整个系统的复杂性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;模块化与接口设计
&lt;ul&gt;
&lt;li&gt;在设计类和其他模块时，最重要的议题是使它们具有深度，以便为常见用例提供简单的接口，同时仍能提供重要的功能。&lt;/li&gt;
&lt;li&gt;在将系统分解为模块时，尽量避免受到运行时操作顺序的影响；这会导致时间分解，从而引发信息泄露和浅层模块。&lt;/li&gt;
&lt;li&gt;软件设计中最关键的要素之一就是确定谁需要知道什么，以及何时需要知道。当细节至关重要时，最好将它们明确且尽可能显而易见地展现出来&lt;/li&gt;
&lt;li&gt;多个方法可以拥有相同的签名，只要它们各自提供有用且独特的功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;代码简化与重构
&lt;ul&gt;
&lt;li&gt;在编写详细代码时，简化代码最有效的方法之一是消除特殊情况&lt;/li&gt;
&lt;li&gt;特殊情况可能导致代码充斥着if语句，这使得代码难以理解且容易产生错误。因此，应尽可能消除特殊情况。最佳的做法是通过设计正常情况，使其自动处理边缘条件，而无需额外代码。&lt;/li&gt;
&lt;li&gt;如果你为了减少方法数量而不得不引入大量额外参数，那么你可能并没有真正简化问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从基础开始&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;变量与方法的命名规范
&lt;ul&gt;
&lt;li&gt;因此，你不应满足于仅仅是“合理接近”的命名。花一些额外的时间来挑选精确、无歧义且直观的优秀名称。这份额外的关注将很快得到回报，随着时间的推移，你将学会迅速选择好的名称。&lt;/li&gt;
&lt;li&gt;名称“cursorVisible”传达了更多信息；例如，它让读者能够猜测真值的含义（通常情况下，布尔变量的名称应始终为谓词形式）。名称中不再包含“blink”一词，因此如果读者想知道为什么光标并非始终可见，他们需要查阅文档；这部分信息相对不那么重要。&lt;/li&gt;
&lt;li&gt;如果你发现很难为一个特定变量想出一个既精确、直观又不太长的名字，这是一个警示信号。这表明该变量可能没有明确的定义或目的。当这种情况发生时，考虑采用其他分解方法。例如，也许你试图用一个单一变量来表示多个事物；如果是这样，将表示分解为多个变量可能会使每个变量的定义更简单。选择好名字的过程可以发现设计中的弱点，从而改进你的设计。&lt;/li&gt;
&lt;li&gt;名称中的每个单词都应提供有用信息；那些无助于阐明变量含义的词汇只会增加冗余（例如，它们可能导致更多行换行）。一个常见的错误是在名称中添加诸如“field”或“object”之类的通用名词，比如“fileObject”。在这种情况下，“Object”这个词很可能并未提供有用信息（是否存在不是对象的文件？），因此应从名称中省略。&lt;/li&gt;
&lt;li&gt;杰兰德的一个观点我深表赞同：“一个名称的声明与其使用之间的距离越远，该名称就应该越长。”之前关于使用名为i和j的循环变量的讨论，正是这一规则的例证。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;代码结构的清晰性与可读性
&lt;ul&gt;
&lt;li&gt;仅凭方法的长度本身很少是拆分方法的充分理由。通常情况下，开发者倾向于过度拆分方法。拆分方法会引入额外的接口，增加了复杂性。同时，它将原方法的各个部分分离，如果这些部分实际上是相关的，这会使代码更难以阅读。除非拆分方法能使整个系统变得更简单，否则不应进行拆分&lt;/li&gt;
&lt;li&gt;长方法并不总是坏事。例如，假设一个方法包含五个20行代码的块，这些块按顺序执行。如果这些块相对独立，那么方法可以逐块阅读和理解；将每个块移到单独的方法中并没有太大好处。如果这些块之间有复杂的交互，那么将它们放在一起就更为重要，以便读者可以一次性看到所有代码；如果每个块都在单独的方法中，读者将不得不在这些分散的方法之间来回翻阅，以理解它们是如何协同工作的。包含数百行代码的方法如果具有简单的签名并且易于阅读，那么它们也是很好的。这些方法是深层的（功能丰富，接口简单），这是好事&lt;/li&gt;
&lt;li&gt;深度比长度更重要：首先确保函数有足够的深度，然后再尝试使其足够短以便轻松阅读。不要为了长度牺牲深度。决定拆分或合并模块应基于复杂度。选择能实现最佳信息隐藏、最少依赖关系及最深接口的结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注释的重要性与编写技巧
&lt;ul&gt;
&lt;li&gt;优质的注释能显著提升软件的整体质量；编写优质注释并不难；而且（这可能难以置信）编写注释实际上可以很有趣。&lt;/li&gt;
&lt;li&gt;注释通过提供不同层次的详细信息来增强代码。有些注释提供比代码更低的、更详细的层次信息；这些注释通过阐明代码的确切含义来增加精确性。其他注释提供比代码更高的、更抽象的层次信息；这些注释提供直觉，比如代码背后的推理，或者一种更简单、更抽象的思考代码的方式。与代码处于同一层次的注释很可能会重复代码的内容。&lt;/li&gt;
&lt;li&gt;具体的注释方式
&lt;ul&gt;
&lt;li&gt;在注释类实例变量、方法参数和返回值时，精确性尤为重要。变量声明中的名称和类型通常不够精确。注释可以填补缺失的细节，例如：
&lt;ul&gt;
&lt;li&gt;这个变量的单位是什么？&lt;/li&gt;
&lt;li&gt;边界条件是包含性的还是排他性的？&lt;/li&gt;
&lt;li&gt;如果允许空值，这暗示着什么？&lt;/li&gt;
&lt;li&gt;如果一个变量指向一个最终必须被释放或关闭的资源，那么谁负责释放或关闭它？&lt;/li&gt;
&lt;li&gt;是否存在某些特性（不变量），对于变量而言总是成立，例如“这个列表始终至少包含一个条目”？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在记录变量时，应考虑名词而非动词。换言之，重点在于变量所代表的内容，而非其如何被操作。&lt;/li&gt;
&lt;li&gt;在记录一个方法时，描述该方法最可能被调用的条件（特别是在方法仅在特殊情况下被调用时）会非常有帮助。&lt;/li&gt;
&lt;li&gt;记录抽象的第一步是将接口注释与实现注释分开。接口注释提供了某人为了使用类或方法所需了解的信息；它们定义了抽象。实现注释描述了类或方法内部如何工作以实现抽象。将这两种注释分开很重要，这样接口的用户就不会接触到实现细节。&lt;/li&gt;
&lt;li&gt;方法接口注释既包含高层次的抽象信息，也包含低层次的精确细节
&lt;ul&gt;
&lt;li&gt;注释通常以一两句话开始，描述调用者感知到的方法行为；这是更高层次的抽象。评论必须详细描述每个参数及其返回值（如有）。&lt;/li&gt;
&lt;li&gt;这些评论必须非常精确，并且必须描述参数值的任何限制以及参数之间的依赖关系。&lt;/li&gt;
&lt;li&gt;如果方法有任何副作用，这些必须在接口注释中记录。副作用是指方法对系统未来行为产生影响的任何后果，但不是结果的一部分。例如，如果方法向内部数据结构添加一个值，该值可以通过未来的方法调用检索，这就是副作用；写入文件系统也是副作用。&lt;/li&gt;
&lt;li&gt;方法的接口注释必须描述该方法可能抛出的任何异常。&lt;/li&gt;
&lt;li&gt;如果在一个方法被调用之前必须满足某些先决条件，这些条件必须被描述出来（可能需要先调用其他方法；对于二分查找方法，被查找的列表必须是已排序的）。尽量减少先决条件是一个好主意，但任何保留的先决条件都必须有文档说明。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;幸运的是，有一个明显的地方是开发者在添加新状态值时必须去的，那就是状态枚举的声明处。我们利用这一点，在那个枚举中添加了注释，指出了所有也必须修改的其他地方&lt;/li&gt;
&lt;li&gt;处理跨模块注释：我最近在尝试一种方法，即跨模块问题记录在一个名为designNotes的中央文件中。该文件被清晰地划分为多个标有明确标签的部分，每个部分对应一个主要主题。&lt;/li&gt;
&lt;li&gt;在遵循注释应描述代码中不明显内容的规则时，“明显”是从初次阅读代码的人（而非你本人）的角度出发的。撰写注释时，尝试站在读者的立场，思考他们需要了解的关键信息是什么。如果你的代码正在接受审查，而审查者指出某些内容不明显，不要与他们争论；如果读者认为某处不明显，那么它就是不明显。与其争论，不如尝试理解他们感到困惑的地方，并思考是否能通过更清晰的注释或更优化的代码来阐明。&lt;/li&gt;
&lt;li&gt;一般来说，注释与它所描述的代码之间的距离越远，它就应该越抽象（这样可以降低因代码变动而导致注释失效的可能性）。&lt;/li&gt;
&lt;li&gt;在撰写提交信息时，问问自己：未来开发者是否需要这些信息？如果是，那么请在代码中记录下来。例如，一个描述了促使代码变更的微妙问题的提交信息。如果这未在代码中记录，那么后续开发者可能会在不知情的情况下撤销该变更，从而重新引入一个错误。如果你想在提交信息中也包含这份信息的副本，那当然可以，但最重要的是将其记录在代码中。这体现了将文档置于开发者最可能看到的地方的原则；而提交日志通常并非这样的场所。&lt;/li&gt;
&lt;li&gt;保持注释最新性的第二种技巧是避免重复。如果文档被复制，开发者找到并更新所有相关副本的难度就会增加。相反，尝试对每个设计决策只记录一次。如果代码中多个地方受到某个特定决策的影响，不要在这些点重复文档。而是找到最显眼的单一位置放置文档。例如，假设某个变量的行为复杂，影响到该变量使用的多个不同地方。你可以在变量声明旁边的注释中记录这种行为。这是一个自然的位置，开发者在理解使用该变量的代码遇到困难时很可能会查看。&lt;/li&gt;
&lt;li&gt;对于更局部化的约定，例如不变量，找到代码中合适的位置来记录它们。如果你不将这些约定写下来，其他人很可能不会遵循它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;何时测试
&lt;ul&gt;
&lt;li&gt;测试，尤其是单元测试，在软件设计中扮演着重要角色，因为它们促进了重构。没有测试套件，对系统进行重大结构改动是危险的。没有简单的方法来发现错误，因此错误很可能会在新代码部署后才被发现，那时发现和修复错误的成本要高得多。因此，在没有良好测试套件的系统中，开发者会避免重构；他们试图为每个新功能或错误修复最小化代码更改的数量，这意味着复杂性积累，设计错误得不到纠正。有了良好的测试集，开发者在重构时可以更有信心，因为测试套件会发现大多数引入的错误。这鼓励开发者对系统进行结构上的改进，从而得到更好的设计。&lt;/li&gt;
&lt;li&gt;测试驱动开发的问题在于，它将注意力集中在使特定功能正常工作上，而不是寻找最佳设计。这纯粹是战术编程，带有其所有的不利之处。测试驱动开发过于渐进：在任何时候，都很容易为了通过下一个测试而匆匆添加下一个功能。没有明显的时间进行设计，因此很容易陷入混乱&lt;/li&gt;
&lt;li&gt;在修复 bug 时，先编写测试是一个合理的做法。在修复 bug 之前，先写一个因为该 bug 而失败的单元测试。然后修复 bug，并确保单元测试现在通过。这是确保你真正修复了 bug 的最佳方法。如果你在编写测试之前就修复了bug，那么新的单元测试可能实际上并未触发该 bug，这种情况下它将无法告诉你是否真正解决了问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;设计模式的应用
&lt;ul&gt;
&lt;li&gt;不要试图将问题强行套入某个设计模式，而应采用更简洁的自定义方法。使用设计模式并不意味着自动提升软件系统的质量；只有当设计模式恰到好处时，才能发挥其优势。&lt;/li&gt;
&lt;li&gt;每当你遇到一个新的软件开发范式的提议时，从复杂性的角度对其进行质疑：这个提议是否真的有助于减少大型软件系统的复杂性？许多提议表面上听起来不错，但如果你深入探究，你会发现其中一些实际上使复杂性变得更糟，而非更好。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具体做法&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;设计两次
&lt;ul&gt;
&lt;li&gt;我注意到，“设计两次”原则有时对非常聪明的人难以接受。在他们成长的过程中，聪明人发现他们对任何问题的第一个快速想法就足以获得好成绩；没有必要考虑第二个或第三个可能性。这往往导致不良的工作习惯。然而，随着这些人年龄的增长，他们被提拔到面临越来越困难问题的环境中。最终，每个人都会达到一个阶段，即你的第一个想法不再足够好；如果你想取得真正出色的成果，无论你多么聪明，你都必须考虑第二个可能性，甚至可能是第三个。大型软件系统的设计就属于这一类：没有人能够一次就做得完美。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注释先行的开发
&lt;ul&gt;
&lt;li&gt;最佳的注释编写时机是在过程的开始，即编写代码的同时。先编写注释使得文档成为设计过程的一部分。这不仅能产生更好的文档，还能带来更优秀的设计，并且使编写文档的过程更加愉快。&lt;/li&gt;
&lt;li&gt;先写注释意味着在开始编码前，抽象概念会更加稳定。这很可能会在编码过程中节省时间。相反，如果先写代码，抽象概念可能会随着编码的进行而演变，这需要比先写注释的方法更多的代码修订。综合考虑这些因素，整体上先写注释可能会更快。
&lt;ul&gt;
&lt;li&gt;对于一个新类，我首先撰写类接口注释。&lt;/li&gt;
&lt;li&gt;接下来，我会为最重要的公共方法编写接口注释和签名，但我会让方法体保持空白。&lt;/li&gt;
&lt;li&gt;我稍微反复斟酌这些评论，直到基本结构感觉差不多合适。&lt;/li&gt;
&lt;li&gt;在此，我为类中最重要的实例变量撰写声明和注释。&lt;/li&gt;
&lt;li&gt;最后，我填充了方法的主体，并在必要时添加了实现注释。&lt;/li&gt;
&lt;li&gt;在编写方法体时，我通常会发现需要额外的属性和实例变量。对于每个新写的方法，我会在方法体之前先写接口注释；对于实例变量，我会在写变量声明的同时填写注释。&lt;/li&gt;
&lt;li&gt;当代码完成时，注释也已完成。从未有过未编写的注释积压。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;性能优化与重构
&lt;ul&gt;
&lt;li&gt;一旦你对什么是昂贵、什么是便宜有了大致的了解，你就可以利用这些信息尽可能选择便宜的操作。在很多情况下，更高效的方法可能和较慢的方法一样简单。&lt;/li&gt;
&lt;li&gt;再举一个例子，考虑在C或C++这样的语言中分配一个结构体数组。有两种方法可以实现这一点。一种方法是将数组用于保存指向结构体的指针，在这种情况下，你必须首先为数组分配空间，然后为每个单独的结构体分配空间。将结构体直接存储在数组中要高效得多，这样你只需为所有内容分配一个大的内存块。&lt;/li&gt;
&lt;li&gt;一般来说，代码越简单，运行速度往往越快。如果你已经定义并处理了特殊情况和异常，那么就不需要额外的代码来检查这些情况，系统运行速度自然更快。深层类比浅层类更高效，因为每次方法调用它们能完成更多工作。浅层类会导致更多的层级跨越，而每次层级跨越都会增加开销。&lt;/li&gt;
&lt;li&gt;在进行任何更改之前，应测量系统的现有行为。这有两个目的。首先，这些测量将确定性能调优影响最大的地方。仅仅测量顶层系统性能是不够的。这可能告诉你系统太慢，但不会告诉你原因。你需要更深入地测量，以详细识别影响整体性能的因素；目标是找出系统当前花费大量时间的少数特定位置，并且你有改进的想法。测量的第二个目的是提供一个基准，这样你可以在更改后重新测量性能，以确保性能确实得到了提升。如果更改没有使性能产生可测量的差异，那么就撤销这些更改（除非它们使系统更简单）。除非能显著加快系统速度，否则保留复杂性是没有意义的。&lt;/li&gt;
&lt;li&gt;改进其性能的最佳方法是进行“根本性”的改变，比如引入缓存，或者采用不同的算法方法（例如平衡树与列表）。&lt;/li&gt;
&lt;li&gt;首先，问问自己，在常见情况下，为了完成所需任务，必须执行的最少代码量是多少。忽略任何现有的代码结构。想象一下，你正在编写一个新方法，只实现关键路径，即在大多数常见情况下必须执行的最少代码量。当前的代码可能充斥着特殊情况；在这个练习中忽略它们。当前的代码可能在关键路径上经过多个方法调用；想象一下，你可以将所有相关代码放在一个方法中。当前的代码也可能使用多种变量和数据结构；只考虑关键路径所需的数据，并假设任何数据结构对关键路径最为方便。例如，将多个变量合并为一个值可能是有意义的。假设你可以完全重新设计系统，以最小化关键路径必须执行的代码量。我们称这种代码为“理想状态”。&lt;/li&gt;
&lt;li&gt;在为性能进行重构时，应尽量减少必须检查的特殊情况数量。理想情况下，开始处应只有一个if语句，通过一次测试就能检测所有特殊情况。在正常情况下，只需进行这一次测试，之后关键路径即可无须额外特殊情况测试地执行。如果初始测试未通过（意味着出现了特殊情况），代码可以跳转到关键路径之外的独立位置处理该情况。&lt;/li&gt;
&lt;li&gt;清晰的设计与高性能是可以兼容的。Buffer类的重写不仅使其性能提升了两倍，同时简化了设计并减少了20%的代码量。复杂的代码往往运行缓慢，因为它执行了多余或重复的工作。相反，如果你编写清晰、简洁的代码，你的系统很可能已经足够快速，以至于你无需过多担心性能问题。在少数确实需要优化性能的情况下，关键仍然是简洁性：找出对性能至关重要的关键路径，并尽可能简化它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;遵守约定和惯例
&lt;ul&gt;
&lt;li&gt;一旦发现任何看似约定的做法，就应遵循。在进行设计决策时，问问自己这个决策是否可能在项目的其他地方也有类似的选择；如果有，找到一个现成的例子，并在你的新代码中采用相同的方法。&lt;/li&gt;
&lt;li&gt;不要改变现有的惯例。抵制那种想要“改进”现有惯例的冲动。拥有一个“更好的想法”并不是引入不一致性的充分理由。你的新想法可能确实更好，但一致性相对于不一致性的价值几乎总是大于一种方法相对于另一种方法的价值。在引入不一致行为之前，问自己两个问题。首先，你是否拥有重要的新信息来证明你的方法，而这些信息在旧惯例建立时是不可用的？其次，新方法是否好到值得花时间去更新所有旧的使用？如果你的组织同意这两个问题的答案都是“是”，那么就大胆进行升级；完成后，旧惯例的痕迹应该荡然无存。然而，你仍然面临风险，即其他开发者可能不知道新惯例，因此他们未来可能会重新引入旧方法。总的来说，重新考虑已建立的惯例很少是开发者时间的良好利用。&lt;/li&gt;
&lt;li&gt;“显而易见”存在于读者心中：注意到他人代码的不明显之处比发现自己的代码问题要容易得多。因此，判断代码是否显而易见的最佳方法是通过代码审查。如果有人阅读你的代码后认为它不明显，那么它就是不明显的，无论对你来说它看起来多么清晰。通过努力理解是什么使得代码不明显，你将学会如何在将来编写更好的代码。&lt;/li&gt;
&lt;li&gt;代码如果符合读者预期的惯例，则最为直观；如果不符合，那么记录这种行为就很重要，以免读者感到困惑。&lt;/li&gt;
&lt;li&gt;为了使代码显而易见，你必须确保读者始终拥有理解代码所需的信息。你可以通过三种方式来实现这一点。最佳方法是减少所需的信息量，运用抽象和消除特殊情况等设计技巧。其次，你可以利用读者在其他情境中已获得的信息（例如，通过遵循惯例和符合预期），这样读者就不必为你的代码学习新信息。第三，你可以通过使用良好的命名和策略性注释等技巧，在代码中向他们展示重要信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;正确对待事件驱动编程
&lt;ul&gt;
&lt;li&gt;事件驱动编程使得跟踪控制流程变得困难。事件处理函数从未被直接调用；它们是通过事件模块间接调用的，通常使用函数指针或接口。即使你在事件模块中找到了调用点，仍然无法确定具体会调用哪个函数：这取决于运行时注册了哪些处理程序。因此，很难对事件驱动代码进行推理，或者确信其工作正常。&lt;/li&gt;
&lt;li&gt;为了弥补这种晦涩，请在每个处理函数接口注释中指明其何时被调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;避免使用通用容器
&lt;ul&gt;
&lt;li&gt;不幸的是，通用容器导致代码不直观，因为被分组的元素具有模糊其含义的通用名称。在上述示例中，调用者必须使用result.getKey()和result.getValue()来引用两个返回值，这无法提供关于值实际含义的任何线索。&lt;/li&gt;
&lt;li&gt;因此，最好不要使用通用容器。如果你需要一个容器，可以定义一个专门针对特定用途的新类或结构。这样，你就可以为元素使用有意义的名称，并在声明中提供额外的文档，这是通用容器无法做到的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;透传变量与上下文
&lt;ul&gt;
&lt;li&gt;透传变量增加了复杂性，因为它们迫使所有中间方法都意识到它们的存在，即便这些方法并不需要使用这些变量。此外，如果一个新的变量出现（例如，系统最初构建时未支持证书，但后来决定添加该支持），你可能需要修改大量接口和方法，以确保该变量能够通过所有相关路径传递。&lt;/li&gt;
&lt;li&gt;我最常用的解决方案是引入一个上下文对象，如图7.2(d)所示。上下文存储了应用程序的所有全局状态（任何原本需要传递的变量或全局变量）。上下文远非理想的解决方案。&lt;/li&gt;
&lt;li&gt;存储在上下文中的变量大多具有全局变量的缺点；例如，可能不明显为什么存在某个特定变量，或者它在何处被使用。如果没有纪律，上下文可能会变成一个巨大的数据杂烩，在整个系统中产生不明显的依赖关系。上下文还可能引发线程安全问题；避免问题的最佳方式是使上下文中的变量不可变。遗憾的是，我尚未找到比上下文更好的解决方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;异常处理和配置参数
&lt;ul&gt;
&lt;li&gt;这些方法在短期内会让你的生活更轻松，但它们增加了复杂性，导致许多人必须处理一个问题，而不是仅仅一个人。例如，如果一个类抛出异常，该类的每个调用者都必须处理它。如果一个类导出配置参数，每个安装环境中的每个系统管理员都必须学习如何设置它们。&lt;/li&gt;
&lt;li&gt;因此，应尽可能避免使用配置参数。在导出配置参数之前，自问：“用户（或更高级别的模块）能否确定比我们在此处确定的更优值？”当确实需要创建配置参数时，尝试提供合理的默认值，以便用户仅在特殊情况下才需提供值。理想情况下，每个模块应完整解决问题；配置参数导致解决方案不完整，从而增加了系统复杂性。&lt;/li&gt;
&lt;li&gt;抛出异常容易，处理异常却难。因此，异常的复杂性主要来源于异常处理代码。减少异常处理带来的复杂性损害的最佳方法，是减少需要处理异常的地方。&lt;/li&gt;
&lt;li&gt;异常屏蔽并非在所有情况下都有效，但在其适用的场合，它是一个强有力的工具。它能够产生更深层次的类，因为它减少了类的接口（用户需要了解的异常更少），并以屏蔽异常的代码形式增加了功能。异常屏蔽是向下转移复杂性的一个例子&lt;/li&gt;
&lt;li&gt;最佳方法是重新定义语义以消除错误条件。对于无法消除的异常，应寻找机会在较低层次上屏蔽它们，从而限制其影响，或者将多个特殊情况处理程序聚合为一个更通用的处理程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软件设计的哲学与美学&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;时刻重构
&lt;ul&gt;
&lt;li&gt;如果你想为一个系统保持一个干净的设计，在修改现有代码时必须采取战略性的方法。理想情况下，当你完成每一项改动后，系统应具备如果从一开始就考虑到这些改动而设计的结构。为了实现这一目标，你必须抵制快速修复的诱惑。相反，要思考当前的系统设计是否仍然是最佳的，考虑到所需的改动。如果不是，就重构系统，以便最终获得尽可能最佳的设计。通过这种方法，系统设计随着每一次修改而不断改进。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;设计的重要性与价值
&lt;ul&gt;
&lt;li&gt;良好软件设计的一个重要元素是区分重要与不重要。应以重要的事物为核心构建软件系统。对于不太重要的事物，应尽量减少它们对系统其余部分的影响。重要的事物应加以强调并使其更加明显；不重要的事物则应尽可能隐藏。&lt;/li&gt;
&lt;li&gt;一旦你确定了重要的事物，你应该在设计中强调它们。强调的一种方式是通过突出：重要的事物应该出现在更可能被看到的地方，比如界面文档、名称或频繁使用的方法的参数。另一种强调的方式是通过重复：关键的想法反复出现。第三种强调的方式是通过中心性。最重要的事物应该位于系统的核心，它们决定了周围事物的结构。一个例子是操作系统中设备驱动的接口；这是一个核心想法，因为成百上千的驱动程序将依赖于它。&lt;/li&gt;
&lt;li&gt;专注于最重要的事物的理念不仅适用于软件设计，在技术写作领域也同样重要：使文档易于阅读的最佳方法是在开头识别几个关键概念，并围绕它们构建文档的其余部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;软件开发中的“好品味”
&lt;ul&gt;
&lt;li&gt;“好品味”这一短语描述了区分重要与不重要事物的能力。拥有好品味是成为优秀软件设计师的重要组成部分。&lt;/li&gt;
&lt;li&gt;成为优秀设计师的回报是，你能够将更多时间投入到充满乐趣的设计阶段。而糟糕的设计师则大部分时间都在复杂且脆弱的代码中追踪错误。如果你提升自己的设计技能，你不仅能更快地产出更高质量的软件，而且软件开发过程本身也会变得更加愉快。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>