Chapter 11

第十一章 連想配列を利用した createWeaponInstance から switch を無くす方法

kuramapommel
kuramapommel
2021.05.07に更新

連想配列の紹介

まず、この章では 連想配列 を使ってこのコードから switch を消す方法をご紹介していきますが、 「必ず使ったほうが良いよ」というものではございません
ちょっとしたテクニックとして覚えておいてもらえると応用できる場面が出てくると思います

ちなみに連想配列について軽く説明しておきますと、 key-value の組み合わせを管理するための配列です
あまり良い例ではないかもしれませんが、例えば 「赤い果物」から「りんご」が連想できるとき、赤い果物を key として指定すると、配列はりんごを value として返してくれる みたいなものをイメージすると想像しやすいかもしれません

連想配列を図で表すとこんな感じです

key value
赤い果物 りんご
紫の果物 ぶどう
オレンジの果物 みかん

これを JavaScript の連想配列実装である Map で表すとこんな感じになります

const fruitMap = new Map([
  [
    // key-value の組み合わせを管理できる
    "赤い果物", "りんご"
  ],
  [
    "紫の果物", "ぶどう"
  ],
  [
    "オレンジの果物", "みかん"
  ],
])

今回の章ではこの子を使って 8 章のコードの createWeaponInstance 関数から switch を取り除いてみたいと思います

ちなみに他の言語でもほとんどの言語において標準ライブラリに HashMapDictionary のような実装は存在すると思います
ご自身がお使いの言語が JavaScriptTypeScript ではない場合は「言語名 連想配列」とかで調べてみてください

Map を使って createWeaponInstance 関数から switch を取り除いてみる

8 章のコードの createWeaponInstance 関数を抜粋してみましょう

function createWeaponInstance(type: WEAPON_TYPE): Weapon {
  switch (type) {
    case WEAPON_TYPE.LONG_SWORD:
      return new LongSword()

    case WEAPON_TYPE.DUAL_BLADES:
      return new DualBlades()

    case WEAPON_TYPE.BOW:
      return new Bow()
  }
  
  throw Error()
}

まず、 このコードはプロダクションレベルで扱っても問題ない コードです
WEAPON_TYPE による Weapon の具体的なインスタンスの選択がここに凝集されているので、 Weapon を使いたいときはこの関数を呼ぶだけで済みますし、今後新しい武器の種類が追加された場合もこの関数に新しい case を追加するだけで良いので、 運用保守性が十分に足りていると言える でしょう

ですので、仮にこのコードがプルリクエストとして投げられたとしてもぼくは thumbs up すると思います

このコードを Map を使って書き換えてみるとこんな感じになります

// 1. 武器を生成する責務を持つ型を定義して
interface WeaponFactory {
  create(): Weapon
}

// 2. それぞれの具象クラスを実装
class LongSwordFactory implements WeaponFactory {
  public create(): Weapon {
    return new LongSword();
  }
}

class DualBladesFactory implements WeaponFactory {
  public create(): Weapon {
    return new DualBlades();
  }
}

class BowFactory implements WeaponFactory {
  public create(): Weapon {
    return new Bow();
  }
}

// 3. Map 型を用いて key-value のスタイルで WEAPON_TYPE とそれぞれの WeaponFactory の具象クラスのインスタンスを紐付ける
// Map 型は 1 つ目の型引数に key となる型、 2 つ目の型引数に value となる型を指定します
const weaponFactoryMap: Map<WEAPON_TYPE, WeaponFactory> = new Map([
  [
    // key は WEAPON_TYPE, value は WeaponFactory の具象クラスのインスタンス
    WEAPON_TYPE.LONG_SWORD, new LongSwordFactory()
  ],
  [
    WEAPON_TYPE.DUAL_BLADES, new DualBladesFactory()
  ],
  [
    WEAPON_TYPE.BOW, new BowFactory()
  ],
])

// 4. もともと switch を使ってどのインスタンスを使うかを選択していたが、 Map を用いることで switch を使う必要なく武器の生成を行えるようになった
function createWeaponInstance(type: WEAPON_TYPE): Weapon {
  const weaponFactory = weaponFactoryMap.get(type)
  return weaponFactory.create()
}

ちょっと長いですが、コード内のコメントにもあるように重要なのは 4 つです

  1. 武器を生成する責務を持つ型 WeaponFactory を定義
  2. それぞれの具象クラスを実装
  3. Map 型を用いて key-value のスタイルで WEAPON_TYPE とそれぞれの WeaponFactory の具象クラスのインスタンスを紐付ける
  4. createWeaponInstanceMap を用いることで switch を使う必要なく武器の生成を行えるようになる

switch の持つフォールスルーという機能

で、これの何が嬉しいのかというと、 「分岐の条件」と「振る舞い」をひとつのセットとして扱うことができる んですね

ちょっと話は逸れますが、 switch の持つ 「便利だったはずなのに実際はバグの温床になってしまった機能」 こと フォールスルー についてお話ししたいと思います

下記コードをご覧ください

switch (num) {
  case 1:
  case 2:
    console.log(num)
}

これ、コンパイル通っちゃいます
コンパイラさんに怒られないんですね

何故かと言うと、そもそも言語仕様的に認められているからなんですよ
どんな時に使うの?と思われるかもしれませんが、こういう時に使えます

switch (pet) {
  case "ポメラニアン":
  case "ダックスフント":
    console.log("わんっ!")
    break
  case "ヒマラヤン":
    console.log("にゃ〜ん")
    break
}

こんな感じで いくつかの条件を同じグループとしてまとめて、同じ振る舞いをさせたい みたいな時に使えるんですね
これを フォールスルー と呼びます

便利な機能に思えますよね?でも、ひとつ前のコードを見たらこの機能がバグの温床になり得ることがご理解いただけるかと思います
break したはずなのに! return したはずなのに!忘れてた!
そう、なんとも悲しいことに意図していないフォールスルーは起こってしまうんですね

つまり、 switch ~ case を読む時には、 フォールスルーしているか否かに注意しながら読む 必要が出てきます

連想配列を利用することでフォールスルーを防ぐ

話を戻して、「分岐の条件」と「振る舞い」をひとつのセットとして扱うことができるという Map のメリットについてですが、もうお分かりのように Map を使えばフォールスルーを防ぐことができます
必ず 1 対 1 で条件と振る舞いが紐付くので、読み手は自信を持ってその時追っている箇所だけを読めばいいと確信できます

ただ、デメリットもあります
Map はインスタンス化されたものなので、メモリを食う という点ですね
ただ、最近はマシンスペックが上がっているので、メモリ管理と可読性のトレードオフでは可読性が優先される傾向にあります
(もちろん、低レイヤな実装であればその限りではないです)

冒頭でもお話しした通り、無理して使うものでもなく、 Tips として覚えておくと使える場面が出てくると思います

[余談] フォールスルーが禁止されている言語や switch が文じゃなく式となっている言語の紹介

ちなみに余談ですが、フォールスルーができない(コンパイラさんに怒られる)言語もあります
また、そもそも switch が文じゃなくて式になっている言語というものもあります

例えば Scala という言語では switch に似た構文として match 式というものを用意してくれています

// Scala という言語のコードです

object Main extends App{
    val pet = "ポメラニアン"
    
    // match 式によって確定した「鳴き声」を cry 変数に代入する
    val cry = pet match {
        // pet が "ポメラニアン" なら、 "わんっ!" を返す
        case "ポメラニアン" => "わんっ!"
        case "ダックスフント" => "わんっ!"
        case "ヒマラヤン" => "にゃ〜ん"
    }
    
    // 今回の場合であれば "わんっ!" がログに出力される
    println(cry)
}

見慣れない記法かもしれませんが、なんとなくやりたいことは読み取れるかと思います
ちょっと 連想配列を使った分岐の例に似ている と思いませんか?
ペットの種類が key で鳴き声が value だと捉えると読みやすいかと思います

match 式の場合は下記のメリットが得られます

  • それぞれの条件に対して明示的に結果を記述しなければコンパイルエラーになってしまうため、 意図しないフォールスルーを防ぐ ことができます
  • 式であることから値を返す(上記で言えば「鳴き声」を返す)ため、 「振る舞いの選択」と「振る舞いの実行」を分ける ことができます

連想配列を使った例では インスタンス化することでメモリを食うデメリットがありましたが、このように式とする記法であればそのデメリットも合わせて解消できる ため、最近では switch 式はいろいろな言語で取り入れられ始めています

ちなみにフォールスルーのメリットであるグループ化をさせたいときは下記のように記述します

val cry = pet match {
    // '|' (パイプ) でつないで複数条件をグループとして扱うことも可能
    case "ポメラニアン" | "ダックスフント" => "わんっ!"
    case "ヒマラヤン" => "にゃ〜ん"
}

この場合も明示的に | で繋ぐ必要があるため、むしろ 意識してフォールスルーのメリットを使う ことができる仕様になっています

このようにモダンな言語にはバグの温床を防ぎつつ可読性の向上も図った仕様が取り込まれていたりするので、興味があれば調べてみてください