我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
概述
从一次实践中,重新思考 SOLID 设计原则。
前言
不知道各位有没有这样的经历——每次看别人写的 SOLID 设计模式案例时好像都很好理解,也感觉自己已经看懂了,但在真正写代码的时候总感觉哪哪都不对劲,又说不出哪里有问题。想把代码按 SOLID 的几条原则重新调整一下,但写出来的结果总感觉很别扭,一遍又一遍回想别人的案例,对照着自己的代码,又感觉好像不是很像。然后一切又回到最初。。。
很多次都是这样的情况,总感觉只是掌握看懂了招式,但没掌握内功心法,驾驭不了这上乘武功 (〒︿〒)
任务概要
很抱歉,没有选通用简单的示例来演示,选了一个真实开发场景下的需求作为演示 Demo,需要花点时间理解一下我在做什么
 ̄▽ ̄
后续会基于 demo-solid 项目进行展开,有兴趣的朋友可以 clone 下来跑一下,对照源码和本文内容。
这次任务核心需求是比较 Scratch 的两个代码块是否完全相同,如下图所示:
分析需求,接下来我们的目标就是怎样定义两个代码块是否完全相同?可以从以下三个维度进行对比判断:
- 代码块中的积木数量是否相同
- 如果积木数量都不同,那两个代码块肯定不同,这个相对比较好判断
- 两个代码块,在相同的位置上的积木必须满足以下两个条件:
- 积木的类型是相同的
- 积木的内容也是相同的
- 循环代码块,判断代码块中是否所有积木都满足第二个条件
ok,到这里,基本上核心算法逻辑基本明确,只要解决了第二步基本上就能实现需求。
接下来,我们来看一下上图两个代码块的原始数据:data
从上图可以看出,代码块的原始数据结构是这样的:
代码块 = {
"积木 id": {
积木
},
"积木 id": {
积木
}
}
在 代码块 = {} 整个代码对象中,积木通过 key - value 的形式存放在代码块的对象中,其中 key 是积木的 积木 id,value 是积木对象。积木与积木之间的连接核心通过 next、parent 两个字段串联起来
积木的各个字段含义可参考:interface-block
ok,到这里我们再把问题转化一下:
- 将上述代码块对象转换成一个数组
- 数组中的前后位置信息表示积木的在代码块中的连接信息
- 两个代码块转成的数组的数据结构将会是这样的:
[{积木}, {积木}, {积木}]、[{积木}, {积木}, {积木}] - 这样,我们只需要循环两个数组,比较下标 0、1、2。。。各个位置中的积木的类型和内容是否相同即可
- 这一步在:rope.ts 中完成,大概流程如下图:
- 积木中包含两种内容,
inputs和fields,我们需要分别比较两个积木的这两中内容是否相同- 对于
inputs和fields的判断,又分成下图中的两种情况- 红色框和字的情况是指
inputs和fields中的是具体的值 - 黄色框和字的情况是指
inputs和fields中的是下一个积木的积木 id
- 对于
ok,到这里基本就明确要实现的内容了,接下来梳理 SOLID 设计原则的内容将会围绕以下两个内容展开:
- rope.ts 将代码块对象转成一个数组
- 从上面 “rope 流程” 一图中可以看出,这一步很核心的地方在于识别
inputs和fields中的下一个积木的积木 id
- 从上面 “rope 流程” 一图中可以看出,这一步很核心的地方在于识别
- 判断
inputs和fields内容是否相同- 这一步核心的点是根据两种情况,判断内容是否相同
不考虑 SOLID 的做法
rope.ts 将代码块对象转成一个数组
焦点先去到 rope.ts 文件中,从上文了解到,rope.ts 的主要作用是将 data 中的代码块对象转换成数组,核心的地方在于识别 inputs 和 fields 中的下一个积木的 积木 id,即下图:(在 rope.ts 的 iterator 函数 中)
上图中的 getInputAndFieldBlockId 函数长这样:
/*
获取以下数据结构中的 积木 id (简单粗暴,不用考虑什么设计原则,直接开干)
"SUBSTACK": [
2,
"2dxV$n*C1TXVr@DuTZ)["
]
*/
export const getInputAndFieldBlockId = (key: string, value: any) => {
if (key.includes('SUBSTACK') || key.includes('TO')) {
return value[1];
}
};
判断 inputs 和 fields 内容是否相同
在 matcher 中:
在 inputsFieldsMatcher 中:
里面用到 matcher 目录下的 case.ts 和 fabric.ts
小节一下
说明一下,以上简单粗暴的方案虽然在代码中注释掉了,但实际上也是可以实现需求的,但会有以下核心问题:
- case.ts 和 getInputAndFieldBlockId 中的判断有重复书写的嫌疑
嗯嗯,接下来我们考虑 SOLID 后的优化,为的就是把这两套判断代码合并成一套 (o゚▽゚)o
考虑 SOLID 后的优化
木有用上 SOLID 中的所有原则,只用了 “S” 和 “D”
rope.ts 将代码块对象转成一个数组
焦点再次去到 rope.ts 文件中:
translator 的功能实际上就是将 inputs 和 fields 内容翻译出来,得到以下数据结构:(Translation)
export interface Translation {
key: InputsAndFieldsKeyEnum; // inputs 或 fields 的 key (类型)
blockId: string | null; // inputs 或 fields 里面的下一个积木的 积木 id
value: string | number | null; // inputs 或 fields 里面的具体的值
}
判断 inputs 和 fields 内容是否相同
在 inputsFieldsMatcher 中:
小节一下
对比优化前后的内容,比较关键的是 translator 目录下的 case.ts、fabric.ts 以及 matcher 目录下的 case.ts、fabric.ts
其中:
- 优化前,简单粗暴的方案使用的是
matcher目录下的 case.ts、fabric.ts - 优化后,考虑 SOLID 设计原则后的方案使用的是
translator目录下的 case.ts、fabric.ts
SOLID 中的 “D”
SOLID 中的 “D” 是指:“依赖反向原则”(Dependency Inversion Principle)
请允许在下用自己的理解来解释这个原则,在下通俗一点来理解,这个原则核心的点是告诉我们要把事务抽象出来,整理整理共同点
怎样理解?
再回到这张图:
思考一下,为什么需要写两套差不多的判断?或者说这两套判断有什么共同点?差异点?
- 共同点:两套判断都是为了识别
inputs或fields的key(类型)对应内容的数据结构中的值,然后根据识别出来的结果去做某些事情 - 差异点:要做的事情不同
- 左边是为了判断
target和other识别出来的inputs或fields的内容是否相同 - 右边是为了获得
inputs或fields中的是下一个积木的积木 id
- 左边是为了判断
既然这样,那就好办啦,只要把共同点抽象出来,就可以把两套判断合并成一套 (*゚∀゚)=3
再进一步,整件事最核心的点转移到,如何抽象出一台“翻译器”,用来翻译不同情况、不同数据结构下的内容,“翻译”出来的内容要区分上文中提到的:
inputs和fields中的具体的值inputs和fields中的下一个积木的积木 id
得到这个翻译结果后,后面需要根据这个翻译结果需要做什么事情就变得很简单了
这种感觉就像,当你不懂任何外文却出国旅游时,带上一位会外文的朋友,出国旅游的事情就变得相对简单,想住酒店也行,想住民宿也可以。泡温泉木有问题,去滑雪也 ok。让懂外文的朋友帮忙翻译就 ok。
SOLID 中的 “S”
SOLID 中的 “S” 是指:“单一职责原则”(Single Responsibility Principle)
所谓的“单一职责原则”,网上比较多的解释是:每个模块只做一件事情,只有单一的一个职责
个人更喜欢这个大牛的解释: ( 参考:使人瘋狂的 SOLID 原則:單一職責原則 (Single Responsibility Principle) )
一个模块应该只对唯一的一个角色负责
这里重点提一下 translator 目录下的 case.ts、fabric.ts
- case.ts
- 里面每个函数的作用就是负责好自己所属的那种“情况”,根据自己的“情况”,“翻译”出:
inputs和fields中的具体的值inputs和fields中的下一个积木的积木 id
- 他们只对这件事情负责,别的不管
- 里面每个函数的作用就是负责好自己所属的那种“情况”,根据自己的“情况”,“翻译”出:
- fabric.ts
- 里面每个函数的作用是负责自己所属的那种数据结构,并且把数据结构里面的内容提取出来
- 他们只对这件事情负责,别的不管
结
虽然 SOLID 一开始是基于面向对象的设计原则,但个人觉得并不一定仅局限在写 class 对象的代码中,SOLID 是一种原则,更是一种编程思想,无论我们是写 class 还是写 functioin 都可以借鉴。
不知不觉,写了快 7 个小时,写到最后一点不知道自己在写什么的感觉 ╥﹏╥...
各位看官请原谅我,看到这里可能各位还是一脸懵逼,说了那么多,啥才是 SOLID ???
嗯嗯,确实,有点乱,也许文中的说明也不准确,但没事,只要有点点新的东西、新的理解,足矣
怎样写代码? 和 怎样写好代码?,是两个截然不同的问题
在 怎样写好代码? 的道路上,在下还有很长的路要走,下次有新的思考时再迭代这次的结论,甚至推翻这次的结论,前进的道路才充满的未知的吸引 o( ̄▽ ̄)d
参考文献
最后,这次思考中的参考文献感觉质量不错,特意记录一下,后续有机会再回顾回顾: