LL脳がscalaの勉強を始めたよ その76


Scalaコップ本20章の残りをやっていきますよー、今回は抽象型のケーススタディとして通貨クラスのCurrencyを設計していきますよ。全部は終わらんので前後編でお届けします(´・ω・`)

ケーススタディ:通貨計算

通貨クラスCurrencyはそのインスタンスがドル、ユーロ、円等の通貨単位の金額を表現して、かつこれらの通貨金額は計算できるものとして生成されるみたいです。

初期設計の試行錯誤

とりあえずサンプル的にスタートアップCurrencyを書いてみます。ちなみに間違っているサンプルだそうです。

// とりあえずで抽象クラスを設計
abstract class Currency {
  // 金額値の定義です
  val amount:Long
  // 通貨単位の文字列
  def designation:String
  // 金額と通貨単位を表示します
  override def toString = amount + " " + designation
  // 演算用のメソッドデス
  def + (that: Currency):Currency = {}
  def * (x:Double):Currency = {}
}

ちなみにこんな感じで金額と通貨単位を指定することによるインスタンス生成を想定しておりマス

new Currency {
  // 金額を指定します
  val amount = 79L
  // 通貨単位を指定します
  def designation = "USD"
}

ちなみに複数通貨モデリング風に拡張してみますね(´・ω・`)まあ間違いなんですけども

// ドル用サブクラスです
abstract class Dollar extends Currency {
  def designation = "USD"
}

// ユーロ用サブクラスです
abstract class Euro extends Currency {
  def designation = "Euro"
}

//// 実際にオブジェクトを生成すると次のような感じになりますかね
// 10ドルを生成しますよ
val dollar10 = new Dollar {
  val amount = 10L
}
// 10ユーロを生成しますよ
val euro10 = new Euro {
  val amount = 10L
}

ちなみに上記定義の何が間違いかというと、複数の通貨単位で計算しようとした場合に通貨変換が行われないので10ユーロと10ドルを加えた金額が20になってしまいますね。改善点としては個々の通貨に特化した演算用メソッドを適用できるようにするといい感じですかね(´・ω・`)

試行錯誤の設計:通貨単位ごとの演算メソッドの定義

通貨単位ごとに演算メソッドを適用できるようにすると言っても、各演算メソッドを通貨単位ごとに定義するのは正直めんどくさいデス。なので抽象メンバーとして演算メソッドを定義してやることでScala的に対応してやります。

とりあえず、まだまだ不完全ではあるもののサンプル書いてみますよ。抽象型を上手く適用して、各演算のパラメータ型や結果型が具象サブクラスで型を固定することで確定するような設計ですねー

abstract class AbstractCurrency {
  // 上限境界を使って制限した抽象型です
  type Currency <: AbstractCurrency
  val amount:Long
  def designation:String
  override def toString = amount + " " + designation
  // 各演算の結果値やパラメータは型が決定すると確定します
  def  + (that:Currency):Currency = {}
  def  * (x:Double):Currency = {} 
}

とりあえずサブクラスは次のような感じになるですかね

abstract class Dollar extends AbstractCurrency {
  // 型を指定してやりますよ
  type Currency = Dollar
  def designation = "USD"
}

なんとなくソレっぽくなってきましたね(`・ω・´)

演算用メソッドを実装していきますよ

まずは加法メソッドから考えてみます。ものすごく安易に考えるとこんな感じになりますかね

def + (that:Currency):Currency = new Currency {
  val amount = this.amount + that.amount
}

残念ながらコンパイルがとおりません(´;ω;`)

// 型を定義しろって怒られました
<console>:11: error: class type required but AbstractCurrency.this.Currency found
         def + (that:Currency):Currency = new Currency {
                                              ^
<console>:12: error: recursive value amount needs type
           val amount = this.amount + that.amount
                             ^
<console>:11: error: type mismatch;
 found   : AbstractCurrency.this.Currency{ ... }
 required: AbstractCurrency.this.Currency
         def + (that:Currency):Currency = new Currency {
                                          ^

どうもScalaの抽象型はインスタンスを作ったり、他のクラスのスーパー型にしたりすることができないという制限があるのだとか(´・ω・`)なので上記サンプルのようにインスタンスを生成するようなコードはコンパイラ様に怒られてしまうとのことデス。

上記のような問題は抽象型のインスタンスを直接作るのではなく、インスタンスを生成するための抽象メソッドを宣言するようなファクトリーメソッドを使うことで回避できるそうです(´・ω・`)とりあえずやっつけっぽくファクトリーメソッドを導入してみますよ

abstract class AbstractCurrency {
  // 抽象型の定義
  type Currency <: AbstractCurrency
  // ファクトリーメソッド
  def make(amount:Long):Currency
  // 以下同文
  val amount:Long
  def designation:String
  override def toString = amount + " " + designation
  def  + (that:Currency):Currency = {
    // 調整中
  }
  def  * (x:Double):Currency = {} 
}

ただし、ファクトリーメソッドを導入すると具象実装を行うたびにファクトリーメソッドの実装を行う必要があるのに加えて、上記のようなコードだとファクトリーメソッドを外部から乱発することで金額を増やすことが可能になってしまいますデス(´・ω・`)例えばこんなイメージみたいです

// 1ドルの通貨オブジェクト
val myDollar = // 何らかの生成コード
// 100ドル増やしてみますよ
myDollar.make(100)

また、オブジェクト経由でファクトリーメソッドを呼び出しているんですが、そもそものオブジェクト生成コードがわからんのです(´・ω・`)なので初期生成メソッドを別に用意して…ウンヌンをする必要があると…なんだから胡散臭い感じになってきました

抽象型とファクトリーメソッドを外にだそうぜ!

上記の問題は抽象型とファクトリーメソッドをクラス内部に作成したとから発生した問題なので思い切って外に押し出してやりますよ。具体的にはクラス内クラスとしてAbstractCurrencyを定義して、外側のクラス内で抽象型とファクトリーメソッドを定義してやりますねー

// 通貨地域として大外クラスを定義しますよ
abstract class CurrencyZone {
  // 抽象型の定義です
  type Currency <: AbstractCurrency
  // ファクトリーメソッドを抽象メソッドとして定義します
  def make(x:Long):Currency
  // 実際に通貨オブジェクトの定義をします
  abstract class AbstractCurrency {
    val amount:Long
    def designation:String
    override def toString = amount + " " + designation
    // 加法メソッドの定義はこんな感じですかねー
    def + (that:Currency):Currency = {
      make(this.amount + that.amount)
    }
    // ついでに乗法メソッドも定義してしまいますね
    def * (x:Double):Currency = {
      make((this.amount * x).toLong)
    }
  }
}

では上記クラスのサンプル具象クラスを定義してみますねー、通貨地域をアメリカに設定しますね

// オブジェクトとしてCurrencyZoneを拡張しますよ
object US extends CurrencyZone {
  // AbstractCurrencyのサブクラスを内部的に定義してやりますよ
  abstract class Dollar extends AbstractCurrency {
    def designation = "USD"
  }
  // 型を具象化します
  type Currency = Dollar
  // ファクトリーメソッドを具象実装します
  def make(x:Long) = new Dollar{val amount = x}
}

おお、なんだかすごいなぁ(´・ω・`)これが、Scalaっぽい書き方なんですかねー、ここらへんがスルッと出てくるようにしないといけないですね。ちなみに基本的な設計はコレでOKらしいです(´・ω・`)

ちょっくら試してみますかね

せっかくなのでここまでの部分を動かしてみますよ、具象クラスとしてUSの値を使ってみますねー

// //通貨オブジェクトを生成しますよ
// 10ドルです
scala> val doller10 = US.make(10)
doller10: US.Dollar = 10 USD
// 5ドルです
scala> val doller5 = US.make(5)  
doller5: US.Dollar = 5 USD

// 加法演算を試してみますかね
scala> doller10 + doller5
res1: US.Currency = 15 USD

// んじゃ、乗法を試してみますよー
scala> doller10 * 5      
res3: US.Currency = 50 USD

おお、できましたー

いじょー

きりが良いので今回はここらへんでー、次回は作成したCurrency関連の補助通貨対応なんかの細かい部分を詰めていきますよー