by 壯衣
在處理數據庫連接或者輸入輸出流等場景時,我們經常需要寫一些非常繁瑣又枯燥乏味的代碼來關閉數據庫連接或輸入輸出流。
例如數據庫操作:
def update(sql: String)(conn: Connection): Int = {
var statement: Statement = null
try {
statement = conn.createStatement()
statement.executeUpdate(sql)
} finally {
if (conn != null) conn.close()
if (statement != null) statement.close()
}
}
例如文本操作:
def save(sql: String, path: String): Unit = {
var pw: PrintWriter = null
try {
pw = new PrintWriter(path)
pw.println(sql)
} finally {
if (pw != null) pw.close()
}
}
從上面的兩個例子中(這樣的例子還有很多)可以發現兩者存在相同模式的代碼:
var a: A = null
try {
a = xxx
//對a進行操作
} finally {
if(a != null) a.close()
}
那能不能把這些相同模式的代碼抽象出來呢?答案是肯定的,我們可以利用Scala的泛型和高階函數來完成。先寫一個高階函數:
def using[A <: { def close(): Unit }, B](a: A)(f: A => B): B = {
try f(a)
finally {
if(a != null) a.close()
}
}
using函數有兩個參數a: A、f: A => B,其中a是需要關閉的資源,f是一個輸入為A輸出為B的函數。現在我們可以利用using函數來重寫數據庫操作和文本操作了。
數據庫操作:
def update0(sql: String)(conn: Connection): Int = {
using(conn) {
conn => {
using(conn.createStatement()) {
statement => statement.executeUpdate(sql)
}
}
}
}
文本操作:
def save0(sql: String, path: String): Unit = {
using(new PrintWriter(path)) {
pw => pw.println(sql)
}
}
可以看出重寫后的函數相比之前更加精煉,函數只需要關心實現自己的邏輯而不用關心資源關閉操作,這些就交給using函數來處理吧。目前看來using函數似乎可以滿足我們的需求了,真的是這樣嗎?我們再看一個例子:
def query[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
using(conn) {
conn => {
using(conn.createStatement()) {
statement => {
using(statement.executeQuery(sql)){
resultSet => f(resultSet)
}
}
}
}
}
}
可以看到上面的例子用到了3次using,嵌套了3層函數,代碼的可讀性變差。而且一旦需要關閉的資源變多,嵌套函數的層數也將相應增加。代碼又陷入另一個繁瑣枯燥的模式。有什么更好的辦法嗎?也許可以試一下for表達式這個語法糖,那么代碼將如下表示:
def query0[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
for {
conn <- Closable(conn)
stmt <- Closable(conn.createStatement())
rs <- Closable(stmt.executeQuery(sql))
} yield f(rs)
}
這樣沒有了復雜的嵌套函數,代碼的可讀性更好了。可是Closable是什么類型?別急,我們可以從上面的代碼分析出Closable類型是什么結構,首先Closable類型有一個可關閉資源的屬性;然后Closable類型可以使用for表達式語法糖,那么Closable類型需要實現map和flatMap函數。Closable類型實現如下:
case class Closable[A <: { def close(): Unit }](a: A) {
def map[B](f: A => B): B =
try f(a)
finally {
if(a != null) a.close()
}
def flatMap[B](f: A => B): B = map(f)
}
到此代碼已經滿足我們的需求了,但是還是很想探究其中的魔法。我們知道for表達式其實就是flatMap加map的語法糖,那么讓我們剝開糖衣看看這塊糖真實的模樣:
def query1[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
Closable(conn).flatMap {
conn => Closable(conn.createStatement()).flatMap {
stmt => Closable(stmt.executeQuery(sql)).map {
rs => f(rs)
}
}
}
}
讓我們接著剝開flatMap
def query2[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
Closable(conn).map {
conn => Closable(conn.createStatement()).map {
stmt => Closable(stmt.executeQuery(sql)).map {
rs => f(rs)
}
}
}
}
最后剝開map
def query3[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
Closable(conn).map {
conn => try {
Closable(conn.createStatement()).map {
stmt => try {
Closable(stmt.executeQuery(sql)).map {
rs => try {
f(rs)
} finally {
if(rs != null) rs.close()
}
}
} finally {
if (stmt != null) stmt.close()
}
}
} finally {
if (conn != null) conn.close()
}
}
}
到此Closable類型的神秘面紗已經完全揭開,希望Closable類型可以在各位讀者工作中在處理一些需要關閉資源的時候提供一種選擇,最后再多說兩句。有時我們只需要處理兩個可關閉資源,而且這兩個資源之間沒有關聯。例如文本操作有一個輸入流一個輸出流,那么我們使用Closable類型代碼將會如下:
def save1(inPath: String, outPath: String): Unit = {
for {
in <- Closable(new BufferedReader(new FileReader(inPath)))
out <- Closable(new PrintWriter(outPath))
} yield out.println(in.readLine())
}
不是說上面的代碼不好,而是我們可以做到更加簡練,代碼如下:
def save2(inPath: String, outPath: String): Unit = {
Closable(new BufferedReader(new FileReader(inPath)))
.map2(new PrintWriter(outPath)){ (in, out) => out.println(in.readLine()) }
}
讓我們來看下 map2的實現:
def map2[B <: { def close(): Unit }, C](b: B)(f: (A, B) => C): C =
for {
ac <- Closable(a)
bc <- Closable(b)
} yield f(ac, bc)
聰明的讀者可能還會有疑惑,map2用于關閉兩個資源,那關閉3個資源我們需要實現一個map3,關閉N個資源豈不是要實現一個mapN。當然我們可以使用for表達式實現,但是還有更好的實現嗎?哈哈,這個就交給讀者課后思考了,相信聰明的你一定有自己的想法。