テクニカルテストでインプレッションを残す例外処理

オブジェクト指向について話すとき、例外処理は常に重要な話題です。今日は、ソフトウェア職人として例外を作る方法を学びます!

目次

1. 序章

プログラミングを学び始めたとき、「エラー」やそれに関連するものは常に怖かったですが、よく勉強してみると、エラーや例外は敵ではなく友達であることがわかりました。ただし、プロジェクトにとって興味深い方法で利用する方法を理解する必要があります。

私の場合、たいていのことにthrow new Exception()を使い、例外の洪水に簡単に迷ってしまいました。始めのうちは問題ありませんでした。まだ監視するチームで働いていなかったからです。

しかし、時間がたち、素晴らしい会社で働き始めると、特にファクトリーパターンを使った素晴らしい例外の実装に遭遇しました。この方法は、エラーに関してもシンプルでエレガントな方法でいかに物事を処理できるかに感激しました。

今日は、ビジネスロジック内のエラーメッセージを、わざわざ長く書かずに、例外書き方の趣味を身につける方法をご紹介します。

2. 避けたいこと

このチュートリアルに少し背景をもたせてみましょう:RPGシステムを開発していると想像してください。その中で、キャラクターのための簡単なインベントリを作成する必要があります。

src
├── Item
│   └── Item.php
└── Player
    ├── Inventory.php
    └── Player.php

この状況で、キャラクターにアイテムを装備しようとしているとします。しかし、明らかにいくつかの検証ルールとその適切な例外を設定するでしょう。

namespace DanielHe4rt\Player;

use DanielHe4rt\Item\Item;

class Player {

    public function __construct(
        public string $username,
        public int $level,
        protected Inventory $inventory,
    ) {

    }

    public function equipItem(Item $item): void
    {

        if ($this->inventory->hasItem($item)) {
            throw new \Exception(
                'アイテム "' . $item->name . '" を持っていません。'
            );
        }

        if ($item->minLevel > $this->level) {
            throw new \Exception(
                'アイテム ' . $item->name . ' を装備するにはレベルが低すぎます。最低レベルは ' . $item->minLevel . 'です。'
            );
        }

        $this->setupItem($item);
    }

    private function setupItem(Item $item): void 
    {
        // クールな処理をする
    }
}

検証ルールによりクライアントに対して異なる例外を送出していますが、これは機能します(コードが完成していれば)し、検証の目的を果たします。しかし、私が学んだことは、仕事の面接テストでは、作成の速さではなく納品の品質が評価されるということです。そのため、私の世界観は少し変わり、"奇妙で醜い"と思えるものを"シンプルでエレガント"なものに変える方法を理解し始めました。

このケースでは、特に二つのことを避けたいです:

  • 一般的な例外;
  • ビジネスロジックを埋める例外;

誤解しないでください、例外はそのままの場所に残りますが、コードの可読性を向上させましょう。

3. リファクタリング1:例外の作成

まず最初に「カスタム」例外を作成し、基本的な例外クラスを新しいクラスに拡張します。特別なことは何もありませんが、コードの可読性と理解度がいくつかのポイントで向上します。

namespace DanielHe4rt\Player; 

class PlayerException extends \Exception
{}

class PlayerInventoryException extends \Exception
{}

そして、equipItem()関数を簡単にリファクタリングして、標準的な例外を新しく作成した例外に置き換えましょう。

namespace DanielHe4rt\Player;

use DanielHe4rt\Item\Item;
use DanielHe4rt\Player\PlayerException;
use DanielHe4rt\Player\PlayerInventoryException;

class Player {

    public function __construct(
        public string $username,
        public int $level,
        protected Inventory $inventory,
    ) {

    }

    public function equipItem(Item $item): void
    {
        if (!$this->inventory->hasItem($item)) {
            throw new PlayerInventoryException(
                'アイテム "' . $item->name . '" を持っていません。'
            );
        }

        if ($item->minLevel > $this->level) {
            throw new PlayerException(
                'アイテム ' . $item->name . ' を装備するにはレベルが低すぎます。最低レベルは ' . $item->minLevel . 'です。'
            );
        }

        $this->setupItem($item);
    }


    private function setupItem(Item $item): void 
    {
        // クールな処理をする
    }
}

この新しい例外で、私たちは例外の内容が何について扱っているのか、特に例外が発生したときにコードベースのどこを探すべきかを正確に知っています。ほんの数クリックで**"PlayerInventoryException"**を検索するだけです。これにより、開発者の作業が楽になり、NewRelic/DataDogなどにこの情報を投入するDevOpsの仕事も簡単になります。

しかし、まだ気になることがあります... なぜこんなに大きなメッセージをビジネスロジックの中に入れる必要があるのでしょうか?そのため、まずファクトリーパターンというデザインパターンについて学びましょう!

4. デザインパターン:ファクトリーパターン

デザインパターンについて聞いたことがある人ならば、それがどんな問題を解決するのか少しは理解しているでしょう。しかし、まだ知らない場合は、説明したいと思います!

"デザインパターンは、一般的な問題に対する汎用的な解決策です。" - 誰か

この汎用的な問題というアイデアに基づき、何人かが集まって、日常的な問題をある程度効率的に解決するためのソフトウェアデザイン原則を作り始めました。デザインパターンは、次の3つのタイプに分類されます。

  • 行動パターン;
  • 生成パターン;
  • 構造パターン;

そして、https:/refactoring.guruのサイトでそれらについてもっと読むことができますし、すべての開発者にこのドキュメンテーションを探求し、自己開発を促進することを強くお勧めします。しかし、今はそのうちのひとつ、ファクトリーメソッド (またはファクトリーパターン)にフォーカスしましょう。

このパターンのアイデアは、異なるクラスで何千ものものをインスタンス化することなくオブジェクトを作成し、ある関数の単純な呼び出しで何かのインスタンスを「製造」することです。プログラミング業界ではModels::make(), Exception::create(), 何某Api::factory()のような呼び出しが数え切れないほどあります。これは、ある特定のクラスのコンストラクタメソッドにアクセスしなくても済むようにするためです。

APIクライアントの例で見てみましょう。ここでは、クライアントIDとシークレットを変更する必要がある場合に備えてコンストラクタをモジュラーにしていますが、関数の単純な呼び出しで最終的なオブジェクトを迅速に生成するオプションも提供しています。

class GithubClient {

    public function __construct(string $clientId, string $clientSecret) 
    {
        $this->client = new Client([
            'clientId' => $clientId,
            'clientSecret' => $clientSecret,
        ]);
    }

    public static function make(): self
    {
        return new self(
            env('github.client_id'),
            env('github.client_secret'),
        );
    }

    public function getUserByUsername(string $username = 'danielhe4rt'): array 
    {
        // githubにリクエストを送る...
    }
}

// オブジェクトを生成せずに呼び出す

$client = (new GithubClient('素晴らしい-client-id', '素晴らしい-client-secret'))
    ->getUserByUsername('danielhe4rt');

// ファクトリーを使って呼び出す 

$client = GithubClient::make()
    ->getUserByUsername('danielhe4rt');

私たちはすべてのパラメータを簡潔な方法で「製造」する静的な呼び出しを行いました。この"make/factory"またはあなたが何と呼びたいかは、注入するものによってはかなり広範なメソッドになることがありますが、それが問題になることはありません。

とにかく、ファクトリーパターンを使うことで可読性が改善されることがわかりました。もちろん、関数にもっと良い名前をつけることはできますが、基本的にはこのようなものです。さて、例外に戻りましょう!

5. リファクタリング2:例外の洗練

素晴らしい、ちょっとしたファクトリーについて学びました。今度は適用してみましょう。

コンテキストに合った理にかなった例外のファクトリーメソッドを作成します。そうです、この時に"make"や"create"を使うのではありません。例外は、ユーザーや開発者に何が起こっているかのストーリーを最低限伝える必要があり、それに焦点を当てていきます。

PlayerInventoryExceptionを少しリファクタリングした後、以下の結果が出ました。

class PlayerInventoryException extends \Exception 
{
    public static function itemNotFound(string $itemName): self
    {
        $message = sprintf('アイテム "%s" を持っていません。', $itemName);
        return new self(
            message: $message,
            code: 403 // Forbidden
        );
    }
}

このファクトリーをコード内で呼び出すと、例外の情報が例外自体に隔離されているため、読みやすさと保守性が向上します。

public function equipItem(Item $item): void
{

    if (!$this->inventory->hasItem($item)) {
        throw PlayerInventoryException::itemNotFound($item->name);
    }

    if ($item->minLevel > $this->level) {
        throw new PlayerException(
            'アイテム ' . $item->name . ' を装備するにはレベルが低すぎます。最低レベルは ' . $item->minLevel . 'です。'
        );
    }

    $this->setupItem($item);
}

次に、PlayerExceptionをリファクタリングするときにも同じアイデアを使用します。

class PlayerException extends \Exception 
{
    public static function lowLevelForThisEquipment(string $itemName, int $itemLevel): self
    {

        $message = sprintf(
            'アイテム %s を装備するにはレベルが低すぎます。最低レベルは %sです。',
            $itemName,
            $itemLevel
        );

        return new self(
            message: $message,
            code: 403 // Forbidden
        );
    }
}

そして今度は、equipItem()関数がすっきりとしました。

public function equipItem(Item $item): void
{
    if (!$this->inventory->hasItem($item)) {
        throw PlayerInventoryException::itemNotFound($item->name);
    }

    if ($item->minLevel > $this->level) {
        throw PlayerException::lowLevelForThisEquipment($item->name, $item->minLevel);
    }

    $this->setupItem($item);
}

素晴らしいですね?でも、まだ何か気になることがあります... なぜプリミティブな型を渡すのですか?これらの例外がクラスと"コミュニケーション"を取っているのなら?

全オブジェクトの参照を例外に渡し、そこで必要に応じて使用すべきものを処理する方が、ずっと綺麗です。何か将来的に必要になるかもしれないもので、今すぐき

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/he4rt/criando-exceptions-para-impressionar-no-teste-tecnico-2nie