《代码规范》如何写出干净的代码(二)函数与方法

在这里插入图片描述

大家好,这是如何写出干净代码系列中的第二篇分享…求关注,求收藏,求点赞…非常感谢(PS:里面有一节拆分规则,如果有好的其他规则求告知博主,谢谢)

前言

在上一章节中我们讲述了在变量,注释,代码结构等多个方面如何进行规范化的描述(上一章节地址:《代码规范》如何写出干净的代码(一)),那么在这一章节,则是要主要对如何写出干净的函数和方法这一块进行更多的分享;

耐心看完,你一定有所收获;

函数的构成

首先我们明确,函数与方法并没有本质上的区别,定义在对象上的函数我们一般称之为方法,两者其实非常的接近,另外干净的函数包含两部分:
第一部分是函数的定义,也就是如下这种:

function getName(type){
    
    
  if(type==="name"){
    
    
    return "oliver";
  }
  else{
    
    
    return "oliver yin"
  }
}

第二部分则是函数的使用

getName("name")

只有两者都是易于阅读,逻辑清晰,分层明确,我们才能称之为干净的函数;

参数

参数,是函数或者方法中非常重要的一个点,无论是参数的个数,还是顺序,又或者是名字,都可能会给读者阅读代码造成一定的困惑,因此,干净的函数的参数从个人感觉上应该具备以下几点:

合理的参数数量

个人认为,参数的数量控制需要遵循一个规范,那就是:当前环境下最小化的参数数量,怎么理解呢,就是当前函数阅读者在看到后可以快速的理解、阅读那么就是最小化的,接下来看个例子吧

// 登录函数
function login(username, password, identity, code){
    
    
  // 登录代码
  loginRequest({
    
    
    username,
    password,
    identity,
    code
  }).then(res=>{
    
    })
}

// 执行登录
login("oliver", "123456", "admin", "123")

这个函数接收4个变量作为参数,从函数的定义上看4个变量的含义都是一目了然,即使不写注释,我们基本也能猜到是什么意思,分别是:用户名,密码,身份和验证码,名字和个数看上去觉得都相对比较合理,但是我们说,干净的函数还要包括使用,也就是下面执行登录时调用函数,由于login接收了4个参数,那么对于顺序的输入就变得十分重要,如果输错,那么函数的运行就会出现异常,因此显然这边就相对没有那么合理,那我们优化一下呢

// 登录函数
function login(user){
    
    
  // 登录代码
  loginRequest(user).then(res=>{
    
    })
}

const user={
    
    
  username,
  password,
  identity,
  code
}
// 执行登录
login(user)

这么改后,登录函数和执行函数都改成了接收一个对象作为参数,更上面相比好处就是此时我们不再需要关心参数的数量和顺序了,并且在扩展性上得到了提升,很明显这种就更合理;

再来看一个例子

// 接收两个参数
function log(msg, isError){
    
    
  if(isError){
    
    
    console.error(msg)
  }
  else{
    
    
    console.log(msg)
  }
}

log("oliver",false)

相对于上面那个例子,这个明显问题更大,别的不说,仅从使用者的角度来看, 根本理解不了第二个参数的意思是什么,需要阅读函数内部的逻辑才可以理解,给使用带来了一定的困难,那么这种代码很明显就可以拆分,参数的数量并不合理

// 重构
function log(msg){
    
    
  console.log(msg)
}

function logError(msg){
    
    
  console.error(msg)
}

当一个函数变成两个函数以后,明显对于代码的理解上来说有了非常大的提升,阅读的时候由于其单一性,因此并不会给理解带来困扰;

动态参数数量

上面说的参数数量其实都是固定的,那么还有一种函数或者方法,它的参数数量是不固定的,比如

const total = sum(1, 2, 3, 4, 5, 6);
const totalNext = sum(10, 11, 12, 14);

function sum(...numbers){
    
    
  let total = 0;
  for(let number of numbers){
    
    
    total += number;
  }
  return total;
}

通过阅读,我们知道这是一个求和的函数,并且参数的数量也是不固定的,那么这种函数需要优化吗,我个人认为是需要的,因为这种函数并不符合我们上面说的最小参数数量规则,那么怎么改,其实很简单

const total = sum([1, 2, 3, 4, 5, 6]);
const totalNext = sum([10, 11, 12, 14]);

简单的就是将其变成一个数组,那么参数的数量就会变得固定有且只有一个,这种就会变得相对合理一些,但是可能会有小伙伴问,上面那种不行吗?看着也很清晰,很合理,其实也行,不要忘记干净代码的核心就是代码结构清晰,易于理解,容易维护,只要团队成员一致觉得可行,那么其实就是最合理的,所谓的规则就是在为团队服务,并不是一成不变的东西;

编码规则

在函数体的编码过程中,有一些我个人认为是非常有意思的注意点,这些注意点可以帮助我们更好的写出干净的代码;

避免直接输出参数

比如某些情况下,我们可能需要对一个对象进行加工处理,之后再将这个对象返回,先看一个最不推荐的方式

// 不推荐
function createId(user){
    
    
  user.id = "oliver"
  // ...其他一些操作 
}

createId({
    
    name: "Oliver"})

这种为什么不推荐,因为在这个函数里面对用户这个对象进行了操作,这种操作从名字上看就是不可预知的,对于未知操作我个人非常不推荐因为会造成不可控的结果,稍微好一点的,那么会从函数的名字上会有体现,比如,明确需求是给用户添加一个id,那么代码可能如下:

// 相对合理些
function addId(user){
    
    
  user.id = "oliver"
}

addId({
    
    name: "Oliver"})

从名字上看,我们能知道这个函数的作用就是为用户添加一个id,因此和第一种比,这种函数我们是能预知到结果的,因此相对合理些,那么有没有更合理的方式,那肯定有的

// 个人觉得最优
class User{
    
    
  constructor(name){
    
    
    this.name = name;
  }
  addId(){
    
    
    this.id = "oliver";
  }
}

const customer = new User("Oliver");
customer.addId();

跟上面两种相比,是不是更加合理一些,我们实力化了一个用户,并且通过执行addId这个方法为customer添加了id这个属性,不管使用代码的结构上还是理解上都相对很清晰,因此,我个人认为这种方法相对最优;

函数体的功能单一

我想有一点肯定所有的小伙伴都会同意的,那就是函数体的代码长度,如果一个函数体的长度有个几百行上千行,换谁看了都会想给写的人突突突一下,那怎么理解代码长度呢?换个专业点的说法,就是代码的层次,结构清晰,功能单一,这样写出来的函数长度往往是可控的,足够的小巧精简,下面看一个例子:

function renderContent(renderInfomation) {
    
    
    const element = renderInfomation.element;
    if (element === "script" || element === "SCRIPT") {
    
    
        throw new Error("异常");
    }

    let partialOpeningTag = "<" + element;

    const attributes = renderInfomation.attributes;

    for (const attribute of attributes) {
    
    
        partialOpeningTag =
            partialOpeningTag +
            " " +
            attribute.name +
            "='" +
            attribute.value +
            "'";
    }

    const openingTag = partialOpeningTag + ">";

    const closingTag = "</" + element + ">";
    const content = renderInfomation.content;
    const template = openingTag + content + closingTag;

    const rootElement = renderInfomation.root;
    rootElement.innerHTML = template;
}

这段代码相信只要耐心看完就一定可以理解,简单的说就是一个创建标签的函数,从之前的规范性干净角度上来看,是符合我们之前说的一些规则的,但是个人觉得这段代码还不足够干净,为什么?因为我们需要花费一定的精力去阅读理解这段代码,而干净的代码要尽可能的让代码易于理解,那么如何优化呢,简单的说,就是拆分,让函数的功能变的单一:

function renderContent(renderInfomation) {
    
    
  const element = renderInfomation.element;
	const rootElement = renderInfomation.root;

  validateElementType(element);
	const content = createRenderableContent(renderInfomation);

	renderOnRoot(rootElement,content)
}

拆分后大致如上,函数的以不同的功能区域为边界进行拆分,将一个区域拆成一个函数,大致被分为了三步:

  1. 验证标签;
  2. 创建标签内部;
  3. 将标签插入到根节点;

是不是从结构上,代码量上都变得更易阅读了,这就是上面说的,以单一功能为边界进行函数拆封;可能有小伙伴还是不理解什么是单一功能,再举个例子吧,登录操作往往包含了校验,发送请求,那从功能上讲是不是就可以拆成校验一个函数,发送请求一个函数,这样应该能理解了吧;

其他拆分规则

除了上面说的以功能,操作为边界进行拆分外,还有一些其他的拆分规则可以帮助我们使得代码的长度得到精简,易于阅读;

公共函数的提取(DIY原则)

就是我们常说的DIY原则,“Don’t Repeat Yourself”,什么意思呢?当多个函数内部使用到了相同或者相似的代码时,那么对于这一部分代码就可以进行拆分,拆分后不管是从代码的长度,还是维护的角度都使得代码得到了提升,比如

function saveUser(user){
    
    
  if(!user.email.includes("@")){
    
    
    // 代码
  }
}

function getUser(user){
    
    
  if(!user.email.includes("@")){
    
    
    // 代码
  }
}

在这两段代码内,都存在一个校验参数user上email属性的过程,对于这一个过程两者完全一致,那么对于这种完全一致的代码我们就可以提取公共函数

function isEmail(email){
    
    
  return email.includes("@");
}

function saveUser(user){
    
    
  if(!isEmail(user.email)){
    
    
    // 代码
  }
}

function getUser(user){
    
    
  if(!isEmail(user.email)){
    
    
    // 代码
  }
}

提取后,公共代码得到了统一的维护与管理,并且一旦哪天发现代码编写错误有BUG,也可以直接修改公共函数,而不需要一个一个的修改;

(PS:其他规则待补充)

避免过度拆分

我们说到,拆分的根本目的是为了让我们的代码变的干净,使其更易理解,更易阅读,更易维护等等,因此拆分前不能忘记我们的初衷,简单的总结了一下,大致有以下几种情况可以不拆分我们的函数或者方法:

仅整合,重组的操作

什么意思呢?直接看例子吧

function saveUser(username,password){
    
    
  const user = buildUser(username,password);
  
  // 针对用户的其他操作...
}

function buildUser(username,password){
    
    
  return {
    
    
    username,
    password
  }
}

个人觉得这种就是典型的过度拆分,因为没有任何意义,即使不拆分会影响我们阅读代码吗,我个人感觉不会

function saveUser(username,password){
    
    
  const user = {
    
    
    username,
    password
  };
  
  // 针对用户的其他操作...
}

对吧,上面那种由于多了一个函数,反而我们需要花更多的时间去阅读理解;

查询新函数花费成本高

找到新函数的时间比阅读花费的时间更长,这个也好理解吧,从字面意思就可以理解,正如上文说的,我们拆分函数的目的是为了让函数体更干净,更易于阅读;
一旦过度拆分就会造成一个后果,我们是不是需要去寻找新函数,一旦新函数的寻找时间付出和收益不再成正比,那么我个人认为就不再合适拆分,或者说至少拆分后的位置不合理,这种那么就需要修改函数位置,或者让函数不再拆分就放在原来的位置即可;

无法合理命名拆分的函数

对于这种函数,我个人认为也不大适合拆分,什么意思呢?简单的说就是我们从原函数中拆分出来了一个段代码,但是针对这一段代码我们无法找到一个合适的名字来概括这段内容,或者说我们的函数名在阅读者阅读代码的时候无法快速的给人概括性的说明,举个例子吧

function createUser(username,password){
    
    
  const user = buildUser(username,password);
  
  // 针对用户的其他操作...
}

function buildUser(username,password){
    
    
  return {
    
    
    username,
    password
  }
}

在这段代码中,我们发现buildUser这个函数名和外界的createUser这个函数名非常接近,甚至会有一些歧义,那么对于这种其实就没有必要进行拆分;

小结

在本文中,简单讲述了一些使得函数和方法的代码变的相对干净的一些规则,大致如下:

  • 函数中参数的数量要合理,不要过多,依据当前环境下最小的参数这个原则进行函数参数的设计;
  • 在函数体中,我们要避免直接对参数进行输出,这样会有一些不可控的后果;
  • 函数的设计从功能上要尽量的单一,单一功能的函数不管是代码量还是可维护性都是相对较高的;
  • 我们要避免过度拆分函数,干净代码的本质是为了让我们更易阅读代码,而不是为了拆分而拆分;

猜你喜欢

转载自blog.csdn.net/zy21131437/article/details/123807212