Chapter 10

第十章 ポリモーフィズムを効かせてリスト化させる

kuramapommel
kuramapommel
2021.05.07に更新

異なる具象クラスのインスタンスを抽象型でひとまとめにする

いきなりですが、 6 章に 出てきた Hunter クラスに Weapon を持たせてみましょう

class Hunter {
  constructor(
    private hp: number,
    // weapon をメンバ変数として保持
    public readonly weapon: Weapon
    ) {}
  
  public damaged(power: number): void {
    this.hp = this.hp - power
  }
  
  public getCurrentHp(): number {
    return this.hp
  }
    
  public attack(): void {
    // メンバ変数の weapon を使う
    this.weapon.attack()
  }
}

こんな感じになるでしょうか

ここで、ある日突然「ハンターは複数種類の武器で攻撃するようにしてほしい」みたいな仕様変更が入ったとします
そういった仰天仕様変更は、まあ往々にありますね

複数のものを同時に管理するとき 配列( array というものが使えるのはご存知でしょうか
配列とは n 個の同一の型のデータを一列に並べて纏めて扱えるようにしたもの のことを呼びます
配列内のそれぞれのデータを 要素( element と呼ぶことが多いのでこれも覚えておきましょう

例えば、 1 ~ 10 までのデータをもつ配列は下記のように表します

// 型名の右に `[]` を添えることで配列で有ることを表す
const array: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

この array 配列の任意の要素を指定するには「 array 配列の 0 番目の要素は 1 , 9 番目の要素は 10 , 10 番目の要素は範囲外」というように「 n 番目の要素」という表現を用いるのですが、この「 n 番目」のことを 添字( index と呼びます

なんか突然色々覚える言葉が出てきちゃったと思うんですけど、この辺の言葉は日常会話で当たり前のように使われるので、息巻いて覚えようとしなくても自然と身に馴染んでくると思いますのでご安心ください

さて、上の array 配列は number 型の配列でしたが、では 「複数種類の武器」を配列を用いて表す ためにはどのようにしたら良いでしょうか
重要なのは「同一の型のデータ」でしたね、「太刀、双剣、弓」を同一の型として表すためにはやはり Weapon 型でしょう
ということで Weapon 型の配列である Weapon[] を使ってあげる と良さそうです

Hunter クラスを修正してみましょう

class Hunter {
  constructor(
    private hp: number,
    // Weapon から Weapon[] に変更、伴って変数名も複数形の weapons に変更
    public readonly weapons: Weapon[]
    ) {}
  
  public damaged(power: number): void {
    this.hp = this.hp - power
  }
  
  public getCurrentHp(): number {
    return this.hp
  }
    
  public attack(): void {
    // コンパイルエラーになってしまうので一時的にコメントアウト
    //this.weapon.attack()
  }
}

このように WeaponWeapon[] としてあげることで、複数の武器を持てるようになりました
weapons 配列の中には何の武器がいくつ入っているかはわからないですが、このハンターがいくつかの武器を抱え持っていることはイメージできるでしょう

では、この Hunter クラスをインスタンス化してみましょう
constructorweapons を受け取っているため、インスタンス化するときに武器の配列を引数に渡して上げる必要があります

// 武器の配列を定義
// ポリモーフィズムを効かせて、異なる具象クラスのインスタンスを Weapon としてひとまとめにして使うことで実現
const weapons: Weapon[] = [
    new Bow(),
    new LongSword()
]

const hunter = new Hunter(100, weapons)

上記は試しに「弓」と「太刀」をハンターに持たせてみたコードです
このように ポリモーフィズムを効かせることで異なる武器の種類も一括して「武器」としてひとまとめに扱うことができる ようになります

ループを使って複数の武器を使う

Hunter に複数武器を持たせることには成功しましたが、一時的に attack メソッド内をコメントアウトすることになってしまいました
attack メソッド内では weapons で抱えている武器たちの attack メソッドを順に呼んであげたいです

どうしたら良いでしょう

こういうときのために、だいたいのプログラミング言語には「ループ・繰り返し」を実現するための構文として for と呼ばれるものが用意されています
配列と for 文を組み合わせることで、配列内の要素を順番に処理することができるようになります

下記コードをご確認ください

class Hunter {
  // ...他の箇所は省略
    
  public attack(): void {
    // weapons の要素を先頭から順番に weapon に代入し、処理する構文
    // 最後の要素の処理が終わるまで for { } 内をループする
    for (const weapon of this.weapons) {
      // 具体的にはどの武器の attack メソッドかはわからないが、ポリモーフィズムが聞いているため weapon.attack() として呼ぶことができる
      weapon.attack()
    }
  }
}

上記は JavaScriptfor 文の一種である for...of を使用した記述です
Java なら拡張 for 文、 C# なら foreach のように各言語に似た構文が用意されていると思いますので、ご自身でお使いの言語でも調べてみてください
たぶん、「言語名 foreach 」とかで検索すると出てくると思います

for を用いることで weapons の要素の attack メソッドを順番に処理することができるようになりました

ポリモーフィズムと配列を利用した管理は以下のような場面で応用できます

  • 「平社員クラス」, 「部長クラス」, 「事業部長クラス」などに「社員 interface 」を実装させて 社員配列 として管理する
  • 「かいふくのくすりクラス」, 「ふしぎなアメクラス」, 「むしよけスプレークラス」などに「どうぐ interface 」を実装させて どうぐリスト として管理する
  • 「おにぎりクラス」, 「お弁当クラス」, 「清涼飲料水クラス」などに「商品 interface 」を実装させて 商品一覧 として管理する

便利なのでぜひ使ってみてください

[余談] Array 型について ~型引数の話にちょっと触れるよ~

TypeScript の配列は [] で表現できますが、実は下記のように Array を使っても表現することができます

// Array<Weapon> で受けることもできるし
const weapons1: Array<Weapon> = new Array<Weapon>(
    new Bow(),
    new LongSword()
)

// Weapon[] で受けることもできる
const weapons2: Weapon[] = new Array<Weapon>(
    new Bow(),
    new LongSword()
)

とは言え [] を使って初期化するのと比較してあまりメリットがないので、一般的には [] を使った初期化が使われます
「こんな書き方もできますよ」な紹介なんですけど、ここで注目したいのが <Weapon> の部分です

これは 型引数( Generics ) と呼ばれる機能です

どんな機能かというと、 メソッドやクラスが使う任意の値の型を定義時は不定にしておいて、呼び出し側が指定できる機能 です
言葉で伝えるのが難しいのでサンプルを載せます

// class 宣言時に item メンバ変数の型を「不定」にしておく
// ちなみに `T` は `Type` の頭文字を使う慣習があるだけで、
// 実際は `A` でも `B` でも `Type` でも ok です
class Box<T> {
  constructor(
    // item の型は「不定」のため `class Box<T>` の `T` を使う
    public readonly item: T
  )
}

// 使う側が、 item の型に Weapon を指定
const weaponBox: Box<Weapon> = new Box<Weapon>(new Bow())
// 使う側が、 item の型に number を指定
const numberBox: Box<number> = new Box<number>(1)

// weaponBox.item の型は Weapon 、 Weapon は name プロパティを持っているのでコンパイルが通る
console.log(weaponBox.item.name)
// numberBox.item の型は number 、 number は name プロパティを持っていないのでコンパイルエラーになる
console.log(numberBox.item.name)

ちなみに Box<T> の読み方は Box of T と覚えておくと理解しやすいです
Box<Weapon> であれば「 Box of Weapon 」つまり「 Weapon 型の Box クラス」みたいな感じですね

実はこれも パラメータ多相 と呼ばれるポリモーフィズムの一種なのですが、今はちょっと難しいと思うので、 「なるほど、後から型を指定できる機能があるのか〜」 くらいに理解しておくと良いでしょう
もしかしたら後々この辺にも触れるかもしれませんし、触れないかもしれませんし、その辺はぼくの気力次第ということで今は許してください

ただ、今は「作り方」を覚えておく必要はないですが、「使い方」は覚えておくと良いと思います
というのも、型付けの言語だと標準ライブラリでしばしば登場してくるからですね

TypeScript で代表的なところだと Promise とかでしょうか
Java をやったことがある方でしたら、もしかしたら List あたりを使ったことがあるかもしれないですね

もちろん他の言語にも用意されていて、例えば C++ の Template とか C# のジェネリック とかも同じ概念です