やったこと
今年話題になった書籍『テスト駆動開発』ではJavaを例にサンプルが実践されていきますが、それをScalaで実践しました。
コードはGitHubにアップしています。
https://github.com/louvre2489/myscalatest
その際に感じたJavaとScalaの差をまとめてみました。
ステップ数
まずは数字としてどの程度違うのか、と。
テストコードを除くSTEP数(Source Code Lines)は、以下のとおりです。
- Java版:91STEP
- Scala版:67STEP
Javaは完全に書籍とおり(若干違う箇所あるかも?)なのに対して、Scala版を書くにあたっては自由に省力化してしまっているのでやや公平さに欠ける比較ですが、ScalaではJavaの7割程度のコード量でコーディングできることとなります。
case class
Java版では通貨の同一性を確認するためにequalsメソッドを実装しています。
また、メッセージ内容を見やすくするためにtoStringメソッドも実装しています。
public boolean equals(Object object) {
Money money = (Money) object;
return this.amount == money.amount
&& this.currency().equals(money.currency());
public String toString() {
return this.amount + " " + this.currency;
}
}
一方、Scala版ではcase classを使うことでequalsメソッドもtoStringメソッドも自動実装してくれるため、独自実装不要です。
※Scala版ではMoneyを継承したDollarクラス、Francクラスを作成しており、今回はこれらクラスをcase classにしています。
あと、コンストラクタもシンプルになるので非常に楽チン。
public class Sum implements Expression {
Expression augend;
Expression addend;
Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}
...
}
// SumクラスはBank.scala内に定義している
case class Sum(augend: Expression, addend: Expression) extends Expression {
...
}
タプル
Java版では為替レートの換算元/換算先のペアをPairという独自クラスを作成することで保持しようとしています。
public class Pair {
private String from;
private String to;
Pair(String from, String to) {
this.from = from;
this.to = to;
}
...
}
// 使い方
rates.put(new Pair(from, to), 2);
一方、Scalaではタプルが存在するためPairを独自実装する必要はありません。
// 使い方
rates.put((from, to), rate) // (from, to)はタプル
caseオブジェクトとsealed
今回のサンプルではDollarとFrancという2つの通貨を扱うわけですが、Java版は通貨を文字列として扱っています。
でも、これって列挙体で扱いたいですよね?Scala版では勝手にここの扱い方をアレンジしています。
Javaの列挙体のような仕掛けを、Scalaではcaseオブジェクトで実現します。
また、caseオブジェクトで作成したクラス群に対してパターンマッチをする場合に、sealedを使っておくとパターン漏れに対して警告してくれます。今回は2通貨しか扱わなかったので漏れることなんてないんですが、世界には多数の通貨が存在するようなので、数多くを対象にパターンマッチする場合はsealedを使って漏れが発生しないようにするのは非常に有効です。
sealed trait MyCurrency
object MyCurrency {
case object USD extends MyCurrency
case object CHF extends MyCurrency
def apply(currencyUnit: MyCurrency)(amount: Int): Money =
currencyUnit match {
case MyCurrency.USD => Dollar(amount)
case MyCurrency.CHF => Franc(amount)
}
}
以下はパターンマッチを不完全にした例です。
警告が発生して漏れを教えてくれます。
コンパニオンオブジェクト
Java版ではBankのインスタンスを作るためにnewしなければなりません。
// インスタンス化
Bank bank = new Bank();
Scala版ではコンパニオンオブジェクトを作ってFactoryメソッドっぽくBankのインスタンスを生成しています。
class Bank private {
...
}
object Bank {
def apply() = new Bank()
}
// インスタンス化
val bank: Bank = Bank()
この場合はnewを無くしたからといってFactoryメソッドのような恩恵を得られるわけではないと思うのですが、タイプ数は少ないのが大正義だと思っています。
※JavaでもFactoryメソッドを作ったら良いだけなんですけどね。。。
まとめ
目新しいことは何一つ書いていませんが、たかが100STEPにも満たないコードの中にもJavaと比較したScalaのメリットを色々見つけることができました。巨大なシステムともなれば、Scalaによって得られる恩恵はもっと大きくなると思います。
みんなScala使えばいいのになー