
Scala Tutorial(7)加強版的 Interface -- Trait
Scala Tutorial(6)Scala 物件導向基礎之二 << 前情 Java 7 中的介面話說由於多重繼承可能產生許多混餚(一個典型的問題是「鑽石問題」),所以在 Java 中並沒有多重繼承這樣的功能,一個類別只能繼承自一個父類別。但相對的,在 Java 中提供了 interface 的功能,顧名思義,interface 只定義了「介面」,而實作則需要由使用該 interface 的類別來實作。 但這產生了一個問題--當 A 和 B 兩個不同的類別使用同一個 interface 時,就算這個 interface 裡的某個函數,在 A 和 B 這兩個類別裡的行為都相同時,我們仍然需要在 A 和 B 兩個類別中分別實作這個函式。[註一] 舉例而言,假設今天我們開發了一個玩具鯊魚,他會依照真正的鯊魚游動的方式來游,但他因為不是魚,所以他不應該繼承 Fish 這個類別。但另一方面,我們也知道不只有魚類會游泳,很多哺乳類也會游泳,所以「游泳」這件事不應該被歸類在「魚」的行為,而是一種通用的介面。 所以在 Java 中要表現這樣的關係,我們可能會這麼寫: interface Swimming { public void swim(); } class Fish {} // 我是魚 class Mammal {} // 我是哺乳類 class Dog extends Mammal implements Swimming { public void swim() { System.out.println("I'm swimming by using my four legs"); } } class Shark extends Fish implements Swimming { public void swim() { System.out.println("I'm swimming like a shark"); } } 這看起來沒什麼問題,因為雖然鯊魚和狗都會游泳,但兩者使用的方法並不相同,所以我們使用不同的實作是合理的。 但如果加入了玩具鯊魚呢?由於他不是魚,所以他不應該繼承自 Fish 類別,但他游泳的方式又和 Shark 相同,所以我們可能會這樣寫: class Toy {} // 我是玩具 class ToyShark extends Toy implements Swimming { public void swim() { System.out.println("I'm swimming like a shark"); } } 但你有沒有發現,ToyShark 和 Shark 的 [註一] Java 8 中的 Interface 提供了 Default Method 功能,同樣也可以解決這個問題喲! 最簡單的型式的 Trait在 Scala 中,最簡單的 Trait 型式和 Java 中的 interface 相同,我們僅定義其名稱與介面,在這個情況下,trait 關鍵字的語義和 Java 的 interface 是相同,實際上也是將其編譯成 Java 中的 interface。 trait Swimming { def swim(): Unit } trait Playing { def play(): Unit } 不過要注意的是,在使用方式上和 Java 不太一樣,在 Scala 中不管某個類別是繼承自父類別,或是引入 Trait,第一個關鍵字都是 但另一方面,由於 Scala 和 Java 一樣只有單一繼承,一個類別只能有一個父類別,所以類別最多只會出現一個 class Fish class Mammal // A 類別繼承 Fish 類別 class A extends Fish { def swim() = println("Swimming") } // B 類別引用了 Swimming class B extends Swimming { def swim() = println("Swimming") } // C 類別繼承了 Fish 類別並且引入了 Swimming 特徵 class C extends Fish with Swimming { def swim() = println("Swimming") } // D 類別繼承了 Fish 類別,並且引用了 Swimming 和 Playing 特徵 class D extends Fish with Swimming with Playing { def swim() = println("Swimming") def play() = println("Playing") } // 編譯錯誤,和 Java 相同,如果沒實作 Trait 中的函式,那麼這個 // 類別要被宣告成 abstract,並且也不能用 new 關鍵字產生實例 /* class E extends Swimming { } */ 在這邊要特別注意的是 B 類別,雖然語法上看起來像是 B 類別「繼承」了 具有實作的 Trait在 Scala 中的 Trait 裡的函式是可以有實作的,若 Trait 裡的函式有實作,那麼類別在引入 Trait 之後若沒有將該函式覆寫掉,那麼呼叫該函式的時候就會使用 Trait 中的實作,例如上述的玩具鯊魚的例子我們可以寫成像下面這樣: trait Swimming { def swim() = println("I'm swimming like a Shark") } trait Charging { def charge() = println("I'm charging my battery...") } class Fish class Toy class Shark extends Fish with Swimming class ToyShark extends Toy with Swimming with Charging class Dog extends Swimming { override def swim() = println("I'm swimming using my four legs!") } val shark = new Shark val toy = new ToyShark val dog = new Dog shark.swim() // 真的鯊魚可以遊泳 //shark.charge() // 但不能充電 toy.swim() // 玩具鯊魚可以充電 dog.swim() // 狗也會泳遊,但游的方法和鯊魚不太一枇 透過這樣子提供了具有實作的 Trait,我們可以讓程式碼的重用性提高。 限制 Trait 可以被誰使用除了做為「有實作的預設介面」外,Trait 可以宣告自己只能被用在哪些情況,或者限制這個 Trait 只能被某些類別引用。 第一個限制的情況是這個 Trait 的某些函式必須依賴在特定的變數,但這個變數卻應該由子類別自己來定義實際的值。這個時候我們可以在 Trait 中宣告這個變數的名稱和型別,但不用等號賦與其值。 這個時候,若引入該 Trait 的類別沒有定義 name 欄位的話,那麼和沒有定義 Trait 中未實作的函式的情況相同,該類別必須要被宣告為 abstract 類別,並且一直到其子類別定義該欄位為止。 trait SelfIntroduce { val name: String def sayHello() = println("Hey, I'm " + name) } class Person(val name: String) extends SelfIntroduce val joe = new Person("Joe") joe.sayHello() // 編譯失敗,Fish 必須要有 name 欄位,否則要宣告成 abstract class, // 讓子類別來定義 name 欄位 // class Fish extends SelfIntroduce // 沒問題,把 name 欄位的問題留給下一層的類別再來處理 abstract class Fish extends SelfIntroduce // MyFish 繼承了 Fish 類別,也定義了 name 欄位,所以可以使用 // SelfIntroduce 裡的 sayHello 函式了 class MyFish(val name: String) extends Fish val myFish = new MyFish("Fissy") myFish.sayHello() 除了使用未定義的欄位來限制外,也可以使用 在這個情況下,TraitA 裡面可以參照到 BaseClass 的成員函式和成員變數。 class BaseClass { def sayHello(content: String) = println(content) } class SubClass extends BaseClass { override def sayHello(content: String) = { println("Inside SubClass:" + content) } } class AnotherClass trait UpperCase extends BaseClass { def greet() { println("Greeting from UpperCase") sayHello("Hello!") } } // 沒問題,UpeerCase 可以被 BaseClass 類別引入 val base = new BaseClass with UpperCase base.greet() // 沒問題,UpperCase 可以被 BaseClass 的子類別引入 val sub = new SubClass with UpperCase sub.greet() // 編譯錯誤,AnotherClass 並不是 BaseClass 的子類別, // 所以不能使用 UpperCase // // val another = new AnotherClass with UpperCase 這算是多重繼承嗎?讀者可能會有疑惑,如果 Trait 可以有實作,而一個類別又可以引入多個 Trait,那麼這算不算是多重繼承呢?會不會產生多重繼承中常出現的鑽石問題呢? 答案是否定的,因為在 Scala 中,類別的階層和 Trait 是不一樣的,兩者是不同的關係鍊。 而且當一個類別使用了多個 Trait,而這多個 Trait 又都有同名函式時,Scala 使用了一套固定的順序規範來執行這些同名的函式,所以並不會有混餚的情況產生,你也可以只透過程式碼就知道其執行的結果,並沒有任何的不確定性產生。 簡單來說,若只是單純擁有同名函式的 Trait,那麼被引入時會出現編譯錯誤,因為我們無法判斷要呼叫哪個函式。 trait Hello { def hello() = println("Hello World") } trait CapitalHello { def hello() = println("HELLO WORLD") } // 編譯錯誤 // class A extends Hello with CapitalHello 但另一方面,若具有同名函式的 Trait 都使用了 舉例而言在像下面的程式碼中,由於 capital 這個變數最後引入的是 CapitalHello 這個 Trait,所以當我們使用 class Printable { def hello() = println("Hello World in printable") } trait Hello extends Printable { override def hello() = println("Hello World in Hello") } trait CapitalHello extends Printable { override def hello() = println("HELLO WORLD in CapitalHello") } val capital = new Printable with Hello with CapitalHello val normal = new Printable with CapitalHello with Hello capital.hello() normal.hello() 在這裡值得一提的,是在 Trait 中使用 猜猜看,當我們在上面的程式碼裡的 Hello 和 CapitalHello 這兩個 Trait 中的 class Printable { def hello() = println("Hello World in printable") } trait Hello extends Printable { override def hello() = { println("Hello World in Hello") super.hello() } } trait CapitalHello extends Printable { override def hello() = { println("HELLO WORLD in CapitalHello") super.hello() } } val capital = new Printable with Hello with CapitalHello capital.hello() 答案是會依序印出下面的文字: HELLO WORLD in CapitalHello Hello World in Hello Hello World in printable 為什麼呢?因為我們的在產生 同裡的,若我們使用 Hello World in Hello HELLO WORLD in CapitalHello Hello World in printable 從這個角度上來看,Trait 的功用有有點像是多重繼承,但他又不是。特別是在 Scala 中要記住的,是 Trait 彼此間的關係和 Class 之間的繼承關係是兩個不一樣的東西,Scala 的類別仍然只能繼承自單一類別,而引入 Trait 的順序,會影響到 Trait 當中的 使用 Trait 來實現 Decorator Pattern在上面的例子中我們可以看到,Trait 並不是傳統的多重繼承,而在其中 裝飾者模式指的是某一個類別只專注在一件事情之上,而後透過一層一層包裹的方式,讓其他的類別來提供其他進階的功能。在 Java 中我們最常使用到的裝飾者模式,大概就屬 在下面的程式碼中,我們要試著完成下列的工作:
當我們使用 Scala 的 Trait 的特性時候,這就會變得很簡單了: // 基礎類別 class Printable { def print(content: String) = println(content) } // 因為這些是裝飾者物件,我們只能讓他們可以被 Printable 類別引用 trait Repeat extends Printable { override def print(content: String) = { super.print(content + "\n" + content + "\n" + content) } } trait UpperCase extends Printable { override def print(content: String) = { super.print(content.toUpperCase) } } trait LowerCase extends Printable { override def print(content: String) = { super.print(content.toLowerCase) } } trait Filter extends Printable { override def print(content: String) = { super.print(content.replace("shit", "XXXX")) } } 有了這些程式碼之後,我們就可以透過不同的組合方式,來達到印出不同效果的自串的功能。 // 大寫並且印三次 val upperAndRepeat = new Printable with Repeat with UpperCase upperAndRepeat.print("Hello World") // 小寫並且過濾 val filterAndLower = new Printable with LowerCase with Filter filterAndLower.print("It looks like a shit") 不過在這邊要注意的是引入 Trait 的順序會引響到哪個 Trait 裡的 println 先被呼叫,所以像下面的程式碼中,由於 UpperCase 裡的 println 優先權比較高,已經先把 content 轉成大寫了,所以 Filter 裡的 println 就會沒有辦法過濾掉 SHIT 這個字。 val upperAndFilter = new Printable with Filter with UpperCase // 會印出 IT LOOKS LIKE A SHIT upperAndFilter.print("It looks like a shit") 小結在這一篇文章中,我們看到了 Scala 提供了 Trait 這樣的功能。在最簡單的情況下,他可以如同 Java 中的 interface 一樣來定義介面,但 Trait 也提供了許多額外的功能,特別是當我們在 Trait 中善用 extends 和 super 關鍵字時,將可以很簡單的實作出裝飾者模式。 |