57
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

バハムート駆動開発をやってください

Last updated at Posted at 2025-12-09

クラスを、オブジェクトを、なんとなく理解したと思えたとき、クラスが召喚石で、オブジェクトがバハムートで、newがバハムート呼び出すことで、メソッドがバハムートのメガフレアみたいなもんかなと思いました。
それを例として後輩に教えていたら、一部の人にツボったみたいで、普段ほぼ聞くことがないであろう「バハムート」という単語が、社内でたまにちらりと聞くようになりました。バハムート知らない人向けに、ピカチュウ使って説明していたりもします。

そこで、良くあるプログラミングの概念を、実際にバハムートつくりながらの物語風に例えてみました。

  • バハムートに特化しているので、プログラムの処理でベストプラクティスでない場合があります。正しくは、書籍・記事・AIで確認してください

  • 今年の流行語大賞、「長袖をください」が選ばれてほしかったです。やくみつるさんは、『長袖をください』を強く推したらしいですが、残念ながら他の方に全く響いてなかったみたいなので、タイトルで利用させてもらいます

  • 何回も見直してますが、ディレクトリの構成図がちょっと文脈とあってなかったりしてます、ごめんなさい。見つけ次第、随時直していきます


1章:クラス + TDD(Bahamut & Shiva)

物語:FF作ることになったので、まず召喚獣

来年度のロードマップで決まったこと:

「FFの召喚獣を PHP で実装した商品を作りたい。とりあえず代表的な召喚獣から頼む」

テックリード
「まずは新人二人にバハムートとシヴァを作ってもらおう。テストコードも書きながらやってね」

新人 A(バハムート担当)と、新人 B(シヴァ担当)は、それぞれ張り切ってクラスを作り始めた。

しかし新人 A がちょっとしたミス。

新人 A
「先輩、やっちゃいました…。攻撃メソッド名をcastMegaFlareじゃなくてcastSumon で書いてましたけど、そのまま進めちゃって…」

テックリード
「テストコード書きながらやってたから、テストコードが落ちて気づけてよかったね。これからはテストを回しながらやるようにしてね」

PHPコード例

<?php
// src/Bahamut.php
class Bahamut {
    public int $mp = 500;

    public function castMegaFlare(): string {
        return "メガフレア!";
    }
}

// src/Shiva.php
class Shiva {
    public int $mp = 300;

    public function castDiamondDust(): string {
        return "ダイヤモンドダスト!";
    }
}

// tests/SummonTest.php
use PHPUnit\Framework\TestCase;

class SummonTest extends TestCase {
    public function testBahamutCast() {
        $beast = new Bahamut();
        $this->assertEquals("メガフレア!", $beast->castMegaFlare());
    }

    public function testShivaCast() {
        $beast = new Shiva();
        $this->assertEquals("ダイヤモンドダスト!", $beast->castDiamondDust());
    }
}

ASCIIクラス図

+-----------------------------+
|          Bahamut            |
+-----------------------------+
| - mp: int                   |
+-----------------------------+
| + castMegaFlare(): string   |
+-----------------------------+

+-----------------------------+
|           Shiva             |
+-----------------------------+
| - mp: int                   |
+-----------------------------+
| + castDiamondDust(): string |
+-----------------------------+

ディレクトリ構成

src/
├── Bahamut.php
└── Shiva.php
tests/
└── SummonTest.php

2章:新人の「メソッド名バラバラ問題」→ インターフェイス導入へ

物語:攻撃メソッドを統一

数日後の定例開発ミーティング。

新人 A
「バハムートの攻撃は castMegaFlare() でいいですよね!」

新人 B
「シヴァは castDiamondDust() にしました! でも氷属性だし doIceAttack() のほうが良かったかなって…」

テックリード
「攻撃名バラバラじゃん、インターフェイスでルール化しちゃって」

PHPコード例

<?php
interface SummonBeast {
    public function castSummon(): string;
}

class Bahamut implements SummonBeast {
    public int $mp = 500;

    public function castSummon(): string {
        return "メガフレア!";
    }
}

class Shiva implements SummonBeast {
    public int $mp = 300;

    public function castSummon(): string {
        return "ダイヤモンドダスト!";
    }
}

// tests/SummonTest.php
class SummonTest extends TestCase {
    public function testBahamutCast() {
        $beast = new Bahamut();
        $this->assertEquals("メガフレア!", $beast->castSummon());
    }

    public function testShivaCast() {
        $beast = new Shiva();
        $this->assertEquals("ダイヤモンドダスト!", $beast->castSummon());
    }
}

ASCIIクラス図

+----------------------------------------------+
            |      SummonBeast       |
            +------------------------+
            | + castSummon(): string |
            +------------------------+
                    ▲       ▲
                    |       |
          +---------+       +---------+
          |                           |
+--------------------+   +--------------------+
|      Bahamut       |   |       Shiva        |
+--------------------+   +--------------------+
| - mp: int          |   | - mp: int          |
+--------------------+   +--------------------+
| + castSummon()     |   | + castSummon()     |
+--------------------+   +--------------------+

ディレクトリ構成

src/
├── SummonBeast.php
├── Bahamut.php
└── Shiva.php
tests/
└── SummonTest.php

3章:新人の「ゼロ系作るとコピペ地獄」問題 → 継承へ

物語:コードがコピペ地獄化して新人が悲鳴を上げる

開発会議で突然、
「既存召喚獣の強化バージョン(上位種)を追加してほしい。名前は◯◯ゼロで統一」
と決まり、バハムートゼロ・シヴァゼロを作ることになった。

※つまり「元の召喚獣を“ちょっと強くした版”を作る」=ゼロ系。

新人 A
「先輩、ゼロ系ってつまり上位種ですよね?
バハムートゼロを作ろうとしたんですけど、mp や技名が少し違うだけで、ほぼ全部コピペになっちゃうんですよね……。“これ本当に正しいのかな…”って不安になってきて……」

新人 B
「私もシヴァゼロを作ると同じ状況で…コピペでいいのかなって不安に…」

テックリード
「そういう、これやばいかなっていうのって“コード臭(code smell)”というらしいよ。元のバハムート/シヴァを親クラスにして、ゼロ系は継承するようにしておいて」

PHPコード例

<?php
class BahamutZero extends Bahamut {
    public int $mp = 600;

    public function castSummon(): string {
        return "メガフレア!";
    }
}

class ShivaZero extends Shiva {
    public int $mp = 350;

    public function castSummon(): string {
        return "ダイヤモンドダスト!";
    }
}

// テストも追加
class SummonTest extends TestCase {
    public function testBahamutZeroCast() {
        $beast = new BahamutZero();
        $this->assertEquals("メガフレア!", $beast->castSummon());
    }

    public function testShivaZeroCast() {
        $beast = new ShivaZero();
        $this->assertEquals("ダイヤモンドダスト!", $beast->castSummon());
    }
}

ASCIIクラス図

Bahamut
   |
   +--> BahamutZero

Shiva
   |
   +--> ShivaZero

ディレクトリ構成

src/
├── Bahamut.php
├── BahamutZero.php
├── Shiva.php
└── ShivaZero.php
tests/
└── SummonTest.php

4章:登場アニメーションを共通化したい → トレイト導入

物語:新人が「アニメーションどう共通化?」と詰まる

デザイナー
「召喚シーンの演出、全召喚獣で同じルールにしたいんです。バラバラだと見た目が不揃いになるので…」

新人 B
「え、でも親クラスに書くと、普通のバハムートやシヴァまで影響しますよね…?」

新人 A
「共通クラスにしてしまうと、ゼロ系だけじゃなくて全部に影響するし…あれ、PHPって多重継承できないんでしたっけ?」

テックリード
「たしかデザインチームでバハムートのアニメーション動作作っていた気がするから、トレイトで使っちゃって」

PHPコード例

<?php
trait SummonAnimation {
    public function playAnimation(): string {
        return "Displaying summon animation...";
    }
}

class BahamutZero extends Bahamut {
    use SummonAnimation;

    public function castSummon(): string {
        return "メガフレア!";
    }
}

class ShivaZero extends Shiva {
    use SummonAnimation;

    public function castSummon(): string {
        return "ダイヤモンドダスト!";
    }
}

ASCIIクラス図

BahamutZero
   |
   +--> uses SummonAnimation

ShivaZero
   |
   +--> uses SummonAnimation

ディレクトリ構成

src/
├── Traits/
│   └── SummonAnimation.php
├── Bahamut.php
├── BahamutZero.php
├── Shiva.php
└── ShivaZero.php

5章:新人が「クラスが太ってきました…」と限界を感じる → SRP(単一責任原則) へ

物語:新人が泣きそうになる

新人 B
「先輩……ゼロ系のクラスに攻撃、アニメーション、MP管理、計算ロジック、データ保存ロジック……全部入れ始めたらめっちゃ膨らんできました……」

新人 A
「ごちゃごちゃしてどこを触ればいいのか分からなくなってます……」

テックリード
「こういうごついクラス、“God Class(神クラス)”っていうらしいよ。出来る限り細かく、1つのクラスに1つの役割ぐらいに、できる限り細かく分割してしまって。それとMPとかは簡単に変えられないようにprivateとかにしちゃって」

新人 A
「なるほど……じゃあ、getter を通して値を取得する形にするんですね?」

テックリード
「そうそう。読み取りはできるけど、変更はクラス中だけで行って外部から簡単に変えられない感じに。」


  • このRepositoryは分離しただけ、DDDのRepositoryではありません

PHPコード例

<?php
// SummonAnimation は SRP のために分離しただけ
trait SummonAnimation {
    public function playAnimation(): string {
        return "アニメーション再生中…";
    }
}

// インターフェイス:この時点ではシンプル
interface SummonBeast {
    public function castSummon(): string;
    public function mp(): int;
}

// BahamutZero
class BahamutZero implements SummonBeast {
    use SummonAnimation;

    private int $mp = 600;

    public function castSummon(): string {
        return "メガフレア!";
    }

    public function mp(): int {
        return $this->mp;
    }
}

// ShivaZero
class ShivaZero implements SummonBeast {
    use SummonAnimation;

    private int $mp = 350;

    public function castSummon(): string {
        return "ダイヤモンドダスト!";
    }

    public function mp(): int {
        return $this->mp;
    }
}

// Repository(SRP の一例として分離しただけ)
class SummonRepository {
    private array $storage = [];

    public function save(SummonBeast $beast): void {
        $this->storage[] = $beast;
    }

    public function all(): array {
        return $this->storage;
    }
}

// 計算サービス(SRP の一例として分離しただけ)
class SummonService {
    public function calculateTotalDamage(array $summons): int {
        $total = 0;
        foreach ($summons as $beast) {
            $total += ($beast instanceof BahamutZero) ? 1000 : 500;
        }
        return $total;
    }
}

ASCIIクラス図

SummonBeast (interface)
   + castSummon()
   + mp()

BahamutZero
   + uses SummonAnimation
   - mp: int
   + mp()

ShivaZero
   + uses SummonAnimation
   - mp: int
   + mp()

SummonRepository
   - storage: array
   + save()
   + all()

SummonService
   + calculateTotalDamage()


ディレクトリ構成(5章時点)

src/
├── Traits/
│   └── SummonAnimation.php
│
├── Repository/
│   └── SummonDataRepository.php      ← SRP用のデータ取得
│
├── Service/
│   └── SummonDamageCalculator.php    ← SRP用の計算クラス
│
├── Bahamut.php
├── BahamutZero.php
├── Shiva.php
└── ShivaZero.php


6章:新人が「new が多くて混乱」→ Factory 導入へ

物語:頻繁な new で新人が迷子

新人 A
「先輩、画面側やテスト側で new Bahamut() とか new ShivaZero() とか、同じクラスを何度も書かないといけません…」

新人 B
「種類が増えるほど、毎回if文でどの召喚獣か分岐しないといけないですし、しかも画面側とテスト側で同じコードを書くのも大変です…」

テックリード
「Factoryっていう概念があるから、それを使ってみて。」

PHPコード例:Factory 定義

<?php
class SummonFactory {
    public static function create(string $name): SummonBeast {
        return match($name) {
            'Bahamut' => new Bahamut(),
            'Shiva' => new Shiva(),
            'BahamutZero' => new BahamutZero(),
            'ShivaZero' => new ShivaZero(),
            default => throw new Exception("Unknown summon: $name")
        };
    }
}

PHPコード例:Factoryを使った呼び出し

<?php
// 以前はこんなふうに個別に new していた
$bahamut = new Bahamut();
$shivaZero = new ShivaZero();

// Factory を使うと…
$bahamut = SummonFactory::create('Bahamut');
$shivaZero = SummonFactory::create('ShivaZero');

ASCIIクラス図

  • 下記追加クラス以外、5章と同じ
SummonFactory
   + create(name): SummonBeast

ディレクトリ構成

src/
├── Factory/
│   └── SummonFactory.php   ← 新登場
├── Traits/
├── Repository/
├── Service/
├── SummonBeast.php
├── BahamutZero.php
└── ShivaZero.php


7章:バハムートがダイヤモンドダストを撃った日 → 属性(Element)の Value Object 化

物語:属性を文字列で持つのって怖いと思っていた矢先からの悲劇

午後 3 時。
社内の QA チャンネルが突然ざわつき始めた。

QA「すいません、なんかバハムートなのに、ダイヤモンドダストを撃ってます」

新人 A(バハムート担当)
「えっ!? そんなわけ……いや、うそでしょ……?」

新人 B(シヴァ担当)
「まさかシヴァの技データを間違って流し込んだとか……?」

焦りながらログを追っていくと、バハムートが堂々とこう宣言していた。

Bahamut used DiamondDust!

新人 A
「うわぁぁぁぁぁ!
なんでバハムートがダイヤモンドダスト撃ってんの!!?
バグり方が派手すぎる!!
うちの象徴的な召喚獣が氷の女王みたいになってるんですが!?」

テックリード
「原因は、これじゃない?」

$element = "Ice";  // ← 誰かのタイプミス or マージ衝突で混入

新人 B
「あっ……属性が文字列だから……
"Flare""Ice" が“同じただの文字列”として扱われちゃうんですね……」

新人 A
「つまり……
バハムートの属性が文字列一発で“氷属性”に変身したってことか……!」

テックリード
「もう、この値だけっていうのを専用のクラスを用意しちゃって。エレメントとかValueObjectって調べると出てくるから。」


PHPコード例:Element(ValueObject)追加

<<?php
// src/SummonBeast.php

interface SummonBeast
{
    public function castSummon(): string;
    public function mp(): int;
    public function element(): Element;
}

PHPコード例:Bahamut に Element を組み込む(Flare)

<?php
// src/Bahamut.php

require_once __DIR__ . '/ValueObject/Element.php';

class Bahamut implements SummonBeast
{
    use SummonAnimation;

    private int $mp = 500;
    private Element $element;

    public function __construct()
    {
        $this->element = new Element("Flare");
    }

    public function castSummon(): string
    {
        return "メガフレア!";
    }

    public function mp(): int
    {
        return $this->mp;
    }

    public function element(): Element
    {
        return $this->element;
    }
}

PHPコード例:Shiva に Element を組み込む(Ice)

<?php
// src/Shiva.php

require_once __DIR__ . '/ValueObject/Element.php';

class Shiva implements SummonBeast
{
    use SummonAnimation;

    private int $mp = 300;
    private Element $element;

    public function __construct()
    {
        $this->element = new Element("Ice");
    }

    public function castSummon(): string
    {
        return "ダイヤモンドダスト!";
    }

    public function mp(): int
    {
        return $this->mp;
    }

    public function element(): Element
    {
        return $this->element;
    }
}

ASCIIクラス図

+----------------------+
|     SummonBeast      |  (interface)
+----------------------+
| + castSummon():str   |
| + mp():int           |
| + element():Element  |
+----------------------+
           ▲
           |
+----------------------+
|       Bahamut        |
+----------------------+
| - mp: int            |
| - element: Element   |
+----------------------+
| + castSummon()       |
| + mp()               |
| + element()          |
+----------------------+

+----------------------+
|        Shiva         |
+----------------------+
| - mp: int            |
| - element: Element   |
+----------------------+
| + castSummon()       |
| + mp()               |
| + element()          |
+----------------------+

+----------------------+
|       Element        |  (ValueObject)
+----------------------+
| - value: string      |
+----------------------+
| + value(): string    |
| + equals(Element)    |
+----------------------+


ディレクトリ構成

src/
├── Factory/
│   └── SummonFactory.php
│
├── Repository/
│   └── SummonRepository.php     ← 5章・6章のまま
│
├── Service/
│   └── SummonService.php        ← 5章の計算クラス
│
├── Traits/
│   └── SummonAnimation.php
│
├── ValueObject/
│   └── Element.php              ← 7章で追加
│
├── SummonBeast.php
├── Bahamut.php
└── Shiva.php


8章:図鑑を押したらメガフレア → エンティティ化の第一歩

物語:バハムートが増殖していた

夕方の開発フロア。
新人 A は「召喚獣図鑑」の UI をのんびりチェックしていた。

新人 A
「よし……図鑑でバハムートを選んだら、
攻撃力とか説明文のパネルが開くだけ……のはず……」

Bahamut casts MEGAF★E!!! 

新人 A
「なんで図鑑でメガフレア撃つの!?!?
説明文見るだけの画面だよね!?!?」

新人 B(隣の席)
「ちょっと待って、図鑑って“戦闘じゃない”よね!?」

新人 A
「戦闘じゃないし!
ポップでかわいい“info”ボタンしかないし!!
なんでバハムートが画面ぶち破るの!!?」

テックリード(近づいてくる)
「どんな操作したの?」

新人 A
「バハムートの“図鑑用データ”を表示しただけなんです!
ただの new Bahamut() のつもりだったんです!」

テックリード
「……その new Bahamut()
もしかして 戦闘用の実体と同一扱い されてる?」

新人 A
「……っ!!……ああああああ!!
図鑑用のバハムートと、
プレイヤーがさっき育成してた 戦闘バハムートのインスタンスが同一視されてる!!

新人 B
「だから図鑑で性能見ようとしただけなのに、
“本物のバハムート” が呼ばれて技を撃った……?」

テックリード
「図鑑バハムートと戦闘バハムートを区別する基準が無いから。図鑑とか戦闘のそれぞれのバハムートにID持たせてみて。」

PHPコード例:召喚獣用 ID クラス

<?php
namespace App\Domain\Summon;

class SummonId
{
    private string $value;

    public function __construct(?string $value = null)
    {
        // nullなら自動生成
        $this->value = $value ?? uniqid('summon_', true);
    }

    public function value(): string
    {
        return $this->value;
    }

    public function equals(SummonId $other): bool
    {
        return $this->value === $other->value();
    }
}

PHPコード例:識別子を持つ召喚獣クラス

<?php
namespace App\Domain\Summon;

class Bahamut implements SummonBeast
{
    private SummonId $id;
    private SummonName $name;
    private Element $element;

    public function __construct(?SummonId $id = null)
    {
        $this->id      = $id ?? new SummonId();
        $this->name    = new SummonName("Bahamut");
        $this->element = new Element("Flare");
    }

    public function id(): SummonId
    {
        return $this->id;
    }

    public function element(): Element
    {
        return $this->element;
    }

    public function castSummon(): string
    {
        return "メガフレア!";
    }

    public function name(): SummonName
    {
        return $this->name;
    }
}

PHPコード例:召喚獣バッグ(PlayerSummons)の更新

<?php
class PlayerSummons
{
    private array $list = [];

    public function add(SummonBeast $summon): void
    {
        $this->list[$summon->id()->value()] = $summon;
    }

    public function all(): array
    {
        return array_values($this->list);
    }
}

ASCIIクラス図

+----------------------------+
|         SummonId           |
+----------------------------+
| - value: string            |
+----------------------------+
| + value()                  |
| + equals()                 |
+----------------------------+

+----------------------------+
|        SummonBeast         | <<interface>>
+----------------------------+
| + id(): SummonId           |
| + element(): Element       |
| + castSummon(): string     |
+----------------------------+
              ▲
              |
+----------------------------+
|          Bahamut           |
+----------------------------+
| - id: SummonId             |
| - name: SummonName         |
| - element: Element         |
+----------------------------+
| + id(): SummonId           |
| + element(): Element       |
| + castSummon(): string     |
+----------------------------+

+----------------------------+
|       PlayerSummons        |
+----------------------------+
| - list: array              |
+----------------------------+
| + add()                    |
| + all()                    |
+----------------------------+

ディレクトリ構成

src/
└── Domain/
    └── Summon/
        ├── Bahamut.php
        ├── SummonBeast.php
        ├── SummonName.php
        ├── SummonId.php        ← 8章で新登場
        ├── Element.php
        └── Shiva.php           

    └── Player/
        └── PlayerSummons.php   ← 8章で新登場

tests/
└── SummonTest.php

9章:どのクラスに実装すれば良いかわからない処理が現れた → Domain Service の自然発生

物語:属性相性の計算、どこに置く?

ある日の午後、新人 B が深刻そうに相談してきた。

新人 B
「召喚獣の“相性ダメージ”を実装しようとしたんですが……」

新人 A
「氷には強くなるみたいな、属性によってダメージ増減するやつですよね?」

新人 B

class Bahamut
{
    public function calcDamageAgainst(Shiva $enemy): int
    {
        if ($this->element->value() === "Flare" && $enemy->element()->value() === "Ice") {
            return 200; // 強い
        }
        return 100; // 通常
    }
}

新人 A
「Bahamut に Shiva を定義するんですか?
他の召喚獣が増えたらコード膨らみますよね…」

新人 B
「そうなんです。Bahamut に書くのは違う気がして…」

テックリード
「そうなんだ、召喚獣に戦闘ルールのロジック入れたくないから、戦闘用のロジック作っちゃって」


PHPコード例:SummonBeast インターフェイスの拡張

<?php
namespace App\Domain\Summon;

interface SummonBeast
{
    public function id(): SummonId;
    public function element(): Element;  // ← 属性取得を外部から保証
    public function castSummon(): string;
}

PHPコード例:Domain Service:属性相性判定専用クラス

<?php
namespace App\Domain\Battle;

use App\Domain\Summon\SummonBeast;

class ElementCompatibilityCalculator
{
    public function calcDamage(SummonBeast $attacker, SummonBeast $defender): int
    {
        $a = $attacker->element()->value();
        $d = $defender->element()->value();

        if ($a === "Flare" && $d === "Ice") {
            return 200;
        }

        return 100;
    }
}

PHPコード例:呼び出し方

$calc = new ElementCompatibilityCalculator();
$damage = $calc->calcDamage($bahamut, $shiva);

ASCIIクラス図

+----------------------------------+
| ElementCompatibilityCalculator   |
+----------------------------------+
| + calcDamage(att, def): int      |
+----------------------------------+
              |
              | uses
              ▼
+-------------------------------+
|         SummonBeast           | <<interface>>
+-------------------------------+
| + id(): SummonId              |
| + element(): Element          |
| + castSummon(): string        |
+-------------------------------+
              ▲
              | implements
              |
    ┌─────────┴─────────┐
    |                   |
+-------------------+   +-------------------+
|     Bahamut       |   |      Shiva        |
+-------------------+   +-------------------+
| - id: SummonId    |   | - id: SummonId    |
| - name: SummonName|   | - name: SummonName|
| - element: Element|   | - element: Element|
+-------------------+   +-------------------+
| + id()            |   | + id()            |
| + element()       |   | + element()       |
| + castSummon()    |   | + castSummon()    |
+-------------------+   +-------------------+

ディレクトリ構成

src/
└── Domain/
    ├── Summon/
    │   ├── Bahamut.php
    │   ├── Shiva.php
    │   ├── SummonBeast.php
    │   ├── SummonName.php
    │   ├── SummonId.php
    │   └── Element.php
    │
    ├── Player/
    │   └── PlayerSummons.php
    │
    └── Battle/
        └── ElementCompatibilityCalculator.php

10章:保存したら世界が全部バハムートになった → Repository の誕生

物語:店も村人もバハムート、世界がすべてバハムート

新人 A
「先輩……た、大変です……」

テックリード
「また何か壊した?」

新人 A
「バハムートを“保存”したら……
世界中の人と町と召喚獣が全部バハムートになりました。

新人 B
「なんで保存でそんな現象が!?」

新人 A
「保存場所がなくて……とりあえず
GlobalStorage::$data に丸ごとぶっこんだら……
全部の参照がバハムートに上書きされて……」

テックリード
「つまり、世界共通の状態に“バハムートの情報”を流し込んだのか……
そりゃ村人も店もモンスターもバハムートになるわ。」

新人 B
「広場に 40 体ぐらい、日常生活してるバハムート見ましたよ……」

新人 A
「パン屋で『焼き立ての炎属性だ(咆哮)』って言われました……」

テックリード
「もう保存ロジックを召喚獣に持たせるのは危険すぎる。召喚獣だけを保存する専用の処理をつっくってしまって。」

PHPコード例:召喚獣保存の“窓口クラス”

<?php
namespace App\Domain\SummonRepository;

interface SummonRepository
{
    public function save(SummonBeast $summon): void;

    public function findById(SummonId $id): ?SummonBeast;
}

PHPコード例:保存版の実装例(Infrastructure に置く)

<?php
namespace App\Infrastructure\SummonRepository;

use PDO;
use App\Domain\Summon\{SummonBeast, SummonId};
use App\Domain\SummonRepository\SummonRepository;

class FileSummonRepository implements SummonRepository
{
    /** @var PDO */
    private PDO $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    public function save(SummonBeast $summon): void
    {
        $sql = "
            INSERT INTO summons (id, name, level)
            VALUES (:id, :name, :level)
            ON CONFLICT(id) DO UPDATE SET
                name = excluded.name,
                level = excluded.level
        ";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            ':id'    => $summon->id()->value(),
            ':name'  => $summon->name()->value(),
            ':level' => $summon->level()->value(),
        ]);
    }

    public function findById(SummonId $id): ?SummonBeast
    {
        $sql = "SELECT id, name, level FROM summons WHERE id = :id";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':id' => $id->value()]);

        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return null;
        }

        // 本来は SummonBeastFactory で分岐するべきだが、記事簡略版
        switch ($row['name']) {
            case 'Bahamut':
                return new \App\Domain\Summon\Bahamut(new SummonId($row['id']));
            case 'Shiva':
                return new \App\Domain\Summon\Shiva(new SummonId($row['id']));
            default:
                return null;
        }
    }
}

  • 使用例
$repo = new FileSummonRepository();

$repo->save($bahamut);

$restored = $repo->findById($bahamut->id());

ASCIIクラス図

+-------------------------+
|    SummonRepository     |<<interface>>
+-------------------------+
| + save(summon)          |
| + findById(id)          |
+-------------------------+

                     ▲
                     |
+-----------------------------------+
|      FileSummonRepository         |
+-----------------------------------+
| - path: string                    |
+-----------------------------------+
| + save()                          |
| + findById()                      |
+-----------------------------------+

ディレクトリ構成

src/
└── Domain/
    ├── Summon/
    │   ├── Bahamut.php
    │   ├── Shiva.php
    │   ├── SummonBeast.php
    │   ├── SummonName.php
    │   ├── SummonId.php
    │   ├── Level.php
    │   └── Element.php
    │
    ├── Player/
    │   └── PlayerSummons.php
    │
    └── SummonRepository/
        └── SummonRepository.php

└── Infrastructure/
    └── SummonRepository/
        └── FileSummonRepository.php

11章:バハムートだけではなく、宿屋のオヤジまでレベルアップ → 境界づけられたコンテキストの自然発生

物語:世界、混ぜすぎ警報発令!

開発が進み、召喚獣、バトル、プレイヤー、保存処理……
全部ひとつのフォルダに詰め込んでいたある日のこと。

新人 B
「先輩、今ちょっと笑えないことになってるんですが……壊れたというか……“世界が混ざった”というか……」

新人 A
「たぶん、僕のレベルアップ機能のせいです……」

テックリード
「レベルアップで何が起きた?」

新人 A
「えっと……
バハムートのレベルを1上げたら、街全体もレベルが上がりました。

新人 B
「宿屋のオヤジが“レベルアップしたから宿代も上がったぞ!”って……」

新人 A
「それだけじゃなくて……
シヴァの属性も勝手に“炎”になって、
ついでにプレイヤーの財布まで“炎属性の財布”になってました」

テックリード
「なにそれ、多分召喚獣もプレイヤーも保存処理も戦闘ルールも、一つのディレクトリにおいてるからだわ。
だからディレクトリを分けよう。
召喚獣は召喚獣のディレクトリ、戦闘は戦闘のディレクトリ、プレイヤーはプレイヤーのディレクトリ。
“国”みたいに分けてしまえば、他国へ影響は漏れない。


ASCIIクラス図

(構造は大きく変わらないが、
“どこに所属するか” が明確になっていることが重要)

[Domain/Summon]
+---------------------------+
| SummonBeast               | <<interface>>
+---------------------------+
| + id(): SummonId          |
| + element(): Element      |
| + castSummon(): string    |
| + level(): Level          |
+---------------------------+
            ▲
            | implements
            |
+---------------+   +---------------+
|   Bahamut     |   |    Shiva      |
+---------------+   +---------------+

[Domain/SummonRepository]
+-------------------------+
|    SummonRepository     | <<interface>>
+-------------------------+
| + save()                |
| + findById()            |
+-------------------------+

ディレクトリ構成

src/
└── Domain/
    ├── Summon/        ← 召喚獣の国
    │   ├── Bahamut.php
    │   ├── Shiva.php
    │   ├── SummonBeast.php
    │   ├── SummonName.php
    │   ├── SummonId.php
    │   ├── Level.php
    │   └── Element.php
    │
    ├── Battle/        ← 戦闘の国
    │   └── ElementCompatibilityCalculator.php
    │
    ├── Player/        ← プレイヤーの国
    │   └── PlayerSummons.php
    │
    └── SummonRepository/  ← 保存の国
        └── SummonRepository.php

└── Infrastructure/
    └── SummonRepository/
        └── FileSummonRepository.php

12章:共通言語が力を発揮する→ユビキタス言語で迷わず設計できる瞬間

物語:新しくジョインした新人が即戦力

新人 C と新人 D が イフリート(Ifrit)リヴァイアサン(Leviathan) を担当することになった日。

以前の新人 A/B の時のような混乱は起きなかった。

新人 C
「召喚攻撃は castSummon() で統一、上位種は SummonAnimationuse ですね」

新人 D
「名前は ValueObject の SummonName で、生成は SummonFactory::create('Ifrit') ですね」

新人 C
「アニメーションは`SummonAnimationか」

新人 D
「召喚獣はSummonBeastですね」

新人 A & B(嬉しそうに)
「その通り! うちは全部そのルールで統一してるから!」

テックリード
「配属したばっかなのに、仕様を詳細に説明してないのにルールにそって開発できているし、自然と“チームの言葉”を使えていて良いね。君たちがこれまでやってきた設計が良いっていうことだよ」


PHPコード例:新規召喚獣のコード

class Ifrit implements SummonBeast {
    private int $mp = 450;

    public function castSummon(): string {
        return "Hellfire!";
    }

    public function mp(): int {
        return $this->mp;
    }
}

class IfritZero extends Ifrit {
    use SummonAnimation;
    private int $mp = 520;

    public function castSummon(): string {
        return "Inferno Storm!";
    }

    public function mp(): int {
        return $this->mp;
    }
}

class Leviathan implements SummonBeast {
    private int $mp = 400;

    public function castSummon(): string {
        return "Tidal Wave!";
    }

    public function mp(): int {
        return $this->mp;
    }
}

class LeviathanZero extends Leviathan {
    use SummonAnimation;
    private int $mp = 480;

    public function castSummon(): string {
        return "Tsunami Barrage!";
    }

    public function mp(): int {
        return $this->mp;
    }
}

PHPコード例:SummonFactory の更新

class SummonFactory {
    public static function create(string $name): SummonBeast {
        return match($name) {
            'Bahamut' => new Bahamut(),
            'BahamutZero' => new BahamutZero(),
            'Shiva' => new Shiva(),
            'ShivaZero' => new ShivaZero(),
            'Ifrit' => new Ifrit(),
            'IfritZero' => new IfritZero(),
            'Leviathan' => new Leviathan(),
            'LeviathanZero' => new LeviathanZero(),
            default => throw new Exception("Unknown summon: $name"),
        };
    }
}

ASCIIクラス図

[Domain/Summon]

+-------------------------+
|       SummonBeast       | <<interface>>
+-------------------------+
| + id(): SummonId        |
| + element(): Element    |
| + castSummon(): string  |
| + mp(): int             |
+-------------------------+
            ▲
            | implements
+--------------------+  +--------------------+  +--------------------+  +--------------------+
|      Bahamut       |  |       Shiva        |  |       Ifrit        |  |     Leviathan      |
+--------------------+  +--------------------+  +--------------------+  +--------------------+
| - mp: int          |  | - mp: int          |  | - mp: int          |  | - mp: int          |
+--------------------+  +--------------------+  +--------------------+  +--------------------+
| + castSummon()     |  | + castSummon()     |  | + castSummon()     |  | + castSummon()     |
| + mp()             |  | + mp()             |  | + mp()             |  | + mp()             |
+--------------------+  +--------------------+  +--------------------+  +--------------------+
            ▲                    ▲                      ▲                        ▲
            | extends            | extends              | extends                | extends
+--------------------+  +--------------------+  +--------------------+  +--------------------+
|    BahamutZero     |  |     ShivaZero      |  |     IfritZero      |  |   LeviathanZero    |
+--------------------+  +--------------------+  +--------------------+  +--------------------+
| use SummonAnimation|  | use SummonAnimation|  | use SummonAnimation|  | use SummonAnimation|
+--------------------+  +--------------------+  +--------------------+  +--------------------+
| - mp: int          |  | - mp: int          |  | - mp: int          |  | - mp: int          |
+--------------------+  +--------------------+  +--------------------+  +--------------------+
| + castSummon()     |  | + castSummon()     |  | + castSummon()     |  | + castSummon()     |
| + mp()             |  | + mp()             |  | + mp()             |  | + mp()             |
+--------------------+  +--------------------+  +--------------------+  +--------------------+

ディレクトリ構成

src/
└── Domain/
    ├── Summon/
    │   ├── Bahamut.php
    │   ├── BahamutZero.php
    │   ├── Shiva.php
    │   ├── ShivaZero.php
    │   ├── Ifrit.php
    │   ├── IfritZero.php
    │   ├── Leviathan.php
    │   ├── LeviathanZero.php
    │   ├── SummonBeast.php
    │   ├── SummonName.php
    │   ├── SummonId.php
    │   ├── Level.php
    │   └── Element.php
    │
    ├── Battle/
    │   └── ElementCompatibilityCalculator.php
    │
    ├── Player/
    │   └── PlayerSummons.php
    │
    └── SummonRepository/
        └── SummonRepository.php

└── Infrastructure/
    └── SummonRepository/
        └── FileSummonRepository.php

まとめ

意外と大変でしたが、考えるのは結構楽しかったです。戦闘にも載せましたが、まとめでも再度記載します。

  • バハムートに特化しているので、プログラムの処理でベストプラクティスでない場合があります。正しくは、書籍・記事・AIで確認してください。
57
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?