从一次不是很SOLID的实践浅聊设计原则

471 阅读8分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

概述

从一次实践中,重新思考 SOLID 设计原则。

前言

不知道各位有没有这样的经历——每次看别人写的 SOLID 设计模式案例时好像都很好理解,也感觉自己已经看懂了,但在真正写代码的时候总感觉哪哪都不对劲,又说不出哪里有问题。想把代码按 SOLID 的几条原则重新调整一下,但写出来的结果总感觉很别扭,一遍又一遍回想别人的案例,对照着自己的代码,又感觉好像不是很像。然后一切又回到最初。。。

很多次都是这样的情况,总感觉只是掌握看懂了招式,但没掌握内功心法,驾驭不了这上乘武功 (〒︿〒)

任务概要

很抱歉,没有选通用简单的示例来演示,选了一个真实开发场景下的需求作为演示 Demo,需要花点时间理解一下我在做什么  ̄▽ ̄

后续会基于 demo-solid 项目进行展开,有兴趣的朋友可以 clone 下来跑一下,对照源码和本文内容。

这次任务核心需求是比较 Scratch 的两个代码块是否完全相同,如下图所示:

分析需求,接下来我们的目标就是怎样定义两个代码块是否完全相同?可以从以下三个维度进行对比判断:

  1. 代码块中的积木数量是否相同
    • 如果积木数量都不同,那两个代码块肯定不同,这个相对比较好判断
  2. 两个代码块,在相同的位置上的积木必须满足以下两个条件:
    • 积木的类型是相同的
    • 积木的内容也是相同的
  3. 循环代码块,判断代码块中是否所有积木都满足第二个条件

ok,到这里,基本上核心算法逻辑基本明确,只要解决了第二步基本上就能实现需求。

接下来,我们来看一下上图两个代码块的原始数据:data

从上图可以看出,代码块的原始数据结构是这样的:

代码块 = {
  "积木 id": {
    积木
  },
  "积木 id": {
    积木
  }
}

代码块 = {} 整个代码对象中,积木通过 key - value 的形式存放在代码块的对象中,其中 key 是积木的 积木 idvalue 是积木对象。积木与积木之间的连接核心通过 nextparent 两个字段串联起来

积木的各个字段含义可参考:interface-block

ok,到这里我们再把问题转化一下:

  1. 将上述代码块对象转换成一个数组
    • 数组中的前后位置信息表示积木的在代码块中的连接信息
    • 两个代码块转成的数组的数据结构将会是这样的: [{积木}, {积木}, {积木}][{积木}, {积木}, {积木}]
    • 这样,我们只需要循环两个数组,比较下标 0、1、2。。。各个位置中的积木的类型和内容是否相同即可
    • 这一步在:rope.ts 中完成,大概流程如下图:
      • rope 流程
  2. 积木中包含两种内容,inputsfields,我们需要分别比较两个积木的这两中内容是否相同
    • 对于 inputsfields 的判断,又分成下图中的两种情况
      • 红色框和字的情况是指 inputsfields 中的是具体的值
      • 黄色框和字的情况是指 inputsfields 中的是下一个积木的 积木 id

ok,到这里基本就明确要实现的内容了,接下来梳理 SOLID 设计原则的内容将会围绕以下两个内容展开:

  1. rope.ts 将代码块对象转成一个数组
    • 从上面 “rope 流程” 一图中可以看出,这一步很核心的地方在于识别 inputsfields 中的下一个积木的 积木 id
  2. 判断 inputsfields 内容是否相同
    • 这一步核心的点是根据两种情况,判断内容是否相同

不考虑 SOLID 的做法

rope.ts 将代码块对象转成一个数组

焦点先去到 rope.ts 文件中,从上文了解到,rope.ts 的主要作用是将 data 中的代码块对象转换成数组,核心的地方在于识别 inputsfields 中的下一个积木的 积木 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];
  }
};

判断 inputsfields 内容是否相同

matcher 中:

inputsFieldsMatcher 中:

里面用到 matcher 目录下的 case.tsfabric.ts

小节一下

说明一下,以上简单粗暴的方案虽然在代码中注释掉了,但实际上也是可以实现需求的,但会有以下核心问题:

嗯嗯,接下来我们考虑 SOLID 后的优化,为的就是把这两套判断代码合并成一套 (o゚▽゚)o

考虑 SOLID 后的优化

木有用上 SOLID 中的所有原则,只用了 “S” 和 “D”

rope.ts 将代码块对象转成一个数组

焦点再次去到 rope.ts 文件中:

translator 的功能实际上就是将 inputsfields 内容翻译出来,得到以下数据结构:(Translation

export interface Translation {
  key: InputsAndFieldsKeyEnum; // inputs 或 fields 的 key (类型)
  blockId: string | null; // inputs 或 fields 里面的下一个积木的 积木 id
  value: string | number | null; // inputs 或 fields 里面的具体的值
}

判断 inputsfields 内容是否相同

inputsFieldsMatcher 中:

小节一下

对比优化前后的内容,比较关键的是 translator 目录下的 case.tsfabric.ts 以及 matcher 目录下的 case.tsfabric.ts

其中:

  • 优化前,简单粗暴的方案使用的是 matcher 目录下的 case.tsfabric.ts
  • 优化后,考虑 SOLID 设计原则后的方案使用的是 translator 目录下的 case.tsfabric.ts

SOLID 中的 “D”

SOLID 中的 “D” 是指:“依赖反向原则”(Dependency Inversion Principle)

请允许在下用自己的理解来解释这个原则,在下通俗一点来理解,这个原则核心的点是告诉我们要把事务抽象出来,整理整理共同点

怎样理解?

再回到这张图:

思考一下,为什么需要写两套差不多的判断?或者说这两套判断有什么共同点?差异点?

  • 共同点:两套判断都是为了识别 inputsfieldskey (类型)对应内容的数据结构中的值,然后根据识别出来的结果去做某些事情
  • 差异点:要做的事情不同
    • 左边是为了判断 targetother 识别出来的 inputsfields 的内容是否相同
    • 右边是为了获得 inputsfields 中的是下一个积木的 积木 id

既然这样,那就好办啦,只要把共同点抽象出来,就可以把两套判断合并成一套 (*゚∀゚)=3

再进一步,整件事最核心的点转移到,如何抽象出一台“翻译器”,用来翻译不同情况、不同数据结构下的内容,“翻译”出来的内容要区分上文中提到的:

  • inputsfields 中的具体的值
  • inputsfields 中的下一个积木的 积木 id

得到这个翻译结果后,后面需要根据这个翻译结果需要做什么事情就变得很简单了

这种感觉就像,当你不懂任何外文却出国旅游时,带上一位会外文的朋友,出国旅游的事情就变得相对简单,想住酒店也行,想住民宿也可以。泡温泉木有问题,去滑雪也 ok。让懂外文的朋友帮忙翻译就 ok。

SOLID 中的 “S”

SOLID 中的 “S” 是指:“单一职责原则”(Single Responsibility Principle)

所谓的“单一职责原则”,网上比较多的解释是:每个模块只做一件事情,只有单一的一个职责

个人更喜欢这个大牛的解释: ( 参考:使人瘋狂的 SOLID 原則:單一職責原則 (Single Responsibility Principle)

一个模块应该只对唯一的一个角色负责

这里重点提一下 translator 目录下的 case.tsfabric.ts

  • case.ts
    • 里面每个函数的作用就是负责好自己所属的那种“情况”,根据自己的“情况”,“翻译”出:
      • inputsfields 中的具体的值
      • inputsfields 中的下一个积木的 积木 id
    • 他们只对这件事情负责,别的不管
  • fabric.ts
    • 里面每个函数的作用是负责自己所属的那种数据结构,并且把数据结构里面的内容提取出来
    • 他们只对这件事情负责,别的不管

虽然 SOLID 一开始是基于面向对象的设计原则,但个人觉得并不一定仅局限在写 class 对象的代码中,SOLID 是一种原则,更是一种编程思想,无论我们是写 class 还是写 functioin 都可以借鉴。

不知不觉,写了快 7 个小时,写到最后一点不知道自己在写什么的感觉 ╥﹏╥...

各位看官请原谅我,看到这里可能各位还是一脸懵逼,说了那么多,啥才是 SOLID ???

嗯嗯,确实,有点乱,也许文中的说明也不准确,但没事,只要有点点新的东西、新的理解,足矣

怎样写代码?怎样写好代码?,是两个截然不同的问题

怎样写好代码? 的道路上,在下还有很长的路要走,下次有新的思考时再迭代这次的结论,甚至推翻这次的结论,前进的道路才充满的未知的吸引 o( ̄▽ ̄)d

参考文献

最后,这次思考中的参考文献感觉质量不错,特意记录一下,后续有机会再回顾回顾: