[重构]一次用ramda重构的记录

前言

最近写的几个方法,事后看起来觉得有点重复了。想要重构试试,正好想起ramda,那就试试用ramda来重构,看下能否减少重复吧。

注: 这次重构主要为业余消遣,本文用到的代码也不是原文, 只是仿照源代码的特点写的示例。如何编写代码跟项目所处环境有关,所以本次重构方法仅为业余娱乐和学习,不对结果和方法作推荐。

重构前代码

故事是这样的,假设我有一个学生类:

function Student(name, score, age) {
  this.name = name;
  this.score = score;
  this.age = age;
}

然后有一个学生数组,并需要得到这些学生中的最低分。 那么学生数组和求最低分的方法如下:

var students = [
  new Student('Peter', 90, 18),
  new Student('Linda', 92, 17),
  new Student('Joe', 87, 19),
  new Student('Sue', 91.5, 20),
]

function getMinScore(students) {
  if (
    !Array.isArray(students) ||
    !students.every(student => student instanceof Student)
  ) {
    return undefined;
  }
  return students.reduce(
    (acc, student) => Math.min(acc, student.score),
    Number.MAX_SAFE_INTEGER
  );
}

嗯, 这代码看着还行,至少自我感觉良好。 其中对类型判断的部分,用typescript的话可以免去,咱这里没用,就简单直接点。

好,故事当然不会到这里就结束,否则就没啥重构的了。
接下来,发现我还需要获得学生中的最大年龄是多少。 好吧,很简单啊,复制粘贴上面的再稍微改一下不就有了吗? 如下:

function getMaxAge(students) {
  if (
    !Array.isArray(students) ||
    !students.every(student => student instanceof Student)
  ) {
    return undefined;
  }

  return students.reduce(
    (acc, student) => Math.max(acc, student.age),
    Number.MAX_SAFE_INTEGER
  );
}

好了,代码写完,工作正常。收工。。。了吗?
回头看了看代码,这两个方法中,其实只有Math.min\Math.maxstudent.score\student.age不同,其他都一样。这很不DRY啊。
看着不是很爽。 这时我想到了ramda

能否用ramda,用组装的方式来完成这两个函数,以此减少重复呢?

好,说干就干。(下列重构基于个人经验和认知,不对的地方欢迎指出共同讨论

重构ING

先来重温下重构前的代码:

点击查看代码

function getMinScore(students) {
  if (
    !Array.isArray(students) ||
    !students.every(student => student instanceof Student)
  ) {
    return undefined;
  }

  return students.reduce(
    (acc, student) => Math.min(acc, student.score),
    Number.MAX_SAFE_INTEGER
  );
}

function getMaxAge(students) {
  if (
    !Array.isArray(students) ||
    !students.every(student => student instanceof Student)
  ) {
    return undefined;
  }

  return students.reduce(
    (acc, student) => Math.max(acc, student.age),
    Number.MAX_SAFE_INTEGER
  );
}

这两个方法做的事情基本可以拆分成以下步骤:

  1. 类型判断,确定传入参数是Student类实例的数组
  2. 若类型错误,返回undefined
  3. 若类型正确,遍历数组,并返回最小/最大的score/age属性值

好,先上个ramda的文档地址: https://ramdajs.com/docs/#

类型判断可以用以下方法实现:

const isInstanceof = (type) => (instance) => instance instanceof type;
const isStudent = isInstanceof(Student)
const allAreStudents = R.all(isStudent)

allAreStudents(students) => true
allAreStudents([1, 2, 3]) => false

这里的类型判断,其实就是一个if else, 可以直接使用ramdaifElse方法,下面再写上类型错误时的处理,加起来就是:

const whileNotStudent = () => undefined
const processIfStudent = R.ifElse(allAreStudents, R.__, whileNotStudent)

熟悉ramda的同学肯定知道了,R.__是一个参数占位符,而ramda的方法几乎都经过柯里化处理,当它的函数缺少足够的参数时,执行后依然会返回一个函数,当获得全部所需参数时,才会真的执行被柯里化的方法。
所以上面这么调用,会让processIfStudent成为一个需要一个参数的方法,这个参数会被传递给ifElse调用。

即下面两种写法等价:

假设我们有一个叫doSomething的函数

// 1
processIfStudent(doSomething) 
// 2
R.ifElse(allAreStudents, doSomething, whileNotStudent)

这时我们需要的第一层逻辑就满足了:

传入参数是学生数组时做XX, 不是时返回undefined
processIfStudent需要的参数,就是这个做XX的方法了。

接下来,我们先抽象一下我们需要做的事情。
无论是获取最大还是最小, score还是age,我们要做的事情都可以抽象成:

1. 遍历数组
2. 用一个逻辑记录下每次遍历的结果,直至遍历完成
3. 返回这个结果

这个逻辑就是获取最大最小某属性了。
然后我们让这个最大最小某属性都可以被定制。

看了下ramda的文档,我觉得我可以使用以下这些方法:

prop => 用于获取属性
minBy/maxBy => 通过两个对象的某属性进行比较并返回较小或较大的对象
reduce => 用于遍历数组
compose => 用于组合方法,具体的后面一点再说

好了,方法选好了,就可以开始实施了。

先根据我的需求,写了以下几个方法:

// 获取分数属性
const getScore = R.prop('score')
// 获取年龄属性
const getAge = R.prop('age')
// 该函数可以返回2个传入参数中分数较小的那个
const minByScore = R.minBy(getScore)
// 该函数可以返回2个传入参数中年龄较大的那个
const maxByAge = R.maxBy(getAge)
// 该函数用reduce进行遍历,可自定义遍历所需的逻辑,和被遍历的数组
const reduceByHandler = handler => instances => R.reduce(
  handler,  
  instances[0],
  instances)
// 可以获取传入的数组中,分数最小的数组对象
const reduceMinScoreStudent = reduceByHandler(minByScore)
// 可以获取传入的数组中,年龄最大的数组对象
const reduceMaxAgeStudent = reduceByHandler(maxByAge)

到这里其实已经差不多了,使用上面的两个reduce方法,可以得到分数最小和年龄最大的学生,但是我们现在需要的不是学生,而是那个数字,所以还需要用compose和prop来获取得到的实例的具体属性,即:

const reduceMinScore = R.compose(getScore, reduceMinScoreStudent)
const reduceMaxAge = R.compose(getAge, reduceMaxAgeStudent)

再套用回前面的processIfStudent方法,就能得到我们最终需要的方法了:

const getMinScore = processIfStudent(reduceMinScore)
const getMaxAge = processIfStudent(reduceMaxAge)

好了, 到这里, 我们终于得到我们需要的getMinScore和getMaxAge方法了。
直接用students调用一下,就能得到我们要的结果了:

const minScore = getMinScore(students)
console.log("minScore", minScore) // 87
const maxAge = getMaxAge(students)
console.log("maxAge", maxAge)   // 20

以下是完整代码:

点击查看代码
function Student(name, score, age) {
  this.name = name;
  this.score = score;
  this.age = age;
}

var students = [
  new Student('Peter', 90, 18),
  new Student('Linda', 92, 17),
  new Student('Joe', 87, 19),
  new Student('Sue', 91.5, 20),
]

const isInstanceof = (type) => (instance) => instance instanceof type;
const isStudent = isInstanceof(Student)
const allAreStudents = R.all(isStudent)
const whileNotStudent = () => undefined
const processIfStudent = R.ifElse(allAreStudents, R.__, whileNotStudent)

const getScore = R.prop('score')
const getAge = R.prop('age')
const minByScore = R.minBy(getScore)
const maxByAge = R.maxBy(getAge)

const reduceByHandler = handler => instances => R.reduce(
  handler, 
  instances[0],
  instances)

const reduceMinScoreStudent = reduceByHandler(minByScore)
const reduceMaxAgeStudent = reduceByHandler(maxByAge)

const reduceMinScore = R.compose(getScore, reduceMinScoreStudent)`
const reduceMaxAge = R.compose(getAge, reduceMaxAgeStudent)

const getMinScore = processIfStudent(reduceMinScore)
const getMaxAge = processIfStudent(reduceMaxAge)

const minScore = getMinScore(students)
console.log("minScore", minScore)
const maxAge = getMaxAge(students)
console.log("maxAge", maxAge)

重构到此结束了。 回头看看,好像还是有很多类似的代码,例如score和age的方法都是成对出现,如reduceMinScorereduceMaxAge
是的,这些都很相似。但因为本身他们包含的逻辑很少,只是调用了相同的方法,但参数都不同,重复已经比之前少多了。 而且细粒度的拆分后,可读性会好一点,也更方便调试(函数式编程有时真的挺难调试的)。

最后

重构结束了。 再次声明,这里的重构方法主要为娱乐和学习,大家是否要在项目中使用,还要根据自身项目情况而定。 上面的重构涉及很多小的方法和类似的方法,乍看起来好像代码行数没有少多少,但是,因为越靠上的方法,越独立且业务无关,也就越是容易被复用。

当类似需求出现得越多, 小而精得方法被复用次数越多,代码量的差距也会越大。我们每次需要重新写的逻辑也就越少。

另外,如果上面哪里有更好的重构方法,也欢迎提出共同探讨学习。

谢谢观看。


原创不易,转载请注明出处: https://www.cnblogs.com/bee0060/p/15704623.html
作者: bee0060
发布于: 博客园

posted on 2021-12-19 16:00  bee0060  阅读(221)  评论(0编辑  收藏  举报

导航