[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
Scalaにはオブジェクトを抽象化する方法に「トレイト(trait)」と「抽象クラス(abstract class)」を使用する方法があります。
ではトレイトと抽象クラスのどちらを使用するのがよいのでしょうか?
この記事ではトレイトと抽象クラスについて解説します。
抽象クラスは実装を持たない抽象メンバーを持つクラス
「抽象クラス」は、実装を持たない「抽象メンバー」を持つクラスです。
以下のようにabstrace
キーワードをclass
の前につけて定義します。
abstract class AbstractClass
抽象クラスには以下の特徴があります。
- 直接インスタンスを作ることはできない
- 抽象メンバーはサブクラスで実装する
直接インスタンスを作ることはできない
抽象クラスから直接インスタンスを作ることはできません。
抽象クラスをnew
するとコンパイルエラーになります。
new AbstractClass ^ error: class AbstractClass is abstract; cannot be instantiated
抽象クラスを継承してサブクラスを作り、サブクラスからインスタンスを作成します。
以下のように抽象クラスを継承して作成したサブクラスを具象クラスと呼びます。
class SubClass extends AbstractClass val obj: AbstractClass = new SubClass
抽象メンバーはサブクラスで実装する
最初に説明したとおり、抽象クラスは実装を持たいない抽象メンバーを持つクラスです。
以下の例では、motherTongue
とgreet
が抽象メンバーです。
abstract class Person: val motherTongue: String def greet(): String
サブクラスで抽象メンバーを具体的に定義します。
override
キーワードは省略可能ですが、抽象メンバーの具体的な実装であることを表すためにつけたほうが良いです。
class Japanese extends Person: override val motherTongue: String = "Japanese" override def greet(): String = "こんにちは" class Chinese extends Person: override val motherTongue: String = "Chinese" override def greet(): String = "你好啊"
定義した具象クラスからインスタンスを作成して、メンバー変数とメソッドにアクセスしてみます。
val person1: Person = new Japanese() val person2: Person = new Chinese() println(s"person1: 母国語: ${person1.motherTongue}, 挨拶: ${person1.greet()}") println(s"person2: 母国語: ${person2.motherTongue}, 挨拶: ${person2.greet()}")
実行結果は以下のようになります。
person1: 母国語: Japanese, 挨拶: こんにちは person2: 母国語: Chinese, 挨拶: 你好啊
トレイトはメソッドとフィールドの定義をカプセル化したもの
「トレイト」はメソッドとフィールドをカプセル化したものであり、コードを再利用するために利用します。
また、「抽象クラス」と同様に「抽象メンバー」を持つこともできます。
トレイトは以下のようにtrait
キーワードを使用して定義します。
trait TraitA
トレイトには以下の特徴があります。
- 直接インスタンスを作成することはできない
- 抽象メンバーを持つことができる
- パラメータを持つことができない
- 複数のトレイトを1つのクラスやトレイトにミックスインできる
直接インスタンスを作成することはできない
トレイトから直接インタンスを作成できません。
トレイトをnew
すると抽象クラスと同様にコンパイルエラーになります。
new TraitA ^ error: trait TraitA is abstract; cannot be instantiated
トレイトはクラスやトレイトにミックスインして使用します。
トレイトをミックスインするには、抽象クラスと同様にextends
キーワードを使用します。
class ClassA extends TraitA
抽象メンバーを持つことができる
トレイトは抽象クラスと同様に抽象メンバーを持つことができます。
抽象クラスで定義したPersonクラスをトレイトを使って定義し直してみます。
抽象クラスとの違いは、abstract class
とtrait
のみであることがわかります。
trait Person: val motherTongue: String def greet(): String
では、トレイトをミックスインしたクラスを定義してみます。
抽象クラスを継承した場合と全く同じコードになりました。
class Japanese extends Person: override val motherTongue: String = "Japanese" override def greet(): String = "こんにちは" class Chinese extends Person: override val motherTongue: String = "Chinese" override def greet(): String = "你好啊"
定義したクラスの実行結果も抽象クラスを使用した場合と同じです。
val person1: Person = new Japanese() val person2: Person = new Chinese() println(s"person1: 母国語: ${person1.motherTongue}, 挨拶: ${person1.greet()}") println(s"person2: 母国語: ${person2.motherTongue}, 挨拶: ${person2.greet()}")
person1: 母国語: Japanese, 挨拶: こんにちは person2: 母国語: Chinese, 挨拶: 你好啊
(Scala 2系まで)パラメータを持つことができない
Scala 2系ではトレイトはクラスとは違いパラメータを持つことができません。
クラスは以下のようにパラメータを持てますが、
class ClassA(val param: String)
トレイトはパラメータを持てません。
以下のコードはコンパイルエラーになります。
trait TraitA(val param: String)
トレイトでは以下のように抽象メンバーを定義してミックスインしたクラスでパラメータを定義します。
trait TraitA: val param: String class ClassA(val param: String) extends TraitA
この制限はScala3でなくなりました。
複数のトレイトを1つのクラスやトレイトにミックスインできる
トレイトは複数のトレイトをミックスインすることができます。
複数のトレイトをミックスインする場合は、with
キーワードを使用します。
class ClassA extends TraitA with TraitB
また、インスタンスを作成する際にトレイトをミックスインすることもできます。
class ClassA val obj: TraitA = new ClassA() with TraitA
トレイトには抽象クラスと違った使い方がある
次にトレイトの抽象クラスとは違う使い方として以下の2つの方法を紹介します。
- クラスにインターフェースを追加する
- ミックスインしたクラスのメソッドに変更を加える
クラスにインターフェースを追加する
トレイトを使用してクラスにインターフェースを追加することができます。
以下の例ではPersonalityOfBloodType
トレイトを使用してpersonality
メソッドをJapanese
クラスに追加します。
trait PersonalityOfBloodType: val bloodType: String def personality(): String = bloodType match case "O" => "おおらか" case "A" => "几帳面" case "B" => "わがまま" case "AB" => "天才" class Japanese(override val bloodType: String) extends Person with PersonalityOfBloodType: override val motherTongue: String = "Japanese" override def greet(): String = "こんにちは"
Japanese
クラスのインスタンスに対してPersonalityOfBloodType
トレイトで定義したメソッドを呼び出すことができます。
val person = new Japanese("AB") println(s"あなたの性格は、「${person.personality()}」です。")
実行結果は以下のとおりです。
あなたの性格は、「天才」です。
ミックスインしたクラスのメソッドに変更を加える
次にトレイトを使用してクラスのメソッドに変更を加える方法を紹介します。
以下の例ではLogger
トレイトを使用してJapanese
クラスのgreet
メソッドが呼び出されたときにログメッセージを表示するようにしています。
trait Logger extends Person: abstract override def greet(): String = println("greet is called") super.greet() class JapaneseWithLogger extends Japanese with Logger
Logger
トレイトはPerson
クラスを継承しています。
これは、Logger
トレイトをミックスインできるのはPerson
クラスの子クラスのみであることを意味します。
greet
メソッドをabstract override
キーワードを付けて定義し、その中でsuper
で親のメソッドを呼び出しています。
実行結果は以下のとおりです。
val person = new JapaneseWithLogger() println(person.greet())
greet is called こんにちは
Logger
トレイトからsuper.greet()
を呼び出すと何が実行されるのでしょうか?
Logger
トレイトが継承したPerson
クラスのgreet
メソッドは抽象メンバーです。
そのため、Person
クラスの子クラスではこのような実装はできません。
トレイトの場合、トレイト内でのsuper
呼び出しは、トレイトを定義した時点で決定するのではなく動的に束縛されます。
そのため、トレイトをミックスインしたクラスの具象メンバーを呼び出すことになります。
トレイトと抽象クラスの共通点と相違点
ここまでの説明でトレイトと抽象クラスには以下の共通点があることがわかります。
- それ自身から直接インスタンスを作成することができない
- 抽象メンバーを持てる
- 継承またはミックスインして具象クラスを作成して使用する
また、以下の違いがあることがわかります。
- 複数のトレイトを1つのクラスにミックスインできるが、複数のクラスを継承したクラスを作ることはできない
- (2系まで)トレイトはパラメータを持てないが、抽象クラスはパラメータを持つことができる
トレイトと抽象クラスを使い分ける
抽象クラスとトレイトはどちらもオブジェクトを抽象化するために利用することができます。
ではトレイトと抽象クラスのどちらを使用するのが良いでしょうか?
抽象クラスとトレイトは以下のように持つ意味に違いがあります。
- 抽象クラス: それが何であるかを表現する
- トレイト: それがどういった振る舞いをするのかを表現する
抽象クラスには、クラスは1つのスーパークラスしか持てないという制約がありますので、それが何であるか(is-aの関係性)を明確に示したい場合に使用しましょう。
そうでなければトレイトを使うのが良いでしょう。
scala3ではパラメータをもつトレイトを使用できる
前述のトレイトと抽象クラスの違いの説明でトレイトはパラメータを持つことができないと説明しました。
これはScala2の制限であり、Scala3ではトレイトがパラメータを持てるように変更されました。
以下のコードはScala2ではコンパイルエラーになりますが、Scala3ではコンパイルできます。
trait TraitA(val param: String) class ClassA(override val param: String) extends TraitA(param) val obj: TraitA = new ClassA("value x")
詳細は以下の記事を参照してください。