104

巧用匿名函数重构你的代码

 6 years ago
source link: https://iammapping.com/the-good-things-of-fn/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

anonymous function

匿名函数最早是LISP语言引入,后面发展为不仅是函数式语言所特有,在解释型语言和编译型语言中也越来越多地看到匿名函数的身影,它或许有个更潮的名字叫 lambda 表达式。

闭包多是用匿名函数实现,在匿名函数中引用了外部变量,那这个匿名函数就形成了闭包。由于闭包和匿名函数之间有着千丝万缕的关系,所以经常会把两者搞混淆。其实在 Js 中匿名函数、闭包、自执行函数、回调函数、箭头函数,这些概念似乎相同,却又不同,请读者朋友自行了解。

匿名函数拥有可动态编程的执行过程。巧妙使用可以让你的代码简约而不失优雅,灵活而不失约束。好了,正式切入本文的正题,巧用匿名函数重构代码。按照重构的惯例,先指出代码中的坏味(Bad Smell):

  • 定义冗长的重复配置
  • 条件多变的集合过滤
  • 说一不二的方法调用

定义冗长的重复配置

在写配置代码时,经常会遇到大量重复的配置,如果要修改一些内容,所有的都要修改,着实很累,而且容易遗漏。比如:

{
  html: `
    <label><input type="checkbox" name="apple" /> Apple</label>
    <label><input type="checkbox" name="banana" /> Banana</label>
    <label><input type="checkbox" name="orange" /> Orange</label>
  `
}

这里面有三个选项,三个选项的结构完全一样,如果要给所有选项加一个 title,那你就要把这件事重复做三遍。

{
  html: `
    <label><input type="checkbox" name="apple" title="Apple" /> Apple</label>
    <label><input type="checkbox" name="banana" title="Banana" /> Banana</label>
    <label><input type="checkbox" name="orange" title="Orange" /> Orange</label>
  `
}

程序猿是个奇怪的群体,他们宁愿做五件不同的事,也不愿重复三次做同一件事。所以这怎么能忍,把配置中不同的内容提取出来:

{
  html: (function(fruits) {
    return fruits.map(it => `<label><input type="checkbox" name="${it.name}" title="${it.title}" /> ${it.title}</label>`).join('');
  }([
    {name: 'apple', title: 'Apple'},
    {name: 'banana', title: 'Banana'},
    {name: 'orange', title: 'Orange'}
  ]))
}

这样的修改对配置的调用方来说是完全透明的,但是对于配置的维护者来说,将结构和数据分离开,要改结构就改自执行方法体,要改数据就改传入自执行方法的参数,可以大大减少犯错的风险,又避免一些无脑的复制粘贴。

虽然在配置中写代码逻辑不是特别推荐的做法,但相对于代码可维护性来说这不算啥了。

条件多变的集合过滤

集合过滤是个非常常见的需求,假设有个学生集合:

let students = [
  {name: 'Lucy', age: 20, sex: 'female'},
  {name: 'LiLei', age: 21, sex: 'male'},
  {name: 'Jim', age: 18, sex: 'male'}
]

现在要过滤出年龄 20 岁的同学:

function filterByAge(list, age) {
  let filtered = [];
  for (let i = 0; i < list.length; i++) {
    if (list[i].age === age) {
      filtered.push(list[i]);
    }
  }
  return filtered;
}

又要过滤出姓名叫 LiLei 的同学:

function filterByName(list, name) {
  let filtered = [];
  for (let i = 0; i < list.length; i++) {
    if (list[i].name === name) {
      filtered.push(list[i]);
    }
  }
  return filtered;
}

还要过滤出性别为男的同学:

function filterBySex(list, sex) {
  let filtered = [];
  for (let i = 0; i < list.length; i++) {
    if (list[i].sex === sex) {
      filtered.push(list[i]);
    }
  }
  return filtered;
}

就在你觉得大功告成,可以站起弯伸腰的时刻,突然被一股强大的力量按回了座椅,帮我找出姓名以 “L” 开头的童鞋。虽然你内心是 mmp~,但还能怎么办,写啊,于是又吭哧吭哧加了如下方法:

function filterByNameStart(list, nameStart) {
  let filtered = [];
  for (let i = 0; i < list.length; i++) {
    if (list[i].name.indexOf(nameStart) === 0) {
      filtered.push(list[i]);
    }
  }
  return filtered;
}

于是乎,这个调用方式:

filterByName(filterBySex(filterByAge(students, 21), 'male'), 'LiLei'); 

就可以理解为找出年龄为 21 岁的男性 LiLei。好吧,可读性还不算太差。

但是,不难看出以上 filterByAgefilterByNameStart 等这一系列方法中,除了过滤条件不同,其他逻辑完全一样,造成了大量代码的重复;并且没有任何灵活性可言,调用方改需求,你就要加方法。

我们现在就使用匿名函数把不同的部分抽出去,让调用方想怎么过滤就过滤。filter 方法的主干逻辑只关心当前的元素是不是要放入结果集合中,其他的判断逻辑都交给匿名函数 fn 去做:

function filter(list, fn) {
  let filtered = [];
  for (let i = 0; i < list.length; i++) {
    if (fn(list[i], i) === true) {
      filtered.push(list[i]);
    }
  }
  return filtered;
}

上面的例子会变成这种写法:

filter(students, function(member) {
  return member.name === 'LiLei' && member.age === 21 && member.sex === 'male';
});

使用箭头方法可以更简洁:

filter(students, member => member.name === 'LiLei' && 
    member.age === 21 && 
    member.sex === 'male');

现在调用方再提一些变态的过滤方式,你可以回一个眼神,自己写去。

说一不二的方法调用

假设有个 Api 接口,将传入的数字翻倍并返回,这个接口支持单个和批量的方式。

传入 5

  • 执行成功返回 {status: 'success', data: 10}
  • 执行失败返回 {status: 'failed', error: 'xxx'}

传入 [2, 3]

  • 执行成功返回 {status: 'success', data: [{status: 'success', data: 4}, {status: 'success', data: 6}]}
  • 执行失败返回 {status: 'success', data: [{status: 'failed', error: 'xxx'}, {status: 'failed', error: 'xxx'}]}

也就是单个输入会按单个格式输出,批量输入会按批量格式输出。三下五除二,实现了下面这个版本:

function multiple(inNum) {
  if (Array.isArray(inNum)) {
    // 处理批量情况
    return {
      status: 'success',
      data: inNum.map(it => {
        if (isNaN(parseFloat(it))) {
          return {
            status: 'failed',
            error: 'The input is not a number'
          };
        } 

        return {
          status: 'success',
          data: it * 2
        }
      })
    };
  } else {
    // 处理单个情况
    if (isNaN(parseFloat(inNum))) {
      return {
        status: 'failed',
        error: 'The input is not a number'
      };
    }

    return {
      status: 'success',
      data: inNum * 2
    };
  }
}

这里面单个和批量两种方式除了输入和输出格式不同,其他逻辑完全一样。如果要将乘 2 改成乘 3,两个地方都要改。那看下怎么使用匿名函数来避免两处修改:

function execute(data, fn) {
  // 最小执行单元
  let single = it => {
    try {
      return {
        status: 'success',
        data: fn(it)
      };
    } catch (e) {
      return {
        status: 'failed',
        error: e.toString()
      }
    }
  };

  if (Array.isArray(data)) {
    return {
      status: 'success',
      data: data.map(single)
    }
  } else {
    return single(data);
  }
}

function multiple(inNum) {
  return execute(inNum, it => {
    if (isNaN(parseFloat(it))) {
      throw new Error('The input is not a number');
    }

    return it * 2;
  });
}

现在 execute 方法只管输入输出的格式和错误处理,包揽了所有脏活累活;multiple 方法则只关心业务的具体实现,也不用关心输入的是单个元素还是数组。如果要改乘 3,只要修改 multiple 方法最后一个 return。如此一来,execute 还可以被其他的 Api 方法复用,可谓一举两得。

本文的目的只是抛砖引玉,代码中可利用匿名函数重构的坏味还有很多,这种重构方式不只是适用于 Js 中。大家只要多思考、多动手,代码质量一定会day day up~~

关于重构我还想多说一点,重构的过程应该是渐进的方式,当你改第一次的时候可能觉得还ok,第二次就要想下是不是有更好的方式来实现。如果修改对调用方透明那是最好了,实在不行让调用方配合修改也是值得的,当然这其中还要权衡时间成本。Your boss is watching you 😡

Happy Code 😁


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK