Scala: 一次命令式到函数式的重构

知识点:
① List的map和exists方法的运用
② Stream的应用

背景:
需要做这么一个功能,检查一个excel文档,它需要满足一下两点要求:
① 文件列的个数和列名与umtsPara配置的列个数和列名完全一致
② umtsPara中设定列不能为空,则文件中对应的列也不能为空
代码评审过程中,发现代码“太命令式”,没有把scala强大的“函数式”发挥出来,决定做一次从“命令式”到“函数式”的重构。

原代码问题分析:
原来的代码采用的是典型的命令式编程的方法,具有非常明显的“命令式”风格
① 复杂的控制结构。包含多层for、if嵌套,还有break等跳转语句。这一切使得代码复杂度过高。
② 具有状态“变量”。 比如用作返回值的标志位ret,不断被修改。
③ 控制结构在不断重复。具体地,每个for循环其实都是在表达一个逻辑概念,即“是否存在某种情况”,这个逻辑概念没有抽象并表达出来。

附原代码:

object ProjectFileCheck {

  def check(src:String):Boolean = {
    var ret:Boolean = true

    try
    {
      var is = new FileInputStream(src)
      val wb = Workbook.getWorkbook(is)
      val sheetNum = wb.getNumberOfSheets
      ret = checkSheet(wb)

      if(ret){
        for(idx <- 0 to sheetNum -1){
          var sht = wb.getSheet(idx)
          var colInSheet = sht.getColumns
          println("col " + colInSheet + "  " + umtsPara.length + " " + idx)
          if(colInSheet != 0){
            if((colInSheet != umtsPara.length) ||  !checkTitle(sht)|| !checkContent(sht) ){
              ret = false
              break
            }
          }
        }
      }
      wb.close()
    }
    catch {
      case e:ControlThrowable =>{
        println("break throw exception")
        ret = false
      }
      case e: Exception => e.printStackTrace()
    }
    ret
  }

  private def checkSheet(wb:Workbook):Boolean={
    var ret = false
    val sheetNum = wb.getNumberOfSheets

    for(idx <- 0 to sheetNum -1){
      var shtTraversal = wb.getSheet(idx)
      val colInSheet = shtTraversal.getColumns
      if(colInSheet != 0){
        ret = true
      }
    }
    ret
  }

  private def checkTitle(sht: Sheet):Boolean = {
    val colInSheet = sht.getColumns
    var ret = true

    for(col <- 0 to colInSheet -1){
      var cel =  sht.getCell(col, 0)
      var content = cel.getContents
      //      println(content)
      if(content != null){
        if(!umtsPara(col).colName.equals(content)){

          ret = false
          break
        }
      }
      else{
        println("cell content is null")
        ret = false
        break
      }
    }
    //   println(ret)
    ret
  }

  private def checkContent(sht:Sheet):Boolean ={
    val rowInSheet = sht.getRows
    val colInSheet = sht.getColumns
    var ret = true
    for(row <- 1 to rowInSheet -1) {
      for(col <- 0 to colInSheet -1){
        //        println("row" + row + "  col" + col)
        var cel =  sht.getCell(col, row)
        var content = cel.getContents

        if(!umtsPara(col).isContentNull){
          if(content.isEmpty != umtsPara(col).isContentNull){
            //            println(content + ut(col).colName +" " + ut(col).colName.equals(content))
            ret = false
            break
          }
        }
      }
    }
    ret
  }
}




用函数式的思想重构:(以checkTitle这个函数为例)

1. 一切都是列表
    原来的: for(col <- 0 to colInSheet -1) 
    用列表的观点来看,应该是(0 until sheet.getColumns).toList

2. 对列表中每个元素都要做一遍的操作,就是在对列表做map
    原来的:
    for(col <- 0 to colInSheet -1) { var cel =  sht.getCell(col, 0) ...}
    用列表的观点来看,应该是把(0 until sheet.getColumns).toList这个列表中的每个元素转换Celll类型,整个列表就变成一个List[Cell]的列表,即:
    (0 until sheet.getColumns).toList.map(sheet.getCell(_, 0))

3. 抽象出重复的控制结构
    原来的: 每个for循环都重复着一种逻辑,即在循环中通过if语句做一个判断(这里我们称它为谓词P),若这个判断为真,就跳出这个循环。
    用列表的观点来看,这个被不断重复的控制结构,实际上是在表达这么一种逻辑概念,即列表里是否存在一个元素满足让“谓词P”为真。这实际就是exist方法表达的概念,这样原来的控制结构可以表达为:
  private def hasBadTitle(sheet: Sheet): Boolean = {
    (0 until sheet.getColumns).toList.map(sheet.getCell(_, 0)).exists(badTitle)
  }

  private def badTitle(cell: Cell): Boolean = {
    !cell.getContents.equals(umtsPara(cell.getColumn).colName)
  }

重构后的代码如下, 没有一个for循环,没有if/else,没有状态变量,每个函数极致精简。至此深深的体会了一把函数式的魅力。

object ProjectFileChecker {

  def check(src: String): Boolean = {

    var wb: Workbook = null
    var is: FileInputStream = null
    try {
      is = new FileInputStream(src)
      wb = Workbook.getWorkbook(is)
      checkSheet(wb.getSheet(0))
    }
    catch {
      case e: Exception => e.printStackTrace(); false
    }
    finally {
      if (is != null) is.close()
      if (wb != null) wb.close()
    }
  }

  private def checkSheet(sheet: Sheet): Boolean = {
    (sheet.getColumns == umtsPara.length) && !hasBadTitle(sheet) && !hasBadContent(sheet)
  }

  private def hasBadTitle(sheet: Sheet): Boolean = {
    (0 until sheet.getColumns).toList.map(sheet.getCell(_, 0)).exists(badTitle)
  }

  private def badTitle(cell: Cell): Boolean = {
    !cell.getContents.equals(umtsPara(cell.getColumn).colName)
  }

  private def hasBadContent(sheet: Sheet): Boolean = {
    (0 until sheet.getColumns).toList.exists(badColumn(sheet, _))
  }

  private def badColumn(sheet: Sheet, col: Int): Boolean = {
    Stream.range(1, sheet.getRows).map(sheet.getCell(col, _)).exists(badCell)
  }

  private def badCell(cell: Cell): Boolean = {
    cell.getContents.isEmpty && !umtsPara(cell.getColumn).isContentNull
  }
}




还漏了重要的一点——Stream:
Stream,它和列表相似,只不过它会延迟计算下一个元素,仅当需要的时候才会去计算。这个延迟计算的特性,有什么用呢?

比如取1到100中前5个能被3整除的数,你可以这么写
    (1 to 100).toList.filter(_ % 3 == 0).take(5)
这样会构造一个有100个元素的列表,然后通过filter方法从中筛选出所有模3为0的数形成一个新的列表,再取出前5个。这是一种优雅的解决方案。

现在这样没什么问题,但想象一下,如果现在题目的要求变为取1到100000000中前5个能被3整除的数,如果仍写为
    (1 to 100000000).toList.filter(_ % 3 == 0).take(5)
就会有很大的问题了——开销太大。仅仅为了取前5个数,要构造一个100000000这么大的列表,从中filter出能被3整除的又构成一个巨大列表,这在内存和性能上的消耗都是无法承受的。

难道我们必须要放弃这种优雅的解决方案,转而投向命令式的for循环?有没有鱼和熊掌可以兼得方法——即能优雅的解决问题,又不导致性能问题?——答案必须是:有!那就是Stream。之所以(1 to 100000000)的时候性能差,是因为我们做了很多无用功——本来我们只要计算这个列表的前15个数就能完成计算的。如果能延迟计算,仅当我们需要的时候才去构造列表中的下一个元素,这样只生成列表中的前15个数就能完成计算了。这就是Stream能达到的效果!在scala REPL里对比运行一下下面两句,看看性能上的差别:
    (1 to 100000000).toList.filter(_ % 3 == 0).take(5) 
    Stream.range(1, 100000000).filter(_ % 3 == 0).take(5).toList

讲了这么多,对我们这次重构有什么用呢?——我们在检查Excel文件的每一列是否有null值时,面临上面讲的同样的问题:因为一列可能有很多行数据,如果对每行都取值构造出一个List[Cell]的列表,然后再判断是否存在badCell,就有可能做很多无用功,并且带来性能的下降。所以我们用Stream来重构它,性能与优雅兼得,如下:
  private def badColumn(sheet: Sheet, col: Int): Boolean = {
    Stream.range(1, sheet.getRows).map(sheet.getCell(col, _)).exists(badCell)
  }

 

转载于:https://my.oschina.net/guanxun/blog/305531

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值