|
| 1 | +# 分离关注点的意义 |
| 2 | + |
| 3 | +软件开发就是解决问题。问题一般如何解决?最常见思路:分而治之。但如何分解、组合,就是软件设计中考虑的问题。 |
| 4 | + |
| 5 | +然而,软件设计环节的大部分人都聚焦如何组合,忽略了第一步:分解,不就是把一个大系统拆成子系统,再把子系统拆成模块,一层层拆下去。这还远远不够,因为粒度太大了,就会导致不同东西混淆一起。 |
| 6 | + |
| 7 | +## 1 技术和业务耦合 |
| 8 | + |
| 9 | +某清结算系统,一开始觉得是个业务规则很多的系统,偶尔故障情有可原。但分析系统故障报告后,发现这个系统设计得很复杂。 |
| 10 | +某处的上游系统以推送方式向这个系统发消息。原本实现,开发人员发现这个过程可能丢消息,于是设计补偿机制:因为推送过来的数据是之前由这个系统发出去的,它本身有这些数据的初始信息。于是,就在DB增加一个状态,记录消息返回的情况。一旦发现丢消息,该系统就会访问上游接口,将丢失数据请求回来。 |
| 11 | + |
| 12 | +这补偿机制设计带来问题。如当系统业务量增加,DB访问压力本身很大,但这时,丢数据概率也增加,用于补偿的线程也会频繁访问DB,因为它要找出丢失数据,还要把请求回来的数据写回DB。即一旦业务量上升,本就吃力的系统,负担更重,系统卡顿加剧。 |
| 13 | + |
| 14 | +补偿机制设计有问题,在于上游系统向下游推消息,应该是通信层面问题。而在原有设计中,因为那个状态的添加,这问题被带到业务层。 |
| 15 | +就是分解没做好,分解粒度太大。开发只考虑业务功能,而忽视其他维度。技术和业务混杂。 |
| 16 | + |
| 17 | +## 2 如何设计呢? |
| 18 | + |
| 19 | +既然是否丢消息是通信层面,争取在通信层解决。 |
| 20 | + |
| 21 | +### 2.1 历史解决方案 |
| 22 | + |
| 23 | +选择吞吐量更大MQ。在未来可见业务量下,消息都不会丢。通信层问题在通信层面解决了,业务层就不受到影响了。改造后,系统稳定性大幅提升。 |
| 24 | + |
| 25 | +- 上游系统的补偿接口,现在也不需要了,上游系统得到简化 |
| 26 | +- 该系统里表示状态的字段,还被用在业务处理中,也引发其他问题,现在它只用在业务处理,角色单一,相关问题就少了 |
| 27 | + |
| 28 | +有人觉得补偿机制还是要的吧,就算换吞吐量大的MQ,还是可能丢消息。只是之前补偿机制设计不合理? |
| 29 | +若分析是不是丢消息,就要看它何时丢消息。之前业务丢消息是因为MQ处理不过来,而换了吞吐更好的MQ,就不存在该问题。 |
| 30 | +真正需要的是可靠的信息传送通道,至于是否为MQ不重要。若怕丢消息,可Pro重试,在Con做幂等。补偿是个能把场景弄复杂的做法,不推荐。 |
| 31 | + |
| 32 | +## 3 区分技术异常、业务异常的 |
| 33 | + |
| 34 | +技术层异常信息不应暴露给上层业务人员,如大型网站错误页面,而不是直接把后台npe堆栈信息给用户。 |
| 35 | + |
| 36 | +技术、业务分割模糊。但分离关注点就涉及到具体业务,具体业务的划分与分离就又迷茫。区分: |
| 37 | + |
| 38 | +- 业务人员能理解的就是业务 |
| 39 | + 如订单 |
| 40 | +- 业务人员不理解的就是技术 |
| 41 | + 如多线程 |
| 42 | + |
| 43 | +软件设计都期望将粒度分解越小越好,但又嫌分解太小过于麻烦。就像很多人希望别人写好文档,自己却不写。 |
| 44 | + |
| 45 | +业务代码和技术实现往往被混写在一起,都是因为分离不够! |
| 46 | + |
| 47 | +## 4 分离关注点 |
| 48 | + |
| 49 | +看来分解粒度太大是不太好哦。那到底该如何考虑分解? |
| 50 | + |
| 51 | +传统上,我们习惯的分解问题的方式是树型: |
| 52 | + |
| 53 | +- 如按功能分解,可分为:功能1、2、3等 |
| 54 | +- 然后,每个功能再分成功能1.1、功能1.2、功能2.1、功能3.1等 |
| 55 | + |
| 56 | +从业务看,似乎没问题。但实现系统,还要考虑非功能性需求。 |
| 57 | +比如数据不能丢失、有的系统还要求处理速度快。 |
| 58 | + |
| 59 | +这与业务不是同维度,设计时要能发现这些非功能性需求。 |
| 60 | +分解问题时,有很多维度,每个都代表一个关注点,这就是设计中一个常见的说法,“分离关注点(Separation of concerns)”。 |
| 61 | + |
| 62 | +可分离的关注点很多,最常见:把业务处理和技术实现两个关注点混杂。 |
| 63 | + |
| 64 | +## 5 现在业务处理性能跟不上,咋办? |
| 65 | + |
| 66 | +多线程!是个解决方案。但若无限制修改这段代码成多线程,则引入多线程相关问题,如资源竞争、线程同步,稍有不慎,更多bug。 |
| 67 | + |
| 68 | +**写好业务规则**和**正确处理多线程**,这是两个不同关注点,应分离业务代码和多线程代码。业务程序员基本都不该写多线程程序,应由专门程序员把并发处理封装成框架供使用,业务开发专心写业务代码即可。 |
| 69 | + |
| 70 | +> Kent Beck:我不准备在这本书里讲高并发问题,我的做法是把高并发问题从我的程序里移出去。 |
| 71 | +
|
| 72 | +把业务处理和技术实现混在一起的问题还有很多。如怎么处理分布式事务?更该问的是,业务需要分布式事务吗?我是不是业务划分不清楚,才造成DB压力? |
| 73 | + |
| 74 | +程序员最常犯错误就是认为所有问题都是技术问题,总试图用技术解决所有问题。任何试图用技术去解决其他关注点的问题,只是越挣扎,陷得越深。 |
| 75 | + |
| 76 | +另外易产生混淆的关注点是 |
| 77 | + |
| 78 | +## 6 不同的数据变动方向 |
| 79 | + |
| 80 | +做DB访问用Spring Data JPA or MyBatis: |
| 81 | + |
| 82 | +- Spring Data JPA简化DB访问,自动生成对应SQL |
| 83 | +- MyBatis则要手写SQL |
| 84 | + |
| 85 | +普通CRUD用Spring Data JPA省事,但对复杂场景,他会担心自动生成SQL的性能有问题,还是手写SQL优化更直接。是不是挺纠结?为何要复杂查询?你说有一些统计报表需要。那你发现其中混淆关注点的地方?普通CRUD需经常改动数据库,而复杂查询使用频率很低。 |
| 86 | + |
| 87 | +之所以出现工具选型困难,是因为把两种数据使用频率不同的场景混杂。若将前台访问(处理CRUD)和后台访问(统计报表)分开,纠结也就不存在。 |
| 88 | + |
| 89 | +不同的数据变动方向还有很多,如: |
| 90 | + |
| 91 | +- 动静分离,把变和不变的内容分开 |
| 92 | +- 读写分离 |
| 93 | +- 高、低频 |
| 94 | +- 冷、热数据…… |
| 95 | + |
| 96 | +不同的数据变动方向,就是一个潜在的、可分离的关注点。 |
| 97 | + |
| 98 | +分离关注点,不只适用于宏观层面。在微观的代码层,用同样思维方式,也能识别一些杂糅代码。如很多人爱setter,但你真的有那么多要改变的东西?可能只是封装没做好。 |
| 99 | + |
| 100 | +## 7 分离关注点的重要性 |
| 101 | + |
| 102 | +- 不同关注点混在一起,会带来一系列问题,正如前面提到的各种 |
| 103 | +- 当分解得够细小,就会发现不同模块的共性,才有机会把同样信息聚合。为软件设计后续过程,即组合,做好准备 |
| 104 | + |
| 105 | +## 8 CQRS |
| 106 | + |
| 107 | +分离更新、查询两个关注点。 |
| 108 | + |
| 109 | +- 静态上,拆分这两块代码。各自采用不同技术栈,针对性调优 |
| 110 | +- 动态上,切分流量,更灵活资源分配 |
| 111 | + |
| 112 | +### 8.1 查询服务实现 |
| 113 | + |
| 114 | +可走从库,降低主库压力,也可水平扩展。但需注意数据延迟。在异步同步和同步多写上要做好权衡。 |
| 115 | +也可都走主库,这时: |
| 116 | + |
| 117 | +- 查询服务最好增加缓存,降低主库压力 |
| 118 | +- 而更新服务要做好缓存的级联操作,以保证缓存时效性 |
| 119 | + 当然也可走非关系型数据库,搜索引擎类的ES,solr,分布式存储的tidb等,按需选择。 |
| 120 | + |
| 121 | +通常增删改会涉及很多domain knowledge。平时更多的操作其实是查询,无需通过从持久化生成domain model到内存中再返回。 |
| 122 | + |
| 123 | +## FAQ |
| 124 | + |
| 125 | +### 订单系统 |
| 126 | + |
| 127 | +- 先下单写到DB |
| 128 | +- 然后发送消息给MQ |
| 129 | + |
| 130 | +这两步无法放到一个事务。若用本地消息表: |
| 131 | + |
| 132 | +- order写DB |
| 133 | +- 再写本地消息表 |
| 134 | + |
| 135 | +这两步就能放到一个事务,保证肯定成功。然后再有线程读本地消息表,MQ发消息,若成功,更改本地消息表状态。 |
| 136 | + |
| 137 | +### 分析 |
| 138 | + |
| 139 | +下单入库和发消息给下游确实是俩动作,但这俩动作顺序一定这样吗?一定要在一个线程完成吗?可不可以先发消息? |
| 140 | +比如,把消息发给下游后,有个下游接收到消息后,再把消息入库。 |
| 141 | +若这样,发消息,由MQ保证消息不丢,下游入库,又可保证订单持久化。这种设计下,其实并不需要事务,也就不必为事务纠结。 |
| 142 | + |
| 143 | +发现大家在工作中往往不做分离,分析需求的时候把方案揉在一起。 |
| 144 | + |
| 145 | +## 如何练习分离? |
| 146 | + |
| 147 | +写代码时,把自己写的函数行数限定在一定规模,如10行。超过10行的代码,仔细想想是否是有东西混在一起。这种方法锻炼找出不同关注点的思维习惯,一旦具备这种思维,再去看大设计,自然发现不同关注点。 |
| 148 | + |
| 149 | +## 用户购买会员 |
| 150 | + |
| 151 | +目前设计两张表: |
| 152 | + |
| 153 | +- 存储用户购买会员的所有记录 |
| 154 | +- 存当前会员信息 (开始、结束时间,但没有会员等级) |
| 155 | + 这张表是为SQL关联查询方便,无需再判断是否过期 |
| 156 | + |
| 157 | +但要用定时器一直扫这表,等会员过期,就删除对应记录。这么做的问题在哪? |
| 158 | + |
| 159 | +首先,没有把业务和实现分清。业务是实现一个会员系统,涉及会员购买,主要是会员时间要延长,还会涉及会员资格判断。 |
| 160 | + |
| 161 | +基于这些内容判断,可以有不同实现。根据当前实现,可以这样,购买会员: |
| 162 | + |
| 163 | +- 若会员信息不存在,则添加会员信息 |
| 164 | +- 会员信息存在,则修改会员结束时间 |
| 165 | + |
| 166 | +会员资格判别,根据用户 ID 和当前时间是否在时间范围内查询: |
| 167 | + |
| 168 | +- 记录存在,则是会员 |
| 169 | +- 否则不是 |
| 170 | + |
| 171 | +因此考虑: |
| 172 | + |
| 173 | +- 购买会员时,可产生会员购买记录,此记录仅供后续查询用 |
| 174 | +- 只有当会员信息表过大时,才考虑是否需要删除 |
| 175 | + |
| 176 | +在这个实现中: |
| 177 | + |
| 178 | +- 把 购买 和 会员信息 分开 |
| 179 | +- 把 会员信息是否生效 与 记录是否删除 分开 |
| 180 | + |
| 181 | +## 总结 |
| 182 | + |
| 183 | +软件设计第一步:分解。 |
| 184 | + |
| 185 | +大多数系统的设计做得不够好,问题常常出现在分解这步就没做好。常见的分解问题就是分解的粒度太大,把各种维度混淆在一起。在设计中,将一个模块的不同维度分开,有一个专门的说法,叫分离关注点。 |
| 186 | + |
| 187 | +分离关注点很重要,一方面,不同的关注点混在一起会带来许多问题;另一方面,分离关注点有助于我们发现不同模块的共性,更好地进行设计。分离关注点,是我们在做设计的时候,需要时时绷起的一根弦。 |
| 188 | + |
| 189 | +- 分离关注点是一种意识,在设计中要意识到需要分离关注点。一旦自己在做设计时,出现纠结或者是觉得设计有些复杂,首先需要想想,是不是因为把不同的关注点混在了一起。这种意识是需要训练的,让自己从无意识中摆脱出来。 |
| 190 | + 一种可以考虑的做法是,把它与一些实践结合起来,比如,在设计评审的DoD中,增加一条“是否考虑了分离关注点”。 |
| 191 | +- 分离关注点也要从小事开始练习 |
| 192 | + 比如,可以从编写小函数开始。给自己设定了一个目标,函数代码小于10行。每次写完代码,就可以对代码进行调整。超过10行代码的函数,问问自己,是不是有混在一起的内容? |
| 193 | + - 有循环,循环里面的部分就是对单个元素的处理,可以提取到一个函数里 |
| 194 | + - 有if...else,每种情况都可以单独放到一个函数里 |
| 195 | + - 有多个 if...else,要问问自己是不是缺少一些模型,是不是可以用多态 |
| 196 | + ... |
| 197 | + 经过练习,函数都能写短,那开始做类练习。把每个类写小,以此类推。 |
| 198 | + |
| 199 | +**分离关注点,发现的关注点越多越好,粒度越小越好。** |
| 200 | + |
| 201 | + |
0 commit comments