在工作中,我们使用Scala作为在Spark应用的开发语言,由于公司的代码托管平台暂不支持Scala代码的规范检测,团队内部拟定了一个的代码规范,希望能在统一代码风格的同时,足够简单且可操作。

基本原则

通用规范

源文件规范

文件名

源文件名必须和它包含的顶层类名保持一致,包括大小写,并以.scala作为后缀名。

文件编码

所有源文件编码必须是 UTF-8

特殊字符

空格

除了换行符之外,ASCII空格(0x20)是唯一合法的空格字符。这意味着

特殊转义字符

对于有特殊转义表示的字符 \b, \t, \n, \f, \r, \", \', \\,禁止使用其它等价转义方式。例如\012或者\u00a表示。

非ASCII字符

对于非ASCII字符,可以使用实际字符(如 )或者它的Unicode转义(如\u221e),取决于哪种写法的可读性更好。
使用注释有助于增强可读性
示例:

示例 说明
val unitAbbrev = “\u03bcs”; // “μs” 虽然合法但是很没有必要的写法
val unitAbbrev = “\u03bcs”; // Greek letter mu, “s” 虽然合法但是很难理解的写法
val unitAbbrev = “\u03bcs”; 让人完全读不懂的写法
return ‘\ufeff’ + content; // byte order mark 很好的写法,用Unicode转义来表示非打印字符,并且有合适的注释帮助阅读者理解
val unitAbbrev = “μs”; 最佳写法,无需注释就可以理解

组织结构

源文件必须按顺序由以下部分组成:

代码书写规范

列宽

每行不超过150个字符

缩进

4个空格,严禁使用Tab

括号的使用

基本原则:K&R风格。

new MyClass() { // 左花括号前不能换行,在其后换行
    @Override
    def method():Unit= {
        if (condition()) {
            try {
                do {
                    something()
                } while (!stop()) // do-while中间的右花括号后
            } catch  { // try-catch中间的右花括号后无需换行
                case e:Exception =>recover()
            } // try-catch结束,右花括号后需要换行
        } else { // if-else中间的右花括号后无需换行
            doSomethingElese()
        } // if-else结束,右花括号后需要换行
    }

其他:scala中的简单表达式可以省略括号

// 推荐
def square(x: Int) = x * x
val y = if (x < 0) -x else x 
if(cond1){
  // one line statement
}else{
  // one line statement
}

// 避免
if (x < 0) 
	-x
else
	x

空行的使用

在以下情况下增加空行:

注释风格

使用java风格的注释,不用使用scala风格

/** 单行注释 */
// 单行注释

/**
 * java风格的多行注释
 * 推荐使用
 */

/** scala风格的注释
  * 不推荐使用
  */

命名规范

基本原则:驼峰命名,命名有业务含义。这里列举了Spark编程中常见是变量/数据结构,在工程中尽量统一使用下列的命名规则。

Scala相关规范

变量

// 推荐
val name = "abc"
// 不推荐
var name = "abc";

方法

定义

方法调用避免中缀表示法

// 推荐
list.map(func)
string.contains("foo")
// 不推荐
list map (func)
string contains "foo"

Return的使用

原则:在方法中避免使用Return语句,而是直接使用值返回,否则容易出现错误

// 使用
def receive(rpc: WebSocketRPC): Option[Response] = {
  tableFut.onComplete { table =>
    if (table.isFailure) {
       None // Do not do that!
    } else { ... }
  }
}
// 避免,下面会导致onComplete返回的是NonLocalReturnControl
// return的意思是:退出包含 return 的最近的一个方法(不是函数),这里是receive
def receive(rpc: WebSocketRPC): Option[Response] = {
  tableFut.onComplete { table =>
    if (table.isFailure) {
      return None // Do not do that!
    } else { ... }
  }
}

下面几种情况可以方法Method可有使用return

其他

使用@Override

Scala中,虽然有的时候override是可选的,但是坚持使用override是安全的。

应尽量避免让trait去extend一个class。

class StarfleetComponent 
// 不推荐
trait StarfleetWarpCore extends StarfleetComponent 
// 推荐
class Starship extends StarfleetComponent with StarfleetWarpCore 
class RomulanStuff // won't compile 
class Warbird extends RomulanStuff with StarfleetWarpCore

链式数据结构的选择

选择使用Seq时

一个原则是:当你对选择集合类型犹疑不定时,就应选择使用Vector。
需要注意的是:当我们创建了一个IndexSeq时,Scala实际上会创建Vector对象:

scala> val x = IndexedSeq(1,2,3) 
x: IndexedSeq[Int]  = Vector(1, 2, 3)

Vector很好地平衡了快速的随机选择和快速的随机更新(函数式)操作。Vector是Scala集合库中最灵活的高效集合。

当面对一个大的集合,且新元素总是要添加到集合末尾时,就可以选择ArrayBuffer。如果使用的可变集合特性更近似于List这样的线性集合,则考虑使用ListBuffer。

如果需要将大量数据添加到集合中,建议选择使用List的prepend操作,将这些数据添加到List头部,最后做一次reverse操作。

Lazy的使用

当一个类的某个字段在获取值时需要耗费资源,并且,该字段的值并非一开始就需要使用,则应将该字段声明为lazy。

异步

Future的使用

在使用Future进行并发处理时,应使用回调的方式,而非阻塞。

// 避免
val f = Future {
 //executing long time
}
val result = Await.result(f, 5 second)

//推荐
val f = Future {
 //executing long time
}
f.onComplete {
 case Success(result) => //handle result
 case Failure(e) => e.printStackTrace
}

使用par对集合的操作并行化

若有多个操作需要并行进行同步操作,可以选择使用par集合

// 同时访问
val urls = List("http://scala-lang.org",
  "http://agiledon.github.com")
def fromURL(url:  String)  = scala.io.Source.fromURL(url)
  .getLines().mkString("\n")
val t = System.currentTimeMillis()
urls.par.map(fromURL(_))
println("time: " + (System.currentTimeMillis - t) + "ms")
线程联合操作

若有多个操作需要并行进行异步操作,并在全部完成后进行某些操作,则采用for comprehension对future进行join方式的执行。

val result1 = Cloud.runAlgorithm(10)
val result2 = Cloud.runAlgorithm(20)
val result3 = Cloud.runAlgorithm(30)
val result = for {
  r1 <- result1 r2 <- result2 r3 <- result3 } yield (r1 + r2 + r3) result onSuccess { case result =>  println(s"total = $result")
}

Spark相关规范

由于Spark API的特点,我们特殊规定了几种规则,适用于Spark应用的开发。

链式调用规范

如果链式调用过长,尽量一个方法调用占用一行,增加可读性。注意:spark-shell下直接粘贴代码会出错,输入:p 或者:paste进入粘贴模式ctrl-D退出并开始执行

val extractDF = spark.read.parquet("someS3Path")
  .select(
    "name",
    "Date of Birth"
  )
  .transform(someCustomTransformation)
  .withColumnRenamed("Date of Birth", "date_of_birth")
  .filter(
    col("date_of_birth") > "1999-01-02"
  )

Spark SQL书写规范

Spark SQL中属性的SQL语句往往很长,且可读性很差,我们规范: 使用"""表示多行字符串来书写语句。

  val firstDF = sqlContext.sql(
    """ select
          ip,
          sum(ip_line_cnt) as ip_line_cnt,
          sum(ip_click_cnt) as ip_click_cnt,
          sum(ip_zhudong_cnt) as ip_zhudong_cnt,
          sum(ip_line_cookieage_less1d) as ip_line_cookieage_less1d,
          sum(ip_ua_abnormal_cnt) as ip_ua_abnormal_cnt
        from table_day_temp_ip
        group by ip
    """)

工具篇

scala-style工具

为了使得团队代码风格一致,我们可以使用一些静态代码检测工具。该工具对代码Scala代码进行基本的格式检查,并输出检测结果。可以配置为maven插件,作为例行的代码检测。在官方的默认配置中有一些配置可能并不使用,例如println配置。

推荐:Spark官方的配置文件

read more

spark 官方scala编程风格指南
spark-style-guide
内网Scala编程规范