web-dev-qa-db-ja.com

Scala caseクラスを宣言することの欠点は何ですか?

多くの美しく不変のデータ構造を使用しているコードを書いている場合、ケースクラスは天の恵みのように見え、たった1つのキーワードで以下のすべてを無料で提供します。

  • デフォルトではすべて不変
  • 自動的に定義されたゲッター
  • まともなtoString()実装
  • 準拠するequals()およびhashCode()
  • マッチングのためのunapply()メソッドを備えたコンパニオンオブジェクト

しかし、不変のデータ構造をケースクラスとして定義することの欠点は何ですか?

クラスまたはそのクライアントにどのような制限がありますか?

ケース以外のクラスを好むべき状況はありますか?

102
Graham Lea

1つの大きな欠点は、ケースクラスがケースクラスを拡張できないことです。それが制限です。

あなたが見逃した他の利点、完全性のためにリストされています:準拠したシリアル化/逆シリアル化、作成するために「新しい」キーワードを使用する必要はありません。

私は、可変状態、プライベート状態、または状態なしのオブジェクト(たとえば、ほとんどのシングルトンコンポーネント)には、ケース以外のクラスを好みます。他のほとんどすべてのケースクラス。

49
Dave Griffith

ここでTDDの原則が当てはまると思います。過剰に設計しないでください。何かをcase classと宣言すると、多くの機能を宣言していることになります。これにより、将来クラスを変更する際の柔軟性が低下します。

たとえば、case classには、コンストラクタパラメータに対するequalsメソッドがあります。クラスを最初に作成するときは気にかけないかもしれませんが、後者では、これらのパラメーターの一部を無視して平等にしたい場合や、少し違うことをしたい場合があります。ただし、クライアントコードは、case classの平等に依存する平均時間で記述される場合があります。

10

ケース以外のクラスを好むべき状況はありますか?

Martin Oderskyは、コースの良い出発点を教えてくれます Scalaの関数型プログラミングの原則 (講義4.6-パターンマッチング)これは、クラスとケースクラスを選択する必要があるときに使用できます。 Scala By Example の第7章には同じ例が含まれています。

たとえば、算術式のインタープリターを作成したいとします。最初は物事をシンプルにするために、数字と+操作のみに制限します。そのような表現は、抽象基本クラスExprをルート、2つのサブクラスNumberおよびSumを持つクラス階層として表すことができます。次に、式1 +(3 + 7)は次のように表されます。

new Sum(new Number(1)、new Sum(new Number(3)、new Number(7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

さらに、新しいProdクラスを追加しても、既存のコードは変更されません。

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

対照的に、新しいメソッドを追加するには、既存のすべてのクラスの変更が必要です。

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

同じ問題がケースクラスで解決されました。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

新しいメソッドの追加はローカルでの変更です。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

新しいProdクラスを追加するには、潜在的にすべてのパターンマッチングを変更する必要があります。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

ビデオレクチャーからの転写 4.6パターンマッチング

これらのデザインはどちらも完璧に優れており、どちらを選択するかはスタイルの問題である場合がありますが、それでも重要な基準がいくつかあります。

1つの基準は、式の新しいサブクラスをより頻繁に作成するのか、または新しいメソッドをより頻繁に作成するのですか?システムの将来の拡張性と可能な拡張パス。

主に新しいサブクラスを作成する場合は、実際にはオブジェクト指向の分解ソリューションが優先されます。その理由は、evalメソッドを使用して新しいサブクラスを作成するだけで非常に簡単で非常にローカルな変更であり、機能的なソリューションのように、戻ってコードを変更する必要があるためですevalメソッド内で新しいケースを追加します。

一方、あなたがやることが多くの新しいメソッドを作成するが、クラス階層自体が比較的安定している場合、パターンマッチングは実際に有利です。繰り返しますが、パターンマッチングソリューションの各新しいメソッドは、ベースクラスに配置するか、クラス階層の外部に配置するかに関係なく、単なるローカル変更にすぎません。オブジェクト指向の分解に示すような新しいメソッドでは、各サブクラスに新しい増分が必要になります。だからもっと多くの部品があるだろう、あなたが触れなければならないこと。

そのため、階層に新しいクラスを追加する場合、または新しいメソッドを追加する場合、あるいはその両方の場合の2次元でのこの拡張性の問題は、式の問題と命名されました。

覚えておいてください:これを唯一の基準ではなく、出発点として使用する必要があります。

enter image description here

6
gabrielgiussi

Scala cookbook by Alvin Alexander第6章:objectsから引用しています。

これは、私がこの本で面白かった多くのことの1つです。

ケースクラスに複数のコンストラクターを提供するには、ケースクラス宣言が実際に何をするかを知ることが重要です。

case class Person (var name: String)

Scalaコンパイラがケースクラスの例に対して生成するコードを見ると、Person $ .classとPerson.classの2つの出力ファイルが作成されていることがわかります。Person javapコマンドを使用した$ .classには、applyメソッドと他の多くのメソッドが含まれていることがわかります。

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final Java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(Java.lang.String); // the apply method (returns a Person) public Java.lang.Object readResolve();
        public Java.lang.Object apply(Java.lang.Object);
    }

Person.classを逆アセンブルして、その内容を確認することもできます。このような単純なクラスの場合、追加の20のメソッドが含まれます。この隠れた肥大化は、一部の開発者がケースクラスを好まない理由の1つです。

0
arglee