Trait는 어떤 클래스의 계층안으로 섞거나(mixin) 자기 것으로 흡수 할 수 있는(assimilate) Behavior입니다. 일부가 구현된 인터페이스와 유사하며 다른 클래스등에 믹스인(Mixin)하거나 포함시킬수 있기 때문에 단일상속과 다중 상속사이에 중간적인 위치에 있다고 할 수 있습니다.
trait SampleTrait {
val data: String
def method() = println("Printed by " + data)
}
Trait는 내부에서 정의하고 초기화된 val과 var는 mix된 클래스들에 제공되며 선언은 되었지만 초기화는 되지 않은 모든 val과 var는 abstract가 되기 때문에 mix된 클래스들은 이것들을 반드시 구현해야 합니다.
class Parent(val data: String) extends SampleTrait
class Child(override val data: String) extends Parent(data)
class Big
class Small(val data: String) extends Big with SampleTrait {
override def method() = println("Small and " + data)
}
val a = new Parent("123")
a.method // Printed by 123
val b = new Parent("456")
b.method // Printed by 456
val c = new Parent("789")
c.method // Small and 789
Parent클래스와 Small클래스가 SampleTrait 트레이트를 믹스인하였습니다. 클래스가 다른 클래스를 상속받지 않는다면 믹스인하기 위해서 extends 키워드를 사용할 수 있으며 상속받은 클래스가 있거나 다수의 Trait를 믹스인 하려면 with 키워드를 사용하면 됩니다.
Trait는 클래스와 중요한 다른점이 2가지 있습니다.
- 선언은 되었지만 초기화는 되지 않은(추상) 변수들과 값들을 구현하기 위해서 mixed-in 클래스를 필요로 합니다.
- 생성자는 어떤 파라미터도 받을 수 없습니다. Trait는 내부에서 구현된 메서드와 일치되는 구현 클래스를 가진 Java Interface로 컴파일 됩니다.
Trait는 믹스인된 클래스의 메서드를 late binding하기 때문에 다중상속에서 보통 발생하는 메서드 충돌문제를 생기지 않습니다. 그래서 Trait내에서 super의 호출은 다른 trait나 믹스된 클래스의 메서드를 호출합니다.
class Example(val data: String)
val d = new Example("abcd") with SampleTrait
d.method // Printed by abcd
인스턴스를 생성할 때 with 키워드를 사용하면 인스턴스레벨에서 선택적으로 Trait로 믹스인 할 수 있습니다. 이는 이미 존재하는 클래스들에 trait를 적용하기 원할 때 유용합니다.
Trait로 Decorating하기
Trait를 클래스를 Decorate하는 용도로 사용할 수 있기 때문에 상황별로 필요한 기능만 추가하여 사용할 수 있습니다.
abstract class Top {
def message(): String = "Top "
}
trait First extends Top {
override def message(): String = "First " + super.message()
}
trait Second extends Top {
override def message(): String = "Second " + super.message()
}
val a = new Top with Second with First
println( a.message ) // First Second Top
val b = new Top with First with Second
println( b.message ) // Second First Top
Trait내에서 super를 사용해서 메서드를 호출하는 것은 늦은 바인딩(late binding)됩니다. 여기서 super는 부모클래스의 메서드를 호출하는 것이 아니라 믹스인된 순서에 따라 왼쪽에 있는 Trait의 메서드를 호출하게 됩니다. 위 예제코드에서 가장 우측에 있는 Trait에서 super.check()의 호출은 그 왼편에 있는 Trait를 호출하게 됩니다.
위 코드에서 Top의 message가 def message() 처럼 추상메서드라면 이를 상속받은 First나 Second는 abstract override 키워드를 사용해서 message를 정의해야 합니다.
함축적인 타입 컨버전(Implicit Type Conversion)
class ExTime(n: Int) {
def hours(h: String): String = {
var result = ""
if (h == "ago") {
result = n + " hours " + h
}
result
}
}
implicit def converter(n: Int) = new ExTime(n)
val ago = "ago"
val t = 3 hours ago
println(t) // 3 hours ago
14번 줄의 3 hours ago는 함수라기 보다는 익숙한 문장처럼 보입니다.(이는 DSL의 특징입니다.) 이는 사실 3.hours(ago)를 의지하지만 3인 Int에는 hours라는 함수가 없습니다. 이를 Implicit Type Conversion을 통해서 해결할 수 있기 때문에 원하는 vocabulary, syntax를 생성하여 DSL(domain-specific language)를 만들수 있게 해줍니다.
DSL용으로 만든 클래스를 implicit 키워드로 메서드로 정의하면 이 정의가 존재하는 스코프내에서는(임포트하거나 파일내에 존재해서 visible한 상태) 자동적으로 타입컨버전이 일어납니다.
메서드를 implicit로 선언하면 현재의 스코프내에 있을경우(import를 하거나 파일내에 존재해서 visible한 상태) 스칼라가 자동으로 선택하게 됩니다. 하지만 컨버전이 필요할때마다 implicit 컨버터를 작성하는 것 보다는 컨버터를 싱글턴 오브젝트로 감춰서 더 나은 재사용성과 사용의 편안함을 얻을 수 있습니다.
object ExTime {
val ago = "ago"
implicit def converter(n: Int) = new ExTime(n)
}
implicit 컨버터를 일일이 작성하는 대신에 해당 클래스를 위와같이 컴패니언 오브젝트로 만들면 ExTime를 임포트해서 바로 타입컨버전을 사용할 수 있습니다. 스칼라는 한번에 1개의 Implict Conversion을 적용하며 타입을 변환함으로써 오퍼레이션, 메서드 호출, 타입컴버전등을 성공할 수 있을 때 컨버전을 적용합니다.
Comments