代码路漫漫,整洁伴我行

恍恍惚惚写代码已经这么久了,阅读过得代码也可以算是有一些了,有的代码看一眼就没有再看下去的欲望,有的就像别人评价雷军那样,代码如诗一般。
不知道大家有没有遇到过接手别人二手项目崩溃的时候,今天主要围绕
  • 如何做到代码清晰简洁
  • 为什么要代码清晰简洁
  • 代码清晰简洁的优点是什么
  • 代码的可维护性与效率
这几点简单分享一下我对代码整洁和代码思想的一些认识。
之前和朋友讨论面向对象等思想的时候,讨论了很久,毕竟每个人都有自己的思想,很难一下接受一个其他的思想,所以如有不足之处,请各位大佬批评指正。

混乱的代价

这段话放在开头,有些人在看到这个标题的时候,可能会直接就翻篇了,毕竟在一部分老板的眼里,只要能快速的出来产品就可以,像重构代码整理代码这些事情只会影响他们所谓的效率。

有些团队在开始的时候确实进展会快,但是随着项目体量不断变大,每次修改代码都要顾及之前的很多处代码的修改,一不小心就可能留下一个未知的bug,修复bug意味着团队生产力的下降,为了弥补速度,只能是一遍改bug,一遍加需求,越来越大量的投入人力,但是新人对项目是需要一个熟悉的过程的,当其中出现了一个人生孩子需要10个月 十个人生孩子需要1个月的这个梗出现的时候,那么那些新人就会迫于压力制造出更多的bug。

但凡你有两三年工作经验或者参加过外包公司,你一定会体验过这个过程。

一、开发者之基石 ——— 面向对象三大特性七大原则

面向对象可谓是老生常谈了

三大特性 封装、继承、多态

封装的目的就是“互不影响”,像红警里边兵营和车场(单一兵种),兵营老老实实造小兵,车场稳稳当当出坦克,不会说因为因为多了个车场,小兵就能直接开着坦克出来。

继承呢,就像特色武器,比如坦克杀手,相当于对之前车场的继承,他和之前车场一样有同样的坦克,也可以造出自己特色的坦克,其他没有这种特色的则不能造出。

然后多态,其实这么解释的话可能会对多态的理解有一些误解,多态是指基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。通俗点说也就是传入不同的参数得到的结果不同,但是实在封装的基础之上,继承和多态的区别在于,继承产生的是有具体事物的具象类,多态则是不针对具体的一个抽象类,还拿红警距离,我修改了游戏的ini脚本,我添加了一个新的建筑,他的能力就是产出兵种,我点击对应的选项,然后不管点击的什么东西,他的作用只有一个就是建造这个进来的东西,产出有封装的共同属性的这么一个兵种。也就是说不会具体到具体生产什么。因为他是抽象的。

class TK {
  go() {
    console.log("造出了一个灰熊坦克");
  }
}

const tk1 = new TK();
tk1.go();
tk1.go();

class bigTk extends TK {
  constructor() {
    super();
  }
  go() {
    console.log("造出了一个坦克杀手");
  }
}
const tk2 = new bigTk();
tk2.go();
tk2.go();

class NB {
  constructor(val) {
    let mark = val instanceof TK;
    if (!mark) {
      console.error("请放入正确实例");
      return;
    }
    this.activeObj = val;
  }
  go() {
    if (!this.activeObj) {
      console.error("传入参数不对");
      return;
    }
    console.log("我好想造点什么");
    this.activeObj.go();
    console.log("虽然不知道是个什么玩意,但是有图纸我就造出来了");
  }
}
const nb1 = new NB(tk1);
const nb2 = new NB(tk2);
nb1.go();
nb2.go();

结果如图

image.png

七大原则 单一、开闭、里氏替换、接口隔离、依赖导致、迪米特和聚合复用

1. 单一职责原则(Single Responsibility Principle)

每一个类应该专注于做一件事情。

2. 里氏替换原则(Liskov Substitution Principle)

超类存在的地方,子类是可以替换的。

3. 依赖倒置原则(Dependence Inversion Principle)

实现尽量依赖抽象,不依赖具体实现。

4. 接口隔离原则(Interface Segregation Principle)

应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。

5. 迪米特法则(Law Of Demeter)

又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。

6. 开闭原则(Open Close Principle)

面向扩展开放,面向修改关闭。

7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)

尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。

整体看来,用最简单的一个词语概括就是解耦,在日常的开发任务之中做不到解耦的代码,在维护起来一定是很痛苦的,举例说明如下。

 //公共请求方法(方法1)
methodQuery(methodModel) {
  // 全局赋值
  this.methodModel = methodModel
  // 取值
  let {
    loadingName,
    methodName,
    methodVal,
    pMehtod,
    callBack
  } = methodModel
  // 判断是执行函数还是promise
  let baseMethod = pMehtod ? pMehtod : this[methodName](methodVal)
  // loading
  loadingName && (this[loadingName] = true)
  // 请求
  baseMethod && baseMethod.then(res => {
      this.callBackThen(res)
  })
  .catch(() => {
    loadingName && (this[loadingName] = false)
  })
},


//promise回调(方法2)
callBackThen(res) {
  // 取值
  let {
    callBack,
    notReset
  } = this.methodModel
  // 回调函数
  callBack && this[callBack](res)
  // 关闭动作
  this.closeClass()
  // 刷新表格
  !notReset && this.resetPostTableBody()
},

//关闭动作(方法3)
closeClass(openMessage, mark) {
  // 取值
  let {
    loadingName,
    message,
    closeMarkName
  } = this.methodModel
  // 关闭的数组
  let markArr = mark ? mark : [closeMarkName, loadingName]
  // 关闭标记 
  markArr.forEach(element => {
    element && (this[element] = false)
  });
  // 提示消息
  let endMessage = openMessage ? openMessage : message
  endMessage && this.$commonUtil.setMessage('success', endMessage, true)
},

这个是我最开始接触前端的时候封装的一个方法,甚至当时觉得完美无瑕,一套流程下来,程序跑的顺顺当当,直到后来业务扩展,发现函数一环套一环,多余出来的逻辑无法满足,但由于项目体积过大,时间紧,只好一点点的往里边填充各种判断,和横向扩充修改,这无疑是很明显的违反了单一和开闭原则。

感谢公司信任,直到后来项目做H5 被改进了一下如下


methodQuery(model) {
        // loading名称 ,消息提示 , promise或方法名称 ,方法参数,回调
        let { loadingName, message, pMethod, params, callBack } = model
        // 打开loading
        loadingName && (this[loadingName] = true)
        // 判断传进来的是promise 还是 方法名称
        let method = typeof pMethod == 'string' ? this[pMethod](params) : pMethod
        // 操作promise
        method.then(async res => {
            message && Toast.success(message || '操作成功')
            // 回调方法
            callBack && await this[callBack](res)
        }).finally(_ => {
            // 关闭loading
            loadingName && (this[loadingName] = false)
        })
    },
    

又到最后从外边改成了建造者模式

我感觉这些特性和原则里边最重要的,首先合理的多态,合理的多态可以让你的代码更加灵活,下边的原则都是围绕上边的特性来讲的,假设你写了一个不参与业务的抽象方法,一个方法有多种实现和你多种方法复制多份比较起来,哪个方便维护和阅读不言而喻吧。

其次最简单的要知道一个单一原则,一个开闭原则,一个函数只做一件事,不要让自己和别人回过头来看自己代码的时候每次都要找了五分钟才能找到重点,浪费了别人的时间。

说到这里我比如吐槽一下最近二开的一个php的项目

image.png
image.png

姑且不说这大段的各种姿势的循环,我万万没有想到一个函数居然有700行之巨!!!真是不吐不快!

回忆起这番经历的时候,我只是感觉出来混早晚是要还的,或者后期维护的时候自己一边改自己的代码一边骂自己2B,或者别人改自己代码的时候骂自己是2B,而且写出方便维护的代码不会出奇奇怪怪的bug,也能更少的加班何乐而不为呢。

二、开发者之矛 ———— 设计模式的思想与清晰的目录结构划分

设计模式,记得有一次面试的时候,问到那个面试者,你常用的设计模式有哪些,然后他回答我按照网上的例子写了几次,感觉很不方便,后来就不怎么用了。

在这里我想表达的是,写代码切记不要生搬硬套,重要的是思想。

也会有人说前端不需要设计模式,这显然是不正确的,设计模式在oo社区里被广泛的应用与推广,设计模式作为设计思想,与语言无关。

简单介绍下我比较喜欢的几种设计模式 ,建造者、抽奖工厂、策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

我对策略模式的认识说来惭愧还是因为大段的 if else 判断的情况下被老同志批评之后才认识的,当时真的是有一种豁然开朗的感觉。简单举个栗子。

go(val) {
  if (val === 'a') {
    return !!val
  }
  if (val === 'b') {
    console.error('123')
  }
  if (val === 'c') {
    return val || 'reset'
  }
}

如上代码一个函数里边判断了四种情况,针对不同的情况去执行不同的操作,在这种情况下。

  • 首先假设用户加了十种情况,那么这段代码的阅读行将会无比的差,在扩展的时候缺乏弹性。
  • 其次算法的复用性差,比如其他地方有类似其中的某一种算法的时候,这部分代码不能服用,那么怎么办呢,我相信肯定会有人出现复制的想法,那么就又陷入了代码维护工作量巨大的问题。

更改后代码如下

let demoConfig = {
  a: 'a',
  b: 'b',
  c: 'c'
}
a(val) {
  return !!val
}
b() {
  console.error('123')
}
c(val) {
  return val || 'reset'
}
go(val) {
  let methodName = Reflect.get(demoConfig, val)
  methodName(val)
}

那么更新之后的这段代码有什么优点呢

  • 参与了更少的业务逻辑,增强各个算法的可移植性,比如可能多个地方都用到了算法a等
  • 有了单独的配置项之后,可以清晰的知道,当参数是某种策略后会去做什么事情,而不是在大篇幅的代码中寻找自己想要的策略

同样为什么说设计模式是一种思想,这个思想可以用到任何适用的地方,比如表单校验,比如环境变量再比如写html厌倦了,换个口味jsx中的模板渲染。

还有不得不说的之前遇到的大段的if else 升级版 switch

const expr = 'Papayas';
switch (expr) {
  case 'Oranges':
    console.log('Oranges are $0.59 a pound.');
    break;
  case 'Mangoes':
  case 'long':
    console.log('Mangoes and papayas are $2.79 a pound.');
    // expected output: "Mangoes and papayas are $2.79 a pound."
    break;
  case 'Papayas':
    console.log('Mangoes and papayas are $2.79 a pound.');
    // expected output: "Mangoes and papayas are $2.79 a pound."
    break;
  case 'Oranges':
    console.log('Oranges are $0.59 a pound.');
    break;
  default:
    console.log(`Sorry, we are out of ${expr}.`);
}

之前抽象工厂和建造者写过博客就不赘述了

抽象工厂点击传送门

建造者模式 点击传送门

这两种设计模式属于创建型模式,个人认为这种创建型模式,很适用于那种重复功能且中规中矩定制的外包项目,前期花一些时间,把各个代码工厂零件写好之后,通篇配置项就可以完成一个完整的产品,举个例子,A的商城中需要订单、购物车、商品列表,B的企业官网需要商品列表、文章列表,C的博客站需要文章列表、书籍商品列表、购物车

那么我们完全没有必要去做重复的工作把每一份代码都重新定制一遍。

或许你会说这三个项目每个项目都有自己独立的逻辑,生搬硬套肯定不使用,这又要讲到另一个思想,叫做依赖注入,控制反转。

为什么前边说要尽可能吧把业务和基础拆分开,就前端而言,写ui的时候颗粒度一定要尽可能的细化,你的组件和页面模板不参与业务,我们只需要写出使用的业务注入到页面模板当中去达到我们要的业务就可以了。

再比如说现在git上边那么多的开源的各种ui组件库,各种的开源的网站生成器,代码生成器等等等等,我想他们在做项目的时候肯定不会说每一个项目都要重新写一遍的那种。

所以总结来说,写代码最关键的是一个思想的问题。

一个清晰的目录结构,写起代码起来事倍功半,拿vue举例

  • 组件
  • 页面
  • 工具类
  • 混入
  • 请求层
  • 中间件层
  • 业务层
  • 枚举
  • 静态资源
  • 初始化 扫描等配置

还有里边的细化,业务组件,基础组件,业务方法,基础方法,能否公用之类的

之前写的一个仿vue-cli的脚手架 zjs-template-cli 有node 环境的话直接

npm i zjs-template-cli -g

zjs-cli init demo

就可以生成一个vue的脚手架,也是之前刚接触前端的时候做的,大佬们多多批评,最近会更新一版ts的到这个上边,喜欢的可以试试哦~~~ (^▽^)

三、开发者之盾 ———— 防御性编程的意识

防御性编程(Defensive programming)是一种具体体现,它是为了保证,对程序的不可预见的使用,不会造成程序功能上的损坏。它可以被看作是为了减少或消除墨菲定律效力的想法。防御式编程主要用于可能被滥用,恶作剧或无意地造成灾难性影响的程序上。

上边这句话摘自百度百科

首先来聊一下什么叫做防御性编程,个人愚见防御性编程的这种意识,是对我们开发者的一个保护,在开发项目中避免我们遇到许多莫名其妙的不在预期之中的问题,是减少bug和提高工作效率的一个意识。

防御式编程的主要思想:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生错误数据。更一般地说,其核心想法是要承认程序都会有问题,都需要被修改

比如说


main(model){
   this.name = model.name 
}

上边这个main函数,应该是接受一个model对象,里边有一个name属性,执行的时候给this.name 赋值。

正常情况下,我们传入了一个正确的参数得到了我们预期的结果,但是有一天一个异步请求没有处理好结果还在路上,或者model被莫名的赋予了一个空值,这个main函数就会抛出一个model is undefined的错误

所以我们应该加一个判断的动作,来保证我们的函数每次都准确无误的达到预料的预期,减少寻找bug的时间

main(model){
    let isExpect =  typeof obj === 'object' && obj !== null
    if(!isExpect){
        console.error('model上不存在name属性')
        return
    }
    this.name = model.name
}

这种写法可能会有很多人觉得,完全没有必要,我有朋友也曾提出反对的意见

  • 他认为,每一部分代码执行都是需要时间和空间的,哪怕是声明了一个变量,在一个函数里边可能感觉不出什么,但是在项目中大量的添加这种防御措施的话,可能就会有明显的感觉。
  • 另外一个,他认为所有的代码都要做这种动作是一个很麻烦的事情

所以我举了一个例子去反驳他,在之前做的项目里边,由于我没有做接口入参的类型校验和字段判断文档也不是很健全,前端请求的时候,有很多时候返回的是400,但是也没有给前端指出入参到底哪里不对,导致前端两眼一抹黑,寻找字段占用了大量的时间。

当然防御性编程也不只是在于代码,就前端而言当键盘在回车上桥下的那一刻,dns之前的都抛开,首先加载网站当前页的静态资源,之后请求后端接口,拿到数据后开始渲染,我们要在各个环节防止异常情况的发生,还有保证用户体验等等,因为用户体验被拉去改代码的时候可也有不少次。

在开发中,经常会有从列表页到详情页的时候需要异步请求数据,不做任何处理的时候进去页面,数据还没有回来,就会达不到预期的效果,常用的方法有给页面根元素加一个判断,等数据回来的时候在渲染页面,好一点在请求的过程中加一个loading,防止页面有破碎感。

当然这也会导致另一个问题,就是比如后端sql巨慢无比的时候,用户就可能会等待相当长久的loading,或者无法忍受而放弃使用,所以这个时候应该是有一个判断接口超时的动作给用户操作,像axios就有一个默认超时的设置,或者取消掉这个请求让用户有操作的空间。

还有一个就是在发送请求的时候,点击按钮loading的开关,或者加一个节流或者防抖,防止用户重复请求多次接口。我常用的方法就是封装一个请求方法在请求之前打开开关,结束后关闭开关,也可以做一个全局的loading,不过pc端的话 可能会影响用户的其他操作

总结

影响代码整洁和开发效率的还有很多,比如各种有意义的变量名称,不要拼音,健全的注释等等。
代码缩进整齐,没有多余的注释,这些是基本要求。
推荐一本书《代码整洁之道》,这本书里边讲了一个例子让我记忆犹新,一个很受欢迎的产品到最后公司倒闭的故事,推荐大家看一看。

然后,零零散散一大堆,深知自己还有很多不足,如果文中发现有不对不足之处,再次请大家能批评指正。

最后 喜欢的点个赞吧 (^▽^) ~~

阅读 1.1k

推荐阅读
前端笔记
用户专栏

坚持分享,共同进步

3 人关注
10 篇文章
专栏主页