[オブジェクト指向]良いコード/悪いコードで学ぶ設計入門 6章 インターフェイスとアプリケーション設計

mino6_4プログラミング

どうも。つじけ(tsujikenzo)です。このシリーズでは、2022年5月に発売されました書籍「良いコード/悪いコードで学ぶ設計入門」についてお送りします。今日は4回目です。

前回のおさらい

前回は、「インターフェイスの概念」をお送りしました。

[オブジェクト指向]良いコード/悪いコードで学ぶ設計入門 6章 インターフェイスの概念
どうも。つじけ(tsujikenzo)です。このシリーズでは、2022年5月に発売されました書籍「良いコード/悪いコードで学ぶ設計入門」についてお送りします。今日は3回目です。前回のおさらい前回は、「switch文の重複と...

今回は、「インターフェイスとアプリケーション設計」をお届けします。

今日のアジェンダ

  • インターフェイス設計
  • 具象クラス
  • Map型による処理の切替
  • 値オブジェクト化

インターフェイス設計

これまでお伝えしてきたインターフェイスを用いて、魔法プログラムのswitch文の重複問題を解決してみましょう。

ソースコード内の不安定な条件分岐を、安定した設計にしよう、というお話です。 

メソッドの抽象化

各魔法クラスには、それぞれのメソッドが実装される予定です。

これらのメソッドから、共通したメソッドを探します。 

共通したメソッドを、抽象メソッドとして抽出します。 

インターフェイスの命名

おさらいですが、抽象メソッドを集めたものが、インターフェイスです。

インターフェイスの名前をつけるときは、抽象メソッドの集まりがなんの仲間であるか、を考えましょう。

今回の例でいうと「Magic(魔法)」が適切でしょう。

ソースコードを書きます。インターフェイスMagicを定義して、実装 {} をもたない抽象メソッドを定義しましょう。

interface Magic{
  String name(); //名前
  int costMagicPoint(); //消費魔法力
  int attackPower(); //攻撃力
  int costTechnicalPoint(); //消費テクニカルポイント	
}

具象クラス

それでは、具体的なクラスを書いていきましょう。

抽象メソッドの実装を、メソッドのオーバーライドを使って記述していきます。

追記ですが、ファイヤクラスでは、プレイヤーのレベルによって攻撃力を変化させたい、ということもあると思います。

今回は、コンストラクタでメンバークラスのインスタンスを受け取ることにしました。

/** Fireクラス */
class Fire implements Magic {
  private final Member member;

  Fire(final Member member) {
    this.member = member;
  }

  public String name() {
    return "ファイヤ";
  }

  public int costMagicPoint() {
    return 2;
  }

  public int attackPower() {
    return 20 + (int) (member.level * 0.5);
  }
  
  public int costTechnicalPoint() {
    return 0;
  }
}

これで、FireクラスやShidenクラスのインスタンスを、Magic型として扱うことができるので、if文による条件分岐をソースコードから排除できました。

/** Memberクラス */
//中略

/** 実行用エントリポイント */
class Sample6_31 {
  public static void main(String[] args) {
   Member member = new Member(100, 100, 10, 4, 5, 20);
  
   Magic magic = new Fire(member);
   System.out.println(magic.name()); // ファイヤ
   magic = new Shiden(member);
   System.out.println(magic.name()); // 紫電
  }
}

Map型による処理の切替

ただし、実際にプレイヤーが魔法を使うときは、switch文で魔法の切替が必要です。

/** MagicSwitchクラス */
class MagicSwitch {
private Member member;

  MagicSwitch(Member member) {
    this.member = member;
  }

//魔法を判定し、処理を切り替えるメソッド
void magic Attack(MagicType magicType) {
  switch (magicType) {
    case fire:
     Magic magic = new Fire(member);
     showMagicName(magic); 
     consumeMagicPoint(magic);
     consumeTechnicalPoint(magic);
     magicDamage(magic);
    break;
   case shiden:
    magic = new Shiden(member);
    showMagicName(magic);
    consumeMagicPoint(magic);
    consumeTechnicalPoint(magic);
    magicDamage(magic);
    break;
  case hellFire:
    magic = new HellFire(member);
    showMagicName(magic);
    consumeMagicPoint(magic);
    consumeTechnicalPoint(magic);
    magicDamage(magic);
    break;
  }
}

// 魔法の名前を画面表示する
void showMagicName(final Magic magic) {
  //中略
}

// 魔法を消費する
void consumeMagicPoint(final Magic magic) {
  //中略
}

// テクニカルポイントを消費する
void consumeTechnicalPoint(final Magic magic) {
  //中略
}

// ダメージ計算する
void magicDamage(final Magic magic) {
  //中略
  }
}

/** 実行用エントリポイント */
class Sample6_32_1 {
   public static void main(String[] args) {
    Member member = new Member(100, 100, 10, 4, 5, 20);
    MagicSwitch magicSwitch = new MagicSwitch(member);

  //ファイヤの攻撃
  magicSwitch.magicAttack(MagicType.fire);
  // ファイヤを使った, MPが2減った, テクニカルポイントが0減った, モンスターに25のダメージを与えた//紫電の攻撃
  magicSwitch.magicAttack(MagicType.shiden);
  // 紫電を使った, MPが7減った, テクニカルポイントが5減った, モンスターに56のダメージを与えた//地獄の業火の攻撃
  magicSwitch.magicAttack(MagicType.hellFire);
  // 地獄の業火を使った, MPが16減った, テクニカルポイントが24減った, モンスターに242のダメージを与えた
  }
}

このswitch文の切替に、Mapを使うとスッキリします。

/** MagicSwitchクラス */
class MagicSwitch {
  //中略
  final Map<MagicType, Magic> magics = new HashMap<>();

  // Mapを生成する
  void setMapElement() {
    final Fire fire = new Fire(member);
    final Shiden shiden = new Shiden(member);
    final HellFire hellFire = new HellFire(member);

    magics.put(MagicType.fire, fire);
    magics.put(MagicType.shiden, shiden);
    magics.put(MagicType.hellFire, hellFire);
}
    
 // 魔法攻撃を実行する
 void magicAttack(final MagicType magicType) {
   final Magic usingMagic = magics.get(magicType);
   usingMagic.attackPower();
  }
 //中略
}

class Sample6_32_2 {
  public static void main(String[] args) {
  Member member = new Member(100, 100, 10, 4, 5, 20);
  MagicSwitch magicSwitch = new MagicSwitch(member);

  // Mapを生成する
  magicSwitch.setMapElement();

  // 地獄の業火の攻撃
  magicSwitch.magicAttack(MagicType.hellFire);
  // 地獄の業火を使った, MPが16減った, テクニカルポイントが24減った, モンスターに242のダメージを与えた
  }
}

プログラム全体から、if文もswitch文も排除できました。どこに何が書かれているかわかりやすくなりました。

このように、インターフェイスをもちいて、処理をごっそり切り替える設計を、ストラテジーパターンと呼びます。

値オブジェクト化

最後に、プリミティブ型として扱っていた各クラスを、余計な値が混入しないように、値オブジェクト化します。

//↓↓↓ 修正前 ↓↓//
interface Magic {
  String name(); // 名前
  int costMagicPoint(); // 消費魔法力
  int attackPower(); // 攻撃力
  int costTechnicalPoint(); // 消費テクニカルポイント
}

//↓↓↓ 修正後 ↓↓//
interface Magic {
  String name(); // 名前
  MagicPoint costMagicPoint(); // 消費魔法力
  AttackPower attackPower(); // 攻撃力
  TechnicalPoint costTechnicalPoint(); // 消費テクニカルポイント
}

具象クラスで実装していたメソッドの戻り値を、インスタンスに変更します。

/** Fireクラス */
class Fire implements Magic {
  private final Member member;

  Fire(final Member member) {
   this.member = member;
  }

  public String name() {
   return"ファイヤ";
  }

  public MagicPoint costMagicPoint() {
    return new MagicPoint(2);
  }

  //中略
}

値オブジェクト達のクラスを定義します。

/** AttackPowerクラス */
class AttackPower {
  final int power;

  AttackPower(int power) {
    this.power = power;
   }
}

/** TechnicalPointクラス */
class TechnicalPoint {
 //中略
}

/** MagicPointクラス */
class MagicPoint {
 //中略
}

戻り値でint型を扱っていたメソッドが、値オブジェクトになり、堅牢化されました。最終系です。

minodriven-goodcodebadcode/Section6/Sample6_32_3.java at main · tsujike/minodriven-goodcodebadcode
ミノ駆動本のソースコードです。. Contribute to tsujike/minodriven-goodcodebadcode development by creating an account on GitHub.

JavaScriptでも書いてみましょう。

minodriven-goodcodebadcode/Section6/myFunction6_5.js at main · tsujike/minodriven-goodcodebadcode
ミノ駆動本のソースコードです。. Contribute to tsujike/minodriven-goodcodebadcode development by creating an account on GitHub.

どこに何が書いてあるのか、仕様変更の影響がどこまで及ぶのか、読みやすくなりましたね。

まとめ

以上で、「インターフェイスとアプリケーション設計」をお送りしました。

インターフェイスを使って、どのようにアプリケーションを設計するのか、順を追って解説してみました。

そして最後は、JavaScriptでも書いてみました。

Strategyパターンとなっており、仕様変更(魔法の追加や効能の変更)も楽に行えそうです。

次回は、最終回で 「インターフェイスの応用と実践」 をお届けします。

参考資料

このシリーズの目次

タイトルとURLをコピーしました