どうも。つじけ(tsujikenzo)です。このシリーズでは、2022年5月に発売されました書籍「良いコード/悪いコードで学ぶ設計入門」についてお送りします。今日は4回目です。
前回のおさらい
前回は、「インターフェイスの概念」をお送りしました。
今回は、「インターフェイスとアプリケーション設計」をお届けします。
今日のアジェンダ
- インターフェイス設計
- 具象クラス
- 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型を扱っていたメソッドが、値オブジェクトになり、堅牢化されました。最終系です。
JavaScriptでも書いてみましょう。
どこに何が書いてあるのか、仕様変更の影響がどこまで及ぶのか、読みやすくなりましたね。
まとめ
以上で、「インターフェイスとアプリケーション設計」をお送りしました。
インターフェイスを使って、どのようにアプリケーションを設計するのか、順を追って解説してみました。
そして最後は、JavaScriptでも書いてみました。
Strategyパターンとなっており、仕様変更(魔法の追加や効能の変更)も楽に行えそうです。
次回は、最終回で 「インターフェイスの応用と実践」 をお届けします。