【Kotlin】第9課-介面(Interface)

Kotlin 的介面與抽象類別相似,都可以宣告抽象的方法或屬性。但類別只可繼承一個父類別,然而卻能實作多個介面。這項特性為類別設計添加了新的概念,以及開發時利用多型的可能性。

本文會介紹設計介面的方式,並讓類別來實作(implement)。接著解說如何同時實作多個介面。最後示範 callback 方法的應用。

一、介面的設計

假設我們有以下的產品類別。

class Product(val id: String,
var name: String,
var price: Int,
var unit: String) {
}

筆者在第8課的範例,另外建立了 BookClothes 類別,並繼承 Product。這是因為書籍與衣物都屬於產品,具有共同的特徵。而介面則傾向這個類別的物件能用來做什麼。下面筆者設計一個介面,實作它的類別,代表其物件可以被輸出成檔案。

interface Exportable {
var exportLocation: String

fun getFileName(): String
fun getExportContent(): String
fun getExportPath() = "$exportLocation\\${getFileName()}"
}

這個介面有一個抽象屬性,代表目標輸出位置。有兩個抽象方法,用來取得檔案名稱與輸出內容。和一個非抽象方法,用來取得輸出路徑。


二、實作介面

接下來我們讓 Product 類別實作 Exportable 介面。做法跟繼承類別相同,實作後也需完成介面定義的抽象屬性與方法。

class Product(val id: String,
var name: String,
var price: Int,
var unit: String) : Exportable {

override var exportLocation = "\\product"
override fun getFileName() = "$id $name.txt"
override fun getExportContent() = "$id\n$name\n單價:$price/$unit"
}

此處筆者另外設計一個 Article 類別,也讓它實作 Exportable 介面。這兩個類別可以各自完成介面定義的抽象屬性與方法

class Article(val id: String,
var title: String,
var content: String,
var author: String) : Exportable {

override var exportLocation = "\\article"
override fun getFileName() = "$title-$author"
override fun getExportContent() = "$title\n$content"
}

完成後,我們於主程式宣告一個 exportFile 方法,它會接收一個可匯出檔案的物件參數,模擬輸出檔案的過程。最後在主程式中使用看看。

fun main(args: Array<String>) {
val product = Product("BK001", "數學複習講義", 320, "本")
val article = Article(
"AR002",
"Kotlin教學系列",
"這系列文章,希望能幫助學過Java的人上手Kotlin的語法...",
"Vincent")

exportFile(product)
println()
exportFile(article)
}

fun exportFile(item: Exportable) {
println("${item.getExportPath()} 已經儲存!")
println(item.getExportContent())
}

方法若使用介面當作參數的資料型態,則該參數不會限制只能傳入特定類別的物件。而是只要是有實作那個介面的各種類別,該方法都接受。


三、實作多個介面

實作介面與繼承類別最明顯的的差異,在於類別可以實作多個介面。此處設計了第二個介面,實作它的類別,代表其物件可以存進資料庫。接著再讓 Product 實作。

interface DBEntity {
fun isFieldValid(): Boolean
}

class Product(val id: String,
var name: String,
var price: Int,
var unit: String) : Exportable, DBEntity {

// 其餘省略

override fun isFieldValid() =
name.isNotEmpty() && price >= 0 && unit.isNotEmpty()
}

這個介面定義的抽象方法,是用來檢驗物件的屬性是否都合法。完成實作後,Product 宛如於身兼額外的類別,能夠傳給不同資料型態的方法參數。

介面中能定義非抽象方法,例如 ExportablegetExportPath 方法。但若有數個介面或父類別定義了相同的方法名稱,那麼類別在同時繼承或實作時便會發生衝突。以下是一個例子。

interface Exportable {
// 其餘省略

fun printStatus() {
println("輸出路徑:${getExportPath()}")
}
}

interface DBEntity {
  // 其餘省略

fun printStatus() {
println("屬性檢查通過:${isFieldValid()}")
}
}

此例發生衝突的方法是 ProductprintStatus,我們必須在類別中自行完成該方法。這樣主程式在呼叫時,才知道要執行什麼程式。在完成的過程中,如果有需要,也可以利用「super<>」語法來呼叫指定介面的方法。範例如下。

class Product(val id: String,
var name: String,
var price: Int,
var unit: String) : Exportable, DBEntity {
// 其餘省略

  override fun
printStatus() {
println("物件狀態")
super<Exportable>.printStatus()
super<DBEntity>.printStatus()
}
}

四、介面繼承介面

類別可以繼承,介面也行,甚至能繼承多個。以下我們將經常會一起實作的介面,統整成一個叫做 DBDocument 的介面。它除了繼承 DBEntityExportable 介面,再外加定義抽象屬性 id。實作它的類別,代表其物件除了能存進資料庫,也能被輸出成檔案。

interface DBDocument : DBEntity, Exportable {
val id: String

override fun printStatus() {
super<Exportable>.printStatus()
super<DBEntity>.printStatus()
}
}

由於 DBEntityExportable 介面都具有叫做 printStatus 的非抽象方法,因此會出現前一節提到的衝突。處理的方式有兩種,第一種是DBDocument 這個子介面將方法實作完成,第二種是將衝突的方法定義為抽象,留到類別實作時才去完成。


五、介面的物件

介面除了提供給類別實作,也能直接用來產生物件。

fun main(args: Array<String>) {
val course = object : Exportable {
val id = "IT001"
val name = "計算機概論"

override var exportLocation = "\\course"
override fun getFileName() = "$id$name"
override fun getExportContent() = "編號:$id\n課程名稱:$name\n學分數:3"
}

exportFile(course)
}

透過「object」關鍵字,並加上介面名稱,就能在不建立新類別的條件下,直接宣告一個有實作該介面的物件。當物件只會在特定的地方使用,這種做法是很方便的。


六、Callback 方法

介面物件的延伸應用是 callback 方法。由於有些操作是「非同步」的,比方說網路連線、事件處理,程式流程未必能等到相關操作執行完畢再往下走。所以需要先寫好屆時要執行的程式,並包裝起來。

本節提供相關的範例。筆者設計一個叫做 Exporter 的類別,用來模擬將物件輸出成檔案,且會隨機失敗。以及一個叫做 ExportFinishListener 的介面,它可以用來包裝一段程式

class Exporter {
fun export(item: Exportable, listener: ExportFinishListener) {
val isFailed = (Math.random().toInt() * 2) % 2 == 0
val msg =
if (isFailed)
"輸出失敗,請重新嘗試!"
else
"輸出成功!\n檔案路徑:${item.getExportPath()}" +
"\n檔案內容:\n${item.getExportContent()}"
listener.onFinish(msg)
}
}

回到主程式,首先準備好要輸出的物件。接著建立 ExportFinishListener 的介面物件,並完成其中的 onFinish 方法,此處為顯示訊息視窗。最後呼叫 Exporter 物件的方法,傳入物件與 listener 物件便可開始模擬。

fun main(args: Array<String>) {
val product = Product("BK001", "數學複習講義", 320, "本")

val listener = object : ExportFinishListener {
override fun onFinish(msg: String) {
JOptionPane.showMessageDialog(
null,
msg,
"匯出 ${product.name}",
JOptionPane.INFORMATION_MESSAGE)
}
}

Exporter().export(product, listener)
}

當 Exporter 的工作結束後,它會將訊息字串傳入 listeneronFinish 方法。接著程式流程便回到主程式的介面物件。在完成 onFinish 方法時,我們存取主程式的 product 變數,獲取資料顯示訊息視窗。

讀者可能會想說,怎麼不直接把訊息視窗的標題和訊息傳給 Exporterexport 方法,並在裡面呼叫 JOptionPane.showMessageDialog 方法呢?這樣就不必利用 callback 的做法了。

這麼做的好處是能夠自行決定匯出後要執行什麼動作。比方說想改成跳轉畫面、開啟檔案等行為,只要寫好 ExportFinishListener 物件的方法即可。反之,則每個呼叫 export 方法的地方,都只能執行固定的程式。

上一篇:【Kotlin】第8課-類別繼承與抽象類別

下一篇:【Kotlin】第10課-列舉(Enum)

留言