知识点:
① 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)
}