Hello trait
挨拶をするGreeterというtraitを定義しておいて、以下のように実装するクラスを作ることができる。
ここまではJavaのインターフェースともあまり差がない。
trait Greeter {
def greetTo(to: String): String
}
class HelloGreeter(name: String) extends Greeter {
def greetTo(to: String) = s"${name} says 'Hello ${to}!'"
}
new HelloGreeter("Alice").greetTo("Bob") //=> Alice says 'Hello Bob!'
Scalaのtraitはインターフェイスと違い、部分実装(AbstractGreeter)を定義したり、適切な実装を補完することでインスタンス生成も可能になる。
trait Greeter {
def greetTo(to: String): String
}
trait AbstractGreeter extends Greeter {
def name: String
def greet: String
def greetTo(to: String) = s"${name} says '${greet} ${to}!'"
}
class HelloGreeter(val name: String) extends AbstractGreeter {
val greet: String = "Hello"
}
new HelloGreeter("Alice").greetTo("Bob") //=> Alice says 'Hello Bob!'
(new AbstractGreeter { def name = "Alice"; def greet = "Hi" }).greetTo("Bob") //=> Alice says 'Hi Bob!'
ここで、例えば中間実装部分の文言を日本語と英語で扱いたいと考えたとき、以下の様な実装例が考えられる。
しかし、いちいち実装クラスを作ってたら色々面倒だし、かと言って毎回traitからインスタンス生成も非常に辛い。
trait Greeter {
def greetTo(to: String): String
}
trait AbstractEnglishGreeter extends Greeter {
def name: String
def greet: String
def greetTo(to: String) = s"${name} says '${greet} ${to}!'"
}
trait AbstractJapaneseGreeter extends Greeter {
def name: String
def greet: String
def greetTo(to: String) = s"${name} は'${greet} ${to}!'と言いました"
}
class HelloEnglishGreeter(val name: String) extends AbstractEnglishGreeter {
val greet: String = "Hello"
}
class HelloJapaneseGreeter(val name: String) extends AbstractJapaneseGreeter {
val greet: String = "Hello"
}
mix-in
ではこれらを無駄を省きながら綺麗にインスタンス生成できるようにまとめてみよう。
以下が実装例になる。
ポイントはAbstractGreeterに抽象化レイヤーをまとめておいて、実装クラスではその抽象化レイヤーだけを期待する(自分型アノテーション)ことで、コンパイル時にはそれを部分実装した下位のtraitを組み込めることだ。
もちろん、AbstractGreeterの部分実装traitをmix-inするということは、本来の目的であるGreeter型としても利用可能になる。(class定義時にextendsしてないけど)
trait Greeter {
def greetTo(to: String): String
}
trait AbstractGreeter extends Greeter {
def name: String
def greet: String
}
trait EnglishGreeter extends AbstractGreeter {
def greetTo(to: String) = s"${name} says '${greet} ${to}!'"
}
trait JapaneseGreeter extends AbstractGreeter {
def greetTo(to: String) = s"${name} は'${greet} ${to}!'と言いました"
}
class HelloGreeter(val name: String) {
// コンパイル時にmix-inが必要なレイヤー
this: AbstractGreeter =>
val greet: String = "Hello"
// AbstractGreeter のメソッドを利用できる
def yellTo(to: String) = greetTo(to).toUpperCase
}
val alice = new HelloGreeter("Alice") with JapaneseGreeter
alice.greetTo("Bob") //=> Alice は'Hello Bob!'と言いました
alice.yellTo("Bob") //=> ALICE は'HELLO BOB!'と言いました
依存性注入
部分実装した下位のtraitをコンパイル時に組み込めるということは、Springなどのライブラリで定義可能な依存性注入(Dependency Injection: DI)の考え方に近くなる。
例として、Userモデルを取得して、アクティベーションを行うサービスクラスの実装を考える。
val userService: UserService = ???
val alice: User = userService.getById("Alice")
val result: Boolean = userService.activate(alice)
単純に考えれば実装してしまえばいいが、DBにデータを取得しに行くDAO部分と、アクティベーションロジックを管理するActivatorをきちんと分離したくなる。
つまり、以下の様な関連性が望ましい。(だいぶ簡略化した)
class User
trait UserDao {
def dbHost: String
def dbName: String
def getById(id: String): User
}
trait PostgresUserDao extends UserDao {
def getById(id: String): User = ...
}
trait UserActivator {
def activate(user: User): Boolean
}
trait LDAPUserActivator extends UserActicator {
def activate(user: User): Boolean = ...
}
class UserService {
this: UserDao with UserActivator =>
}
// 実装の配線と設定値の挿入
val userService = new UserService extends PostgresUserDao with LDAPUserActivator {
val dbHost = "192.168.1.1"
val dbName = "db1"
}
このように、機能別に実装を分離したり、最終的な配線を一箇所に集約することができる。