zoukankan      html  css  js  c++  java
  • [重构]一次用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
    发布于: 博客园

  • 相关阅读:
    九度OJ 1333:考研海报 (区间操作)
    九度OJ 1326:Waiting in Line(排队) (模拟)
    九度OJ 1325:Battle Over Cities(城市间的战争) (并查集)
    九度OJ 1324:The Best Rank(最优排名) (排序)
    九度OJ 1323:World Cup Betting(世界杯) (基础题)
    九度OJ 1283:第一个只出现一次的字符 (计数)
    九度OJ 1262:Sequence Construction puzzles(I)_构造全递增序列 (DP)
    九度OJ 1261:寻找峰值点 (基础题)
    SDWebImage
    MBProgressHUDDemo
  • 原文地址:https://www.cnblogs.com/bee0060/p/15704623.html
Copyright © 2011-2022 走看看