1. 序章

トレイトは、クラスの動作を拡張するために使用できる再利用可能なコンポーネントです。 これらはインターフェースに似ており、抽象的および具体的なメソッドとプロパティの両方が含まれています。

このチュートリアルでは、特性を作成および拡張する方法を見てみましょう。

2. 例

映画音楽をモデル化するための工夫された例を考えてみましょう。

2.1. トレイトの作成と拡張

ミュージカルスコアにはコンポジションが必要です。 Composition特性を作成することから始めましょう。

trait Composition {
  var composer: String

  def compose(): String
}

上記を拡張して、Scoreクラスを作成しましょう。

class Score(var composer: String) extends Composition {
  override def compose(): String = s"The score is composed by $composer"
}

ご覧のとおり、トレイトを拡張するときは、すべての抽象メンバー(メソッドとプロパティの両方)の実装を提供する必要があります。 実装をスキップしたい場合は、継承クラスを抽象化する必要があります

2.2. 複数の特性を拡張する

スコアにもサウンド制作が必要です。 同じの特性を作成しましょう

trait SoundProduction {
  var engineer: String

  def produce(): String
}

Score クラスを変更して、上記の特性も拡張してみましょう。

class Score(var composer: String, var engineer: String)
  extends Composition with SoundProduction {

  override def compose(): String = s"The score is composed by $composer"

  override def produce(): String = s"The score is produced by $engineer"
}

複数の特性を継承する場合、キーワードextendsは最初の特性にのみ使用できることに注意してください。後続の特性では、キーワードwithを使用する必要があります。

2.3. 別の特性で特性を拡張する

コンポジションにはオーケストレーションミキシングが必要です。 それでは、Orchestrationの特性を作成しましょう。

trait Orchestration {
  var orchestra: String
}

そしてミキシング:

trait Mixing {
  var mixer: String
}

コンポジショントレイトを変更して、上記の両方のトレイトを拡張してみましょう。

trait Composition extends Orchestration with Mixing {
  var composer: String

  def compose(): String
}

Composition はそれ自体がトレイトであるため、親トレイトの抽象メンバーをオーバーライドする必要はありません。 代わりに、Scoreクラスのこれらのメンバーをオーバーライドしてみましょう。

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String)
  extends Composition with SoundProduction {

  override def compose(): String =
    s"""The score is composed by $composer,
       |Orchestration by $orchestra,
       |Mixed by $mixer""".stripMargin

  override def produce(): String = s"The score is produced by $engineer"
}

2.4. コンクリート部材のオーバーライド

すべてのミキシングには、ミキシングするための品質比とアルゴリズムが必要です。 Mixed トレイトで両方の具象部材を作成して、デフォルトの機能を提供できます。

val qualityRatio: Double = 3.14 
def algorithm: String = "High instrumental quality"

トレイトの具象メンバーをオーバーライドすることはオプションです。 この例のために、ScoreクラスのqualityRatioalgorithmの両方をオーバーライドすることを検討してみましょう。

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String,
            override val qualityRatio: Double)
  extends Composition with SoundProduction {

  // Other fields defined previously

  override def algorithm(): String = {
    if (qualityRatio < 3) "Low instrumental quality"
    else super.algorithm
  }
}

2.5. テスト

Score クラスをインスタンス化し、さまざまなメソッドをテストするときが来ました。

class ScoreUnitTest {

  @Test
  def givenScore_whenComposeCalled_thenCompositionIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 10, studio)

    assertEquals(score.compose(),
      s"""The score is composed by $composer,
        |Orchestration by $orchestra,
        |Mixed by $mixer""".stripMargin)
    }

  @Test
  def givenScore_whenProduceCalled_thenSoundProductionIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 3, studio)

    assertEquals(score.produce(), s"The score is produced by $engineer")
  }

  @Test
  def givenScore_whenLowQualityRatioSet_thenCorrectAlgorithmIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 1, studio)

    assertEquals(score.algorithm(), "Low instrumental quality")
  }

  @Test
  def givenScore_whenHighQualityRatioSet_thenCorrectAlgorithmIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 10, studio)

    assertEquals(score.algorithm(), "High instrumental quality")
  }
}

2.6. オブジェクトインスタンスへのトレイトの追加

上記のメンバーに加えて、特定のスコアにもボーカルが必要な場合があります。 ただし、ここでの問題は、すべてのScoreインスタンスが同じものを必要とするわけではないということです。

最初にVocalsの新しい特性を作成して、それを解決しましょう。

trait Vocals {
  val sing: String = "Vocals mixin"
}

ここで、いくつかのScoreインスタンスが上記の特性を簡単に継承できるようにする必要があります。 そのために、 Scalaは、トレイトをオブジェクトインスタンスに直接アタッチする方法を提供します

// Initialize the other fields
val score = new Score(composer, engineer, orchestra, mixer, 10) with Vocals

assertEquals(score.sing, "Vocals mixin")

2.7. 特性を継承するクラスを制限する

ScoreSoundProductionを持つことができるのは、レコードレーベルから資金提供を受けている場合のみであるという別の制限について考えてみましょう。 そのために、新しいRecordLabelタイプを作成します。

class RecordLabel

S oundProductionRecordLabelタイプも拡張する場合に限り、タイプごとに拡張できるようにする必要があります。

これを行う1つの方法は、 SoundProduction特性をRecordLabel:に拡張することです。

trait SoundProduction extends RecordLabel { 
  // Other methods previously defined 
}

ScoreクラスのRecordLabelタイプを拡張すると、コンパイルエラーがないことがわかります。

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String,
            override val qualityRatio: Double,
            var studio: String)
  extends RecordLabel with Composition with SoundProduction { 

  // Other methods previously defined
}

クラスを拡張するトレイトは一般的ではないため、より洗練された方法は、SoundProductionトレイトのthisプロパティに制限タイプを設定することです。

trait SoundProduction {
  this: RecordLabel =>
  
  // Other methods previously defined
}

3. 多重継承競合の解決

どのスコアでも、コンポジションサウンドプロダクションは別々のレコーディングスタジオで行うことができます。 getStudioというメソッドをCompositionトレイトに追加しましょう。

var studio: String
def getStudio(): String = s"Composed at studio $studio"

そして、 SoundProduction へ:

var studio: String
def getStudio(): String = s"Produced at studio $studio"

Scoreクラスの上記のメソッドをオーバーライドしてみましょう。

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String,
            override val qualityRatio: Double,
            var studio: String)
  extends RecordLabel with Composition with SoundProduction {

  // Other methods previously defined

  override def getStudio(): String = super.getStudio()
}

両方の親特性に同じメソッドシグネチャがあるため、 Scoreクラスでsuper.getStudio()を呼び出すと競合が発生します。 Scalaがどのように競合を自動的に解決するか、そしてどのように自分たちで解決を強制できるかを見てみましょう。

3.1. デフォルトの競合解決

デフォルトでは、Scalaは右優先検索と深さ優先検索を使用して親トレイトのメソッドを検索します。

Score クラスは、最初に Composition 特性を拡張し、次に SoundProduction 特性を拡張するため、 getStudio()の対応するメソッドを呼び出します。 ]SoundProduction特性。 単体テストでも同じことを確認できます。

// Initialize the other fields
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 10, studio)

assertEquals(score.getStudio(), s"Produced at studio $studio")

3.2. 明示的な競合の解決

親特性で競合するメソッドを明示的に呼び出したい場合は、次に、s uperキーワードに次のタイプを指定できます。

override def getStudio(): String =
  super[Composition].getStudio() + ", " + super[SoundProduction].getStudio()

ユニットテストで上記の動作を見てみましょう。

assertEquals(
  score.getStudio(),
  s"Composed at studio $studio, Produced at studio $studio"

3.3. Java8インターフェースとの比較

Scala 2.12以降、トレイトは単一のインターフェースクラスファイルにコンパイルされます。 これは、インターフェースの具象メソッド(デフォルトメソッドとも呼ばれる)のJava8サポートのために可能です。

ただし、トレイトとインターフェイスには大きな違いがあります。 Javaのデフォルトメソッドの自動競合解決はありません。したがって、親インターフェイスに競合するメソッドがある場合、コンパイラは次のことを期待します。 superキーワードを使用して競合を解決します。

4. 封印された特性

最後に、Mixedトレイトのアルゴリズムを列挙します。 MixedAlgorithmを表す封印された特性を作成しましょう。

sealed trait MixingAlgorithm

同じファイルで、封印されたトレイトを拡張してケースオブジェクトを作成しましょう。

case object LowInstrumentalQuality extends MixingAlgorithm {
  override def toString(): String = "Low instrumental quality"
}

case object HighInstrumentalQuality extends MixingAlgorithm {
  override def toString(): String = "High instrumental quality"
}

Mixedトレイトのalgorithm()を変更して、新しい列挙型を返しましょう。

def algorithm: MixingAlgorithm = HighInstrumentalQuality

Score クラスのオーバーライドされたメソッドを変更して、新しいタイプを返しましょう。

override def algorithm(): MixingAlgorithm = {
  if (qualityRatio < 3) LowInstrumentalQuality
  else super.algorithm
}

ケースオブジェクトのtoStringをオーバーライドしたので、テストは同じものを使用して値をアサートできます。

assertEquals(score.algorithm().toString, "High instrumental quality")

封印された特性には、いくつかの重要な特性があることに注意してください。

  • 封印された特性は、その宣言と同じファイルでのみ拡張できます
  • コンパイラは、封印された特性のすべての可能なサブタイプを認識しているため、 case の一致を見逃した場合に、網羅性チェックを実行して警告をスローできます。

5. 抽象クラスとの比較

トレイトと抽象クラスの両方が、再利用性のメカニズムを提供します。 ただし、2つの間にいくつかの基本的な違いがあります。

  • 複数の特性から拡張できますが、抽象クラスは1つだけです
  • 抽象クラスはコンストラクターパラメーターを持つことができますが、トレイトは持つことができません

6. 結論

このチュートリアルでは、特性を作成および拡張する方法と、それらが抽象クラスとどのように異なるかを確認しました。

いつものように、例の完全なソースコードは、GitHubから入手できます。