Skip to content

Commit 76dc8df

Browse files
committed
docs:更新设计专栏
1 parent 331a9ca commit 76dc8df

File tree

5 files changed

+1322
-0
lines changed

5 files changed

+1322
-0
lines changed

docs/.vuepress/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,8 @@ module.exports = {
940940
sidebarDepth: 0,
941941
children: [
942942
"ChannelPipeline接口",
943+
"(06-1)-ChannelHandler 家族",
944+
"(08)-学习Netty BootStrap的核心知识,成为网络编程高手!",
943945
"(18)-检测新连接"
944946
]
945947
}],
@@ -1081,6 +1083,8 @@ module.exports = {
10811083
sidebarDepth: 0,
10821084
children: [
10831085
"代码的坏味道",
1086+
"分离关注点的意义",
1087+
"架构之美:教你如何分析一个接口?",
10841088
"模板方法设计模式(Template Pattern)",
10851089
"策略模式Strategy Pattern",
10861090
]
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
![](https://javaedge-1256172393.cos.ap-shanghai.myqcloud.com/%E5%88%86%E7%A6%BB%E5%85%B3%E6%B3%A8%E7%82%B9.png)

0 commit comments

Comments
 (0)