じぶん対策

日々学んだことをアウトプットして備忘録にしています。

2023年の振り返り

はじめに

2023年もあっという間に大晦日になりました。
今年は、エンジニアとして仕事面でも成長でき、プライベートも充実した一年間でした。
今年一年間力をいれて取り組んできたことと、来年取り組んでいきたいことをまとめてみたいと思います。

2023年の目標と達成度

2023年当初に立てた目標と達成度は以下の通りです。

資格

残念ながらあと少しというところで合格できませんでした。
引き続き来年度も挑戦したいと思います。

敗因は、勉強時間の確保が業務都合で難しかったことと、問題自体への慣れの不足で時間配分が難しかったことです。
勉強時間というよりは試験対策のほうが重要な試験だなと感じました。

こちらはまだ受験できていないんですが、今年度中には合格を目指してそろそろ勉強を始めていこうと思います。

個人開発

  • 個人ブログの立ち上げ

これまで、はてなブログを利用して個人ブログを運用していましたが、今年は個人ブログを自作して運用を開始していくことができました。
現時点ですでにはてなブログのアクセス数を超えることができているので、来年はさらにアクセス数を増やしていきたいと思います。

個人で、実際にデプロイやCI/CDの構築を1からやってみることができたので、そのあとの業務でも活かすことができました。

URLは以下になります。

https://www.lyricrime.com/

フロントエンドの知見の獲得

今年は、フロントエンドの知見を獲得するために、以下のような取り組みを行いました。

  • 副業

ありがたいことに、本業の傍らでNext.jsを利用したフロントエンドの開発に携わることができました。
フロントエンドの知見を獲得するためには、実際に開発に携わることが一番だと思っていたので、この機会をいただけたことは本当にありがたかったです。
3月から継続して月40時間程度の稼働を続けることができ、単純にフロントエンドに携わる時間を増やすことができました。
また、初めて青色申告を行ったり、個人事業主としての経験を積むこともでき、経費を利用して技術書や開発PCへの投資も積極的に行うことができました。
来年も引き続き、副業での稼働もキープしていきたいと思います。

  • 勉強会の参加

以前から行っていたチーム勉強会でもフロントエンドの技術を取り上げることが多い一年だったなと思います。
Next.jsやastroなどフロントエンドにおける革新的な仕組みが多く登場したのも2023年の特徴だったと思います。
これらのキャッチアップのために、勉強会でも積極的にフロントエンドの技術について調査、議論を行うことができました。

目標外での成長

採用活動への参加

今年は、採用活動にも積極的に参加することができました。
チームメンバーの減少にともなって、採用活動やインターン生との交流の機会が増えた一年間でした。
採用活動の参加によって、自分が一緒に働きたい人はどんなひとなのか、自分はどんなエンジニアになりたいのかということを改めて考える機会になりました。

社内の共通ライブラリへの貢献

入社してからこれまで、担当のプロダクトで精一杯の状態でしたが、今年は社内の共通ライブラリにも貢献することができました。
こういうライブラリがあったら、他のプロダクトでも便利に使えるのでは、という視点を持ち担当プロダクトだけではなく社内全体の技術的課題への意識を持つことができるようになりました。

実装速度の向上

実装速度を向上させたいなという漠然とした思いは以前からありましたが、2023年は、ChatGPT、GitHub Copilotなどの登場によって、かなりの向上が見られました。
自分の意図していた形ではないですが、これらのツールの進化をいち早く積極的に取り入れたり、自分なりに試していくことができたので、実装速度の向上につながったと思います。

2024年の目標

資格

まずは今年取りきれていない資格を来年も引き続き取得を目指していきたいと思います。

個人開発

  • 新しく個人開発を始める

発信

  • ブログ、Qiita,Zenn等での発信を継続する

引き続き月2記事のペースで記事を投稿していき、アクセス数を増やしていきたいと思います。

  • 社外勉強会への参加

これまで、コロナ禍ということもあり、オフラインでの勉強会への参加は控えていましたが、来年はオフラインでの勉強会への参加も積極的に行っていきたいと思います。

知見の還元

  • 社内勉強会の継続

社内勉強会の主催を2024年も継続していきたいと思います。
自然消滅しない仕組みを考えながら、無理しないペースで継続していきたいと思います。

プライベートでの振り返り

普段は稼働時間が多い反動からか今年はプライベートでも積極的にいろんなイベントを楽しむことができました。

ライブ

単独

フェス

  • KOYABU SONIC
  • ジャイガ
  • レディクレ
  • ロックロックこんにちは!

改めて振り返ると多いですが、大阪に来たメリットを実感できた一年間でした。
仕事終わりだったり、半休でライブに行くことができるのは幸せなことだなと思います。

運動

  • ジム

今年は肩こりへの対策とリモートワークでの運動不足解消のためにジムに通い始めました。
週一程度ですが、かなり肩こりが改善されたので来年も継続していきたいと思います。

  • バスケ

これまでと変わらず、週一ペースで社会人サークルでゆるくバスケットボールをしています。
運動不足が続くとかなりしんどいですが、定期的に運動する機会を作っていきたいと思います。

所感

2023年は、エンジニアとしての成長を実感できた一年間でした。
起きてから寝るまでエンジニアとしてプログラムのことを考えているという状態になっていたので、来年はもう少しバランスを取りながらエンジニアとしての成長を継続していきたいと思います。
基本的に8時に起きて、9時に業務を開始し18時に業務を終了してからご飯とお風呂を済ませてあとは寝るまで副業での作業を行うという生活を続けていました。肩こり等に悩まされ、ジムに通い始めたりしながらなんとか継続できました。
たくさんの行動を起こし少ししんどいと感じる時期もありましたが、来年はこれらを継続する一年としたいと思います。

PHPの例外入門!

はじめに

PHPやLaravelを使って開発をしていると、例外処理という言葉をよく耳にします。
前半は例外処理について、初学者を対象に解説していきます。(PHPにはこういう機構があるよといった入門的な部分)
後半はPHP,Laravelの例外の概要と、筆者の考える実践パターンをいくつか紹介します。

対象読者

  • ジュニアエンジニアで、PHP/Laravelを使った開発を始めたばかりの人
  • PHP以外の言語経験があり、PHPにおける例外処理の仕組みを知りたい人
  • WEB業界に入ったばかりの駆け出しエンジニアで、エラーハンドリング?なにそれおいしいの?状態だった過去の自分

例外処理とは

まずは知らないIT用語を調べるときに大変お世話になるサイトから見てみましょう

参考: わわわIT用語辞典

例外処理(読:レイガイショリ 英:exception handling)とは
(想定内の)エラーが起きたときにやる処理のこと
です。

エラーには種類があります。

実はエラーは2種類に分けられます。
1.想定内のエラー
2.想定外のエラー
の2つです。

例外処理は、プログラムの実行中に発生する予期せぬエラーに対処するためのプログラミング手法です。 プログラムはエラーが発生した際にも制御された方法で動作を継続または適切に終了することができます。

具体的には、例外が発生した時に

  • リトライする
  • エラーの内容をログに記録する
  • メールやSlackなどで通知する
  • ユーザーに対してエラーが発生したことを通知する

といった処理を行います。

上記は概念的な例外処理についての説明ですが、例外という単語は、プログラミング言語の機能としての文脈で使用されることもあります。
プログラミング言語の機能を利用して、上記の例外に対して任意の処理を実装することも例外処理と呼ばれます。

例外処理の基本概念

PHPに限らず、多くのプログラミング言語(JavaC#など)では似たような例外処理の仕組みが用意されています。
Go,Erlang,Rustなどとは例外の考え方が少し異なるので、これらの言語の経験しかない場合は少し戸惑うかもしれません。

  • try-catch-finally構文tryブロック内でコードを実行し、エラー(例外)が発生した場合にはcatchブロックでそれを捕捉し、処理します。finallyブロックはエラーの有無に関わらず、最後に実行されるコードです。
  • 例外の伝播:例外が発生した場合、それを捕捉して適切に処理するか、あるいはさらに上のレベルへ伝播させることができます。
  • カスタム例外の定義:特定のエラー条件に対応するために、独自の例外クラスを定義することが可能です。

PHPにおける例外機構

PHPでも上記のtry-catch-finally構文を使って例外処理を行うことができます。(公式ドキュメント 例外)

PHPにおける例外処理は、PHP 5以降で導入され、PHP 7では更に拡張されました。これにより、開発者はコードの実行をより柔軟に制御し、エラー発生時の動作を明確に定義できます。

コード例(PHP8.2)

PHPの例外処理は、以下のような特徴を持ちます。
公式からの引用

例外がスローされ、現在の関数スコープに catch ブロックがなかった場合、 その例外は、マッチする catch ブロックが見つかるまで関数のコールスタックを "遡って" いきます。 その途中で見つかった全ての finally ブロックが実行されます。 グローバルスコープに遡るまで全てのコールスタックを探しても、マッチする catch ブロックが見つからない場合は、 グローバルな例外ハンドラが設定されていない限り fatal error となり、 プログラムが終了します。

わかりやすく、以下のようなコードを書いてみましょう。パターン別に2つ例外処理を書いてみます。

パターン1 呼び出し元関数側で例外をキャッチして例外処理

function divide($dividend, $divisor) {
    if ($divisor == 0) {
        throw new Exception("除算エラー:0で除算しようとしました。");
    }
    return $dividend / $divisor;
}

function doCalculation() {
    try {
        $result = divide(5, 0); // ここで例外が発生
    }
    catch (Exception $e) {
        echo "エラーが発生しました: " . $e->getMessage();
    }
    return $result;
}

この例では、divide()関数内で$divisorが0の場合に例外を発生させ、doCalculation()関数内でその例外を捕捉しています。
ちなみに、PHPでは文字列連結には.を使います。+を使用する言語もありますが、PHP.を使用するのはPerl由来のものだと思われます。

パターン2 呼び出し元関数側で例外をキャッチせず、さらにその呼び出し元で例外処理

PHPでは例外が発生すると、その例外は呼び出し元の関数に伝播します。

function divide($dividend, $divisor) {
    if ($divisor == 0) {
        throw new Exception("除算エラー:0で除算しようとしました。");
    }
    return $dividend / $divisor;
}

function doCalculation() {
    return divide(5, 0); // ここで例外が発生
}

try {
    doCalculation(); // この関数の内部で例外が発生
} catch (Exception $e) {
    echo "エラーが発生しました: " . $e->getMessage(); // ここで例外をキャッチ
}

doCalculation関数はdivide関数を呼び出し、divide関数で例外が発生します。しかし、doCalculation関数内にはtry-catchブロックがありません。 このコードでは、doCalculation関数内で発生した例外が、関数外のtry-catchブロックで捕捉されます。つまり、例外は発生した場所からコールスタックを遡り、最初に見つかった適切なcatchブロックで処理されるのです。

このようなコールスタックを遡って、より上位のレベルの処理にコントロールを移す手法は大域脱出と呼ばれます。
大域脱出自体は例外に限った用語ではないですが、基本的には例外に関することが多いようです。

PHPの例外クラス

PHPの例外処理はExceptionクラスを中心に構築されています。このクラスは、エラーメッセージ、エラーコード、ファイル名、エラーが発生した行番号などの情報を提供します。例外はthrowキーワードで発生させ、catchブロックで捕捉します。

try {
    // 例外を発生させる
    throw new Exception("エラーメッセージ", 1);
} catch (Exception $e) {
    echo "例外が捕捉されました: " . $e->getMessage();
}

PHP 7のErrorクラス

PHP 7では、Exceptionクラスとは別にErrorクラスが導入されました。これにより、PHPエンジンによる致命的なエラー(例えばメモリの不足やタイプエラー)もオブジェクト指向の方法で処理できるようになりました。ErrorクラスはExceptionクラスと同様の方法で使用できますが、主に内部システムエラーを表すために使われます。

try {
    // 致命的なエラーを発生させる
    throw new Error("致命的なエラーが発生しました", 1);
} catch (Error $e) {
    echo "エラーが捕捉されました: " . $e->getMessage();
}

ただし、基本的にErrorは開発中に発生するものであり、ユーザーには表示されないようにするのが望ましいです。
例えば、最もよく目にするのはType Errorですが、開発中に発生した場合は発生しないように修正するのが望ましく、ハンドリングするコードも書くことは少ないでしょう。

カスタム例外

PHPでは独自の例外クラスを作成し、特定のエラーシナリオに適した例外処理を実装できます。これにより、アプリケーション固有のエラーハンドリングを作成し、エラーの原因をより明確にすることが可能です。

class MyCustomException extends Exception {
    // カスタム例外クラスの実装
}

try {
    throw new MyCustomException("カスタムエラーが発生しました");
} catch (MyCustomException $e) {
    echo "カスタム例外が捕捉されました: " . $e->getMessage();
}

PHPの例外機構を使用することで、エラーの特定、捕捉、および処理をより効果的に行うことができ、より安全で信頼性の高いアプリケーションを構築することが可能になります。

PHPにおける例外の関係性

PHPの例外処理は、以下のような関係性を持ちます。
すべてのthrowされるオブジェクトは、Throwableインターフェースを実装しています。

  • Throwable
    • Exception
      • ErrorException
      • RuntimeException
        • LogicException
        • ...
    • Error
      • TypeError
      • ParseError
      • ...

Exceptionクラスは、すべての例外の基底クラスです。
Errorクラスは、すべてのエラーの基底クラスです。
これらの仕組みは、Javaなどの言語と近い構造です。

Throwableをキャッチすることもできるので、以下のようなコードを書くことで、投げられる例外全てをキャッチするようにすることもできます。

try {
    throw new Exception("エラーメッセージ", 1);
} catch (Throwable $e) {
    echo "例外が捕捉されました: " . $e->getMessage();
}

Javaとの例外区分の違い

Javaでは、例外には検査例外と非検査例外の2種類の例外があるらしいです。

参考: 段階的に理解する Java 例外処理

検査例外(Checked Exeptions)

コンパイル時にチェックされる例外。コンパイルを通すためには、try-catchで例外をキャッチするか、throwsで例外を投げることを明示する必要がある。

非検査例外(Unchecked Exceptions)

コンパイル時にチェックされない例外。実行時にエラーが発生するもの。

PHPでは、全ての例外は検査されないため、コンパイル時に例外をチェックすることはありません。つまりこの観点からは全ての例外は非検査例外と言えます。

ただしPHPコミュニティの中でもスタンスが分かれる部分でもあるらしく、

  • PHPの例外は全て非検査例外である
  • RuntimeException以外は検査例外である

という意見があるようです。

前者はコンパイル時の警告有無で区別していて、後者はRuntimeExceptionがあるのでこちらを実行時エラーとして、それ以外は検査例外としているということですね。
スタンスの是非には触れませんが、Javaの例外の区分はPHPにおける例外を区分するために参考になるかもしれません。

Laravelにおける例外機構

Laravelにおける例外処理の公式ドキュメントを要約して紹介します。

Laravelの例外処理システム

  • 中心的なハンドラー: App\Exceptions\Handlerクラスは、Laravelで発生するすべての例外をハンドリングする中心的な場所です。このクラスをカスタマイズすることで、アプリケーション特有の例外処理を実装できます。
  • 例外のレポーティング: Laravelでは例外が自動的にアプリケーションのログに記録されます。開発者はこの情報を利用して、発生したエラーの分析と対処が可能です。
  • カスタム例外の作成: 特定のエラーシナリオに適した独自の例外クラスを作成し、これを利用して例外を投げることができます。これにより、エラーハンドリングをより柔軟に行うことができます。

具体的なエラーハンドリングの方法

  • HTTPレスポンスのカスタマイズ: 例外に基づいてカスタマイズ可能なHTTPレスポンスを生成することが可能です。これにより、ユーザーに向けたより適切なフィードバックを提供できます。
  • ログとエラーレポーティング: Laravelでは、例外が発生した際に自動的にログに記録されるため、開発者はエラーの原因を追跡しやすくなります。

Laravelにおける例外の種類

PHPフレームワークであるLaravelでは、PHPのExceptionを継承するかたちで、独自の例外クラスを用意しています。

その詳細な種類については割愛しますが、以下の記事が参考になるかもしれません。

例外の命名の参考にするために Laravel の例外すべて漁ってみた

自作例外の実装によるメリット

さて、これまで見た通り、PHPでは独自にExceptionクラスを継承して例外クラスを作成することができます。
独自の例外クラスを作成することで、以下のようなメリットがあります。

  • 独自の例外を定義することで、発生したエラーの種類と原因をより明確にできる
  • 例外のメッセージ、処理を統一し、一貫性を保つことができる
  • 独自の例外を用いることで、発生したエラーがアプリケーション固有のものか、ライブラリやフレームワーク起因のものかを区別できる

例外処理における基本戦術

Laravelでは、例外処理をまとめて行うApp\Exceptions\Handlerクラスが用意されています。
つまり、フレームワーク側の思想としては、それぞれの処理で例外を投げて、Handler内でその例外処理を記述することを想定しているとも取れます。
例外という機構がコールスタックを遡っていく(大域脱出)という性質を利用して、例外処理をまとめて行うことは自然な流れです。

例外処理におけるアンチパターン

例外処理において、基本的に避けるべきアンチパターンを紹介します。

  • 例外の無視

最もわかりやすく、避けるべきアンチパターンは、例外を握りつぶすことです。
例外を無視した場合にいいことは何もなく、単に怠惰から無視されることがほとんどです。
どんなに問題が発生しにくいコードでも、エラーチェックとエラーハンドリングは必要です。

  • 例外をアプリケーションの制御フローに使用する

たまに見かけるのは、for文でループする最中に例外を投げて、break的に例外を利用しているようなものです。
プログラム上の異常を示す例外を通常フローの処理に使用するのはあきらかな誤用なので避けましょう。

  • 広すぎるキャッチ

PHPは例外クラスの設計上、Throwableをキャッチすれば全ての例外をキャッチできます。
ただし、これを行うと細かいエラーハンドリングを全て無視してしまうことになります。
Handlerなどの機構のメリットを活かせなくなってしまうので、各ユースケースでは想定している例外をキャッチし、それ以外は上位のレイヤーに任せるようにしましょう。

参考:
エラーを無視するな
例外設計における大罪

例外以外のエラーハンドリングとそのトレードオフ

いくつかのプログラミング言語では、PHPのtry-catchとは異なるエラーハンドリングのアプローチを採用しています。その背景にはtry-catchを使用することによる課題が存在します。

  • return以外の制御フローを持つ
    • 通常、関数はreturnによって上位の関数に返されます。プログラミングにおいて、処理の出口を1箇所にするという規約(MISRA-C)もあるように、処理の複雑さが増加することになります。
  • throwした場合にcatchされる箇所に処理が移動する。
    • 実質的にgoto文と同じように処理順が飛躍するため、処理を追いにくくなってしまう問題があります。
  • 呼び出し元のメソッドは、呼び出し先のメソッド内でthrowされる例外を知っておく必要がある

エラーハンドリングの方法には、これまで紹介した例外処理以外の方法もあります。

他の言語で例外処理以外の方法を採用しているものをいくつか調査してみました。

Scala

try-catch-finallyという例外機構自体は存在するようです。

import scala.util.{Try, Success, Failure}

def divide(x: Int, y: Int): Try[Int] = {
  Try(x / y)
}

val result = divide(10, 0) match {
  case Success(value) => s"結果は$valueです"
  case Failure(exception) => s"エラー: ${exception.getMessage}"
}

println(result)

ただ、関数型言語の性質上、関数の入出力以外の作用(副作用)を避けるために、あまり推奨されていません。

ほかに用意されている方法として、Eitherという抽象クラスを利用します。
eitherは「どちらか一方」という意味を持ち、LeftRightという2つのサブクラスを持ちます。
Tryとくらべると、例外処理以外の用途も表現でき、一方が正しい値、もう一方がエラー、もしくは代替の値を表現することができます。
TryよりEitherが一般的に使用されているようです。

def safeDivideEither(x: Int, y: Int): Either[String, Int] = {
  if (y != 0) Right(x / y) else Left("0で割ることはできません")
}

val result = safeDivideEither(10, 0) match {
  case Right(value) => s"結果は$valueです"
  case Left(error) => s"エラー: $error"
}

println(result)

Rust

Rustには「例外」の概念が存在しません。代わりに、主に以下の2つの方法でエラーを扱います:

  1. Result: RustのResult<T, E>型は、操作が成功した場合の値(Ok)またはエラーの場合のエラータイプ(Err)を格納します。これは関数型プログラミングの影響を受けたアプローチで、エラーが発生する可能性のある関数は、そのエラーを明示的にResult型として返す必要があります。

     fn divide(x: i32, y: i32) -> Result<i32, &'static str> {
         if y == 0 {
             Err("0で割ることはできません")
         } else {
             Ok(x / y)
         }
     }
    
     let result = match divide(10, 0) {
         Ok(value) => format!("結果: {}", value),
         Err(e) => format!("エラー: {}", e),
     };
    

    この例では、divide関数はResult<i32, &'static str>型を返します。これにより、エラーの発生がコードの中で明示的に扱われます。

  2. panicマクロ: Rustでは、通常の操作中に回復不能なエラーが発生した場合、panicマクロを使用してプログラムをクラッシュさせることができます。これはJavaのunchecked exceptionに似ていますが、Rustでは通常、本当に回復不能なエラーにのみ使用されます。

     fn risky_operation() {
         panic!("何か問題が発生しました");
     }
    

Rustでは、エラーは常に明示的に扱われる必要があります。これはJavaのchecked exceptionに似ていますが、RustではResult型を使ってエラーの存在を明示的に表現します。
Rustではエラー処理がより前面に出ており、エラーの可能性を無視することが難しいと言えます。

Go

Go言語のエラーハンドリングはJavaとは大きく異なり、よりシンプルで明示的なアプローチを採用しています。Goでは「例外」の概念がなく、エラーは普通の値として扱われ、プログラムのフローの中で明示的に管理されます。

  1. エラーは値: Goでは、エラーはerrorインターフェース型の値として表されます。このインターフェースはError()という単一のメソッドを持ち、エラーメッセージを文字列で返します。

  2. 明示的なエラーチェック: Goの関数は、エラーが発生する可能性がある場合、通常はその結果とともにエラーを返します。呼び出し元はこのエラーをチェックし、適切に対処する必要があります。

     func divide(x, y int) (int, error) {
         if y == 0 {
             return 0, errors.New("0で割ることはできません")
         }
         return x / y, nil
     }
    
     result, err := divide(10, 0)
     if err != nil {
         fmt.Println("エラー:", err)
     } else {
         fmt.Println("結果:", result)
     }
    

    この例では、divide関数は結果とエラーの両方を返し、エラーがある場合はそれがチェックされ、処理されます。

  3. panicrecover: Goにはpanicrecoverというメカニズムもありますが、これらは通常のエラーハンドリングとは異なり、主にプログラムが回復不能な状態に陥った時に使用されます。panicはプログラムをクラッシュさせ、recoverはプログラムがpanicから回復しようとする際に使用されます。

Goのエラーハンドリングのアプローチは、エラーを単なる値として扱い、プログラムの自然なフローの中でそれらを管理することに重点を置いています。

個人的なPHPを使用する場合の例外処理のベストプラクティス

これまで、PHP、Laravelにおける例外処理の仕組みとその課題、他の言語におけるエラーハンドリング方法について見てきました。
その中で、PHPの例外処理における課題を解決しつつ、うまく言語機構に乗っかったベストプラクティスを考えてみます。

課題について再度整理すると、

  • return以外の制御フローを持つ
  • throwした場合にcatchされる箇所に処理が移動する
  • 呼び出し元のメソッドは、呼び出し先のメソッド内でthrowされる例外を知っておく必要がある

という3つの課題がありました。

これらの解決をPHPで実践するためにPHPで取れるアプローチは、以下のようなものが考えられます。

  • できるだけ各階層で例外をキャッチして投げ直す
  • PHPDoc等を利用して、呼び出し元で投げられる例外を知る

できるだけ各階層で例外をキャッチして投げ直す

これは処理が大域脱出する際に追いづらくなることの回避を目的としています。ただ、例外処理で書くことがなく、投げ直すだけになってしまうことが多くなりすぎるとあまりメリットを享受できないため、設計の段階でどのようなレベルまで例外を投げるかを決めておく必要があります。
ある程度レイヤー分けされたアプリケーションであれば、レイヤーごとに自作例外を用意するのも方法としては検討できそうです。

ビジネスロジックを書いたドメイン層と、その外側の層では利用する例外は分けた方が良く、この区別のために自作例外を用意するといいと思います。
よくあるのは、UnauthorizedExceptionやNotFoundExceptionといった、HTTPステータス由来の例外をビジネスロジックからスローしてしまうような例です。
分離のために、ビジネスロジックからは独自の自作例外を投げ、それをHandlerのような外側の層でキャッチして、HTTPステータスに変換するといった仕組みにしておけば、関心を分離することができます。

また、想定内のエラーと想定外のエラーを区別して処理することも有効な手段です。
Webにおいて、エラーは全てレスポンスとして返されます。

Laravelの場合はHandlerに例外が到達した時点でAPMに自動的にエラーとして記録する挙動がデフォルトなので、想定内のエラーについてはそれぞれのコントローラー部分で適切にレスポンスを生成して返し、想定外のエラーについてはサーバーエラーとしてHandlerまで伝播させるといった方法を取ることができます。

PHPDoc等を利用して、呼び出し元で投げられる例外を知る

PHP自体が動的型付け言語であるため、内部で投げられる例外を事前に言語側の仕組みで全て把握することは難しいです。
少しでも例外を把握するために、PHPDocを利用して、呼び出し元で投げられる例外を知ることができます。
IDEによっては、自動で記載してくれたり、警告を出してくれたりするので、頼っていくのも良いかもしれません。

PhpStormでは、PHPDocの@throwsに応じて警告を出してくれます。

デフォルトでは下記の例外が除外(継承先を含む)されており、それ以外の例外については警告が出るようになっています。

  • \RuntimeException
  • \Error
  • \LogicException

参考:
PhpStorm の throws のチェックがうるさい

PHPDocにおける @throws の使い方 2021 ver.

上記や、先述したJavaでの例外の分類から、予期できない例外はRuntimeExceptionに属すると言えるため、RuntimeExceptionでラップして投げ直すというのが良いかもしれません。

例えば、下記のコードでは、Repositoryで発生した例外をUseCaseで処理します。
検査例外の処理パターンとして、1つ有用な方法として、RuntimeExceptionでラップして投げ直すという方法があります。
このように、例外のラップを行うことで、呼び出し元に情報を伝えることができます。

class UseCase {
    public function execute() {
        $repository = new Repository();
        try {
            $repository->findById();
        } catch (PDOException $e) {
            throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

class Repository {
    /**
     * @throws PDOException
     */
    public function findById() {
        // 処理
    }
}

参考: 【Java】汎用的な例外処理の対応方法、リトライの考え方など

まとめ

  • 例外は想定内のエラーと想定外のエラーに分けられる
  • PHPの例外処理はJavaの例外処理と似ているが、検査例外と非検査例外の区別など違う点もある
  • 他の言語では、例外機構の代わりにEitherなどを用いてエラーハンドリングを行うこともある
  • Laravelでは、例外処理をまとめて行うApp\Exceptions\Handlerクラスが用意されている
  • 想定内のエラーの場合、PHPの例外処理における課題の多くは、できるだけ各階層で例外をキャッチして投げ直すことで解決でき、特にドメイン層では独自例外を用意して、関心の分離を行うことができる
  • 想定外のエラーの場合、Handlerを利用してアプリケーション側で正常に扱えないエラーをサーバーエラーとして返すと同時にAPMへの記録をすることができる

所感

最終的に出た結論はHandlerで想定外のエラーを扱い、各コントローラーでは階層ごとに例外をキャッチして投げ直すという方法で、今回の記事を書く前後で大きく変わらない結果となりました。
ですが、他言語での例外の仕組み、Javaでの例外の分類を調べてみることで、自作例外を作成する際の設計の解像度があがりました。

想定している例外と、想定していない例外を区別することが重要で、Webシステムにおいては、想定している例外は最終的に適切なエラーレスポンスを生成して返すことになります。そのため、各層での例外の扱いを明確にすることが重要です。
一方で、想定していない例外についてはサーバーエラーとして扱うほかなく、Handlerを使用して、レスポンスの生成をすると同時にAPMへの記録を行うことで、Laravelの仕組みをうまく活かすことができます。

例外の伝播においてはPHPの場合、PHPDocやIDEのサポートをうまく活用していくことで呼び出し元への情報伝達をうまく行うことができます。

実際にプロダクトのコードを書く場合には今回調査したような例外処理の仕組みとクラスの設計を踏まえて、自作例外側の設計も抽象化ができると関心を分離しつつ楽にハンドリングできそうです。

Mockeryを使った引数の検証方法まとめ

はじめに

プログラミングをしていて、誰しもわからない箇所やメソッドの使い方がわからなくて調べる事があると思います。
今回、私は一度引っかかったことにもう一度引っかかって、さらに思い出すまでに時間がかかったのでメモとして残しておきます。

また、mockeryはとても直感的に使用できるライブラリではあるんですが、日本語での情報が少なく、今回問題を解決しようとして自分が以前書いた記事を見直すこともあったので、改めて知見を日本語で残しておこうと思います。

勝手に高専出身勢は全員英語出来ないと思っているので日本語の記事があると助かりますね。

ChatGPTに誤字脱字の校閲してもらったら指導を受けました。

読者層の想定: 記事の冒頭で「勝手に高専出身勢は全員英語出来ないと思っている」という記述があります。これは一般化の可能性があり、読者によっては不快に感じる可能性があります。対象読者層をより広くするためには、このような前提を排除する表現が適切かもしれません。

以前書いた記事はこちらです。

Mockeryの基本的な使い方

また、mockeryの公式ドキュメントの翻訳は以下になります。

https://readouble.com/mockery/1.0/ja/index.html

結論

  • 基本的にモックの引数を検証する場合において、引数がオブジェクトの場合はMockery::onを使用しましょう。
  • 引数が複数の場合は、引数ごとにMockery::onを渡す必要があります。

今回の問題

今回の問題はmockeryを使用した引数の検証についてです。

例として、今回は以下のようなケースを考えます。

class User
{
    public function __construct(
        private readonly string $userIdentifier,
        private string $userName,
    ) {
    }

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

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

    public function changeUserName(string $userName): void
    {
        $this->userName = $userName;
    }
}

class UserRepository
{
    public function findById(string $identifier): User
    {
        // 再構築処理
    }

    public function save(User $user): void
    {
        //  永続化処理
    }
}

これらのクラスを利用するユースケースを考えます。

class ChangeUserName
{
    public function __construct(
        private UserRepository $userRepository,
    ) {
    }

    public function execute(string $userIdentifier, string $userName): void
    {
        $user = $this->userRepository->findById($userIdentifier);
        $user->changeUserName($userName);
        $this->userRepository->save($user);
    }
}

このユースケースに対してテストを書いていきます。

class ChangeUserNameTest
{
    public testExecute(): void
    {
        $userRepository = \Mockery::mock(UserRepository::class);

        // findByIdの動作を定義します。
        $userRepository->shouldReceive('findById')
            ->with('user-identifier')                               // 引数の検証
            ->andReturn(new User('user-identifier', 'user-name'));  // 戻り値の指定

        $userRepository->shouldReceive('save')                      // 素直に書けばこれで引数の検証ができる?
            ->with(new User('user-identifier', 'new-user-name'));

        $changeUserName = new ChangeUserName($userRepository);
        $changeUserName->execute('user-identifier', 'new-user-name');
    }
}

ところが、このテストは失敗します。

公式ドキュメントを確認します。

引数のバリデーション

オブジェクトの引数のマッチングでは、Mockeryは厳密な===比較だけを行いますので、全く同じ$objectのみ一致します。

PHPにおいて、厳密な比較の場合、オブジェクトの場合は同じインスタンスである必要があります。今回のテストでは、findByIdで取得したオブジェクトとsaveに渡すオブジェクトは別のインスタンスになっているため、テストが失敗しています。

解決策

今回の問題を解決するには、Mockery::onを使用します。

公式ドキュメントにも記載があるのですが、いくつかバリエーションを紹介しておきます。

単純なMockery::on

まずは冒頭のケースでのMockey::onの使用例を紹介します。

public function testExecute(): void
{
    // ~~省略~~
    $userRepository->shouldReceive('save')
        ->with(\Mockery::on(function (User $user) {             // 無名関数の引数には実際にメソッドに渡される引数を指定します。
            $this->assertSame('user-identifier', $user->userIdentifier());
            $this->assertSame('new-user-name', $user->userName());
            return true;
            // 検証に成功したかどうかをboolで返します。今回の場合は検証に失敗するとassertSame関数が例外を投げるので、常にtrueを返します。
        }));
    // ~~省略~~
}

assertSameを使用せずに引数の確認

Mockery::onを使用する場合は、無名関数内でboolを返せばいいので、比較を自分で書いても大丈夫です。

公式にもこちらの方法の記載があります。

また、$thisを使用しない用に書くとstaticを付与する事ができるようになります。

public function testExecute(): void
{
    // ~~省略~~
    $userRepository->shouldReceive('save')
        ->with(\Mockery::on(static function (User $user) {          // 無名関数の引数には実際にメソッドに渡される引数を指定します。
            return $user->userIdentifier() === 'user-identifier';   // 例えば、IDのみを比較したい場合。
        }));
    // ~~省略~~
}

メソッドが複数引数を取る場合

今回私が引っかかったのは、メソッドが複数の引数を取る場合でした。

次のようなユースケースのテストを考えます。

class CreateUser
{
    public function __construct(
        private UserFactory $userFactory,
        private UserRepository $userRepository,
    ) {
    }

    public function execute(UserName $userName, Email $email): void
    {
        $user = $this->userFactory->create($userName, $email);
        $this->userRepository->save($user);
    }
}

class UserFactory
{
    public function create(UserName $userName, Email $email): User
    {
        // IDの採番
        // エンティティの作成
    }
}

このユースケースに対してテストを書いていきます。

先程と異なる点は、ID、Emailといったパラメータがオブジェクトとなっています。

class CreateUserTest
{
    public function testExecute(): void
    {
        $userFactory = \Mockery::mock(UserFactory::class);
        $userRepository = \Mockery::mock(UserRepository::class);

        $userFactory->shouldReceive('create')
            ->with(
                \Mockery::on(static function (UserName $userName) {
                    return $userName->value() === 'user-name';  // 第一引数の検証
                }),
                \Mockery::on(static function (Email $email) {
                    return $email->value() === 'user-email';    // 第二引数の検証
                })
            ) // withの引数には、引数の数だけMockery::onを渡す必要があります。
        ->andReturn(new User('user-identifier', 'user-name'));  // 戻り値の指定

        $userRepository->shouldReceive('save')
            ->with(\Mockery::on(function (User $user) {
                $this->assertSame('user-identifier', $user->userIdentifier()); // Factoryから返却されたUserIdであることを検証します
                $this->assertSame('user-name', $user->userName());
                $this->assertSame('user-email', $user->email());
                return true;
            }));

        $createUser = new CreateUser($userFactory, $userRepository);
        $createUser->execute(new UserName('user-name'), new Email('user-email'));
    }
}

このように、mockery::onを使用することでかなり柔軟な引数の検証が可能になります。

まとめ

  • 基本的にモックの引数を検証する場合において、引数がオブジェクトの場合はMockery::onを使用しましょう。
  • 引数が複数の場合は、引数ごとにMockery::onを渡す必要があります。

所感

オブジェクトに対する引数の検証は、設計をきっちりしようとするとかなり頻出するパターンです。
今回の例のように、Repositoryを採用した場合は、抽象化されるため、検証内容が以下のように変わります。

実際のDBに意図した値が保存されていること => saveメソッドに渡されるオブジェクトが意図したものであること

Mockeryを使用する上で、withを使用した様々なテスト実装パターンが存在しますが、Mockery::onがあれば、オブジェクトの検証はほぼカバーできると思います。

社内勉強会レポ 2023-11-10 GraphQL

はじめに

今回は社内勉強会のレポートです。
定期的に社内のエンジニアで集まって勉強会を行っています。
勉強会のノリとしてはだいぶ緩く、お題はなんでもいいんですが参加者が気になっている技術などについてあまり前準備なく、みんなで調べて見解を共有するという感じのものです。
具体的に質問や議論があればそれらをテーマにすることもありますが、社内勉強会では参加するメンバーが別々のプロジェクトチームに所属していることが多いので、共通して興味のありそうなものを選択します。
今回はGraphQLについて調べてみました。

GraphQLとは

参考: GraphQLを徹底解説する記事 参考: 15分で分かった気になるGraphQL

まずは理解度を確認するために、GraphQLとは何かをざっくりと確認します。

GraphQLはクエリ言語であり、REST APIの代替として開発されました。
特に複雑なデータ構造や多様なクライアントのニーズを持つアプリケーションで有効なようです。
こちらのスライドにあるように、クエリを送ることで必要なデータを取得できるようになります。

OverFetchとUnderFetch

オーバーフェッチ(OverFetching)

定義: オーバーフェッチは、クライアントが必要以上のデータを取得する状況を指します。

REST APIでの問題: RESTでは、エンドポイントが固定されたデータセットを返すため、クライアントが必要とする特定の情報だけではなく、余分なデータも含まれることが多いです。

GraphQLによる解決: GraphQLでは、クライアントが必要なデータのみを指定してクエリを送ることができます。このため、余分なデータの取得を避け、効率的なデータ取得が可能になります。

アンダーフェッチ(UnderFetching)

定義: アンダーフェッチは、クライアントが一度のリクエストで必要なデータをすべて取得できない状況を指します。

REST APIでの問題: 一つのエンドポイントからは限られた情報しか取得できないため、複数のリソースを組み合わせる必要がある場合、複数のAPIリクエストを行う必要があります。

GraphQLによる解決: GraphQLでは、一つのクエリで複数のリソースや関連するデータを同時に取得できます。これにより、必要なすべてのデータを一度のリクエストで効率的に取得することが可能です。

弊社では、基本的にRESTfulなAPIを作成しているため、このあたりの辛さはあるよねといった課題感を再確認します。

メリット、デメリット

参考: GraphQLを導入する時に考えておいたほうが良いこと

GraphQLのメリット

GraphQL自体はさまざまなWebサイトやブログ等でメリットやデメリットが解説されていますが、まとめると以下のような内容になります。

  1. 効率的なデータ取得: クライアントは必要なデータのみをリクエストでき、オーバーフェッチやアンダーフェッチを防ぎます。
  2. 柔軟なクエリ: 複数のリソースを一つのクエリで取得でき、APIリクエストの数を減らすことが可能です。
  3. リアルタイムデータ: サブスクリプションを利用することでリアルタイムのデータ更新が容易になります。
  4. 自己文書化: クエリ言語がスキーマに基づいているため、APIの文書化が容易になります。

GraphQLのデメリット

  1. 複雑なクエリのパフォーマンス: 深くネストされたクエリはサーバーに大きな負荷をかける可能性があります。
  2. キャッシュの難しさ: HTTPキャッシュの利用がRESTに比べて難しくなることがあります。
  3. 学習曲線: 新しいクエリ言語の学習とシステムへの適応には時間がかかることがあります。
  4. ファイルアップロード: 標準のGraphQLはファイルアップロードを直接サポートしていないため、実装にはカスタムスキーマや追加のライブラリが必要になることが多いです。
  5. エラーハンドリング: デフォルトではGraphQLはエラーを返さず、全てステータスコード200となります。エラーはあくまでレスポンスの内容という形をとります。
  6. モニタリング: すべてのリクエストが同じエンドポイントを使用するため、リクエストの種類を識別しにくいことがあり、モニタリングが複雑になることがあります。

REST APIのメリット

  1. 広範なサポートと成熟度: 広く使われており、多くのツールとライブラリが利用可能です。
  2. シンプルな設計: 理解しやすく、多くの開発者にとって馴染み深いです。
  3. キャッシュの利用: HTTPキャッシュメカニズムを簡単に活用できます。
  4. ファイルアップロード: RESTはマルチパートフォームデータをサポートしており、ファイルアップロードが比較的簡単に実装できます

REST APIのデメリット

  1. オーバーフェッチとアンダーフェッチ: 必要ないデータの取得や、複数のリクエストが必要な場合があります。
  2. 固定されたデータ構造: エンドポイントが固定されたデータセットを返すため、柔軟なデータ取得が難しいです。
  3. 多くのエンドポイント: 多様なデータニーズに対応するために多くのエンドポイントを設計する必要があります。

GraphQLの実装

GraphQLは特定のライブラリではなく、RESTのようなアーキテクチャの一つです。そのため、具体的な実装方法はいくつか存在します。

PHPにおいては、LightHouseというGraphQLの実装があります。

https://lighthouse-php.com/

また、一般的に知られているのは、Apollo ServerというGraphQLの実装です。

https://www.apollographql.com/

採用事例

GraphQLの概要がなんとなく掴めてきたところで、実際に採用している事例を見てみます。

メルカリShops の技術スタックと、その選定理由

Next.js + NestJS + GraphQLで変化に追従するフロントエンドへ 〜 ショッピングクーポンの事例紹介

How Netflix Scales its API with GraphQL Federation (Part 1)

考察

採用事例をみて、参加者で気付いたことを話し合いました。

  • コンテンツの配信のようなリソースの種類や量が多く、1ページの情報量が多いプロダクトがほとんど
  • 発信力もあるかもしれないが、比較的有名なサービスが多い

また、下記のようなサイトも参考にしました。

参考: GraphQLがあまり普及しない理由はなんですか?

この回答では、以下のような内容が説明されています。

  • マイクロサービスをまとめるためのBFFとしては非常に有効
  • GraphQL特有の問題がある(N+1、クエリのコスト)
  • フロント、バックが同じチームならREST APIで十分

確かに、マイクロサービスの場合、BFFとしてGraphQLを採用することで、フロントエンドの開発者が必要なデータを自由に取得できるようになります。

弊社での採用についても少し意見を出しました。

  • 弊社のプロダクトの場合、マイクロサービスではなく、フロントとバックも分かれていないことが多い
  • 採用事例のようなコンテンツの配信のようなリソースの種類や量が多く、1ページの情報量が多いプロダクトはスマレジアプリマーケットくらい
  • ただし、リソースの量や開発規模を考慮するとGraphQLを採用するほどのモチベーションはないんじゃないか

脱線

REST APIへの対抗策として、こんなのもあるよと私の方から提案したのが、tRPCというものです。

個人的に副業や個人開発を通してフロントエンドの技術を触っていて、キャッチアップした内容を簡単に共有しました。

Next.js14や、Blitz.jsといったフレームワーク、T3 Stackと呼ばれる技術スタックなどがあり、フロントエンド界隈でのREST API以外のアーキテクチャの動きについて共有しました。

まとめ

今回はGraphQLについてざっくりと調べてみました。

  • GraphQLはクエリ言語であり、REST APIの代替として開発されました。
  • GraphQLは特定のライブラリではなく、RESTのようなアーキテクチャの一つです。
  • GraphQLのメリットとしては、効率的なデータ取得、柔軟なクエリ、リアルタイムデータ、自己文書化があります。
  • GraphQLのデメリットとしては、複雑なクエリのパフォーマンス、キャッシュの難しさ、学習曲線、ファイルアップロード、エラーハンドリング、モニタリングがあります。
  • GraphQLの実装としては、LightHouseやApollo Serverがあります。
  • GraphQLの採用事例としては、メルカリShopsやYahoo!ショッピングNetflixなどがあります。
  • 弊社で採用する候補としては、スマレジアプリマーケットがありますが、リソースの量や開発規模を考慮するとGraphQLを採用するほどのモチベーションはないんじゃないかという結論になりました。

所感

このようなゆるい勉強会を定期的に行っています。
直接的な業務ではなくても、特定の技術にたいして「名前は聞いたことある」程度から「概要は掴めた」程度まで解像度を上げていくことができるので、個人的にはとても良い勉強になっています。
また、社内のエンジニアの技術力や興味のある技術についても知ることができるので、社内の技術力の向上にもつながっていると思います。
ちなみに、弊社ではPHPやLaravelといった技術を採用しているため、このあたりがテーマになった時はもう少し深掘りした内容となることが多いです。

DDD(Domain Driven Design: ドメイン駆動設計)を布教したい

はじめに

今回はドメイン駆動設計を布教する際に押さえておくことを整理して記事にしたいと思います。

この記事では、DDDの概要を整理するとともに、DDDの採用におけるメリットを整理していきます。

これから勉強する人、そもそもDDDに興味がない人がすこしでもDDDに対して興味を持っていただければと思います。

DDDについて学習し、批判的な意見を持てるころには、設計への知見が深まっていると思います。

対象読者

  • バックエンドエンジニアで、設計の勉強をしてみたい人
  • DDDについてある程度調べたが採用メリットがいまいち理解できない人

まとめ

  • DDDは業務をモデリングする戦略的設計と、モデルを実装する場合のオブジェクト指向における実装パターンを指す戦術的設計に分けて語られる
  • DDDは何か特別な手法ではなく、業務の複雑さを愚直にソフトウェアに反映するための一つの考え方
  • DDDはデザインパターンのベストプラクティス集でもあり、クラス分割のための指針としても使える

過去の記事

ちなみに、過去にDDDについて学んだ際に書いた記事がいくつかあります。

「ドメイン駆動設計入門」を読む その1 ドメインオブジェクト編

「ドメイン駆動設計入門」を読む その2 ユースケースを組み立てるためのパターン編

LaravelにおけるRepositoryについて再考してみる

動機

DDDとクリーンアーキテクチャは設計思想として色物扱いされがちであったり、たびたび論争の火種になりがちです。
私の観測範囲だと批判のほとんどは理解不足や誤解によるところが大きいと感じています。
もちろんソフトウェア開発における銀の弾丸はないので、全てのソフトウェアに有効ではないですが、DDDについて理解しておくことで、ソフトウェア開発における設計の幅が広がると思います。
ただ、色物扱いされる理由でもありますが正しい情報が少ないのも事実です。そこでこの記事ではDDD入門として、参考となる情報源を紹介するとともに、採用におけるメリットを整理していきます。

DDDにおいて最も重要なこと

DDDは、関心事をモデリングし、分離することでソフトウェアの複雑性に向き合うことを目的としています。

DDDの二つの側面

DDDという思想は大きく分けると二つの側面を併せ持ちます。
1つは、戦略的設計、もう1つは戦術的設計と呼ばれます。

戦略的設計

こちらはドメイン、つまり業務の分析を行う際に、どのようにモデリングするかを考えることを中心としています。
どこまでが業務の範囲なのか、どのように分割するのかということをドメインエキスパートと呼ばれる業務に詳しい専門家とエンジニアが協力してモデリングを行っていく考え方です。

戦術的設計

こちらは戦略的設計で作成したモデルを実装する際に、どのように実装するかを考えることを中心としています。

例として

  • ValueObject
  • Entity
  • Repository
  • Service
  • Factory
  • Aggregate

などのパターンがあります。

これらは個別のデザインパターンとして知られているものもありますが、DDDの文脈として責務を分離するために用いられることが多いです。

個人的な理解としては設計における1種のフレームワークとして捉えています。

それぞれのパターンについてはいい記事が存在していたりするのでそちらに説明を譲ります。
DDD基礎解説:Entity、ValueObjectってなんなんだ

戦術的設計や軽量DDDと呼ばれたりするこちらのコードの品質に着目した考え方は、戦略的設計において改善されたモデルを継続的にソフトウェアに反映するために、拡張性や保守性を高めるためのベストプラクティス集と考えることができます。

軽量DDDには意味がない、というような意見を目にすることもありますが、私個人としては拡張性を高めるだけでも十分に価値があると考えています。

よくあるパターンとしては、戦略的設計、つまりモデリングをコードに反映したいと思った時にコード品質が低く、変更が容易でないなどの理由でコードがボトルネックとなってしまうパターンです。
このような場合は先にデザインパターンとしての軽量DDDをどんどん取り入れつつ、モデリングに徐々に着手していくことになると思います。
そもそもDDDは、継続してモデリングとソフトウェアへの反映を反復してソフトウェアの価値を高めることが大切なので、どちらが先か、ではなくどちらから始めてサイクルを回すかという違いでしかないと思っています。

DDDに適しているケース

DDDの目的はソフトウェアの「複雑さ」に立ち向かうことです。
つまり、複雑さをもつソフトウェアに適していると言えます。

ソフトウェアの抱える複雑さと辛さ

ここでは、業務で一定規模以上のソフトウェアを開発する際に抱える複雑さと辛さについて考えてみます。
つまり、どうして小難しいDDDなんてものを勉強して適用しなくちゃいけないの?という疑問に対する具体的な答えを考えてみようということです。

ちなみに一定規模というのはここではDBの持つテーブル数が20を超えたあたりからそれ以上の規模を想定しています。
プロダクトのフェーズでいうとある程度ユーザーがいて、今後もメンテナンスが必要な10-100のようなフェーズを想定します。

このフェーズのプロダクトに仕様変更や機能追加を行う場合、どういう手順を踏むでしょうか。

設計手法はどうであれ、DB設計は必要になると思います。

コードの設計についてはどうでしょうか。

このフェーズのプロダクトでは、設計したDBの構造がそのままCRUD機能として実装できることが少なくなってきたり、既存のDBのデータを使いたいが、レコードという単位ではないデータを扱う必要が出てきたりします。

このような場合にDDDではコードを作成する指標としてドメインモデルを作成します。

また、DDDにおいてはRepositoryパターンの採用によって具体的な永続化手段に依存せずにコードを記述することができます。このドメインではこういうモデルが必要、モデルを作成するためにDBから構築する処理はRepositoryにカプセル化しておく、というようなことができるため、ドメインモデルを中心としたコードの設計が可能になります。

参考事例: ドメイン駆動設計で保守性をあげたリニューアル事例 〜 ショッピングクーポンの設計紹介

DDDに適していないケース

DDDでは、ドメインに着目してクラスを設計するため、クラスの数としては単純なMVCと比較して多くなります。

そのため、単純なCRUD機能のみのプロダクトや、テーブル設計をそのままコードに落とし込むだけで十分足りてしまうようなプロダクトの場合にはメリットが薄くなります。
ソフトウェアにおいて、ある単位で整合性を保つ関係を表現するために、RDBが使用されます。RDBにおけるレコードというのはデータの永続化と整合性の担保を実現します。この仕組みが、ソフトウェアが大きく複雑になるに従って、ボトルネックになることがあります。データの永続化はしたいが、1つのデータが複数の領域において整合性を担保したいようなケースです。
このようなケースの解決策としてDDDが提案するのが、集約という考え方です。
DDDはこのようにソフトウェアとドメインの関連性に着目し、RDBのようなインフラと疎結合を実現することによって、ソフトウェアの複雑性に立ち向かいます。
そのため、RDBのレコードで整合性を担保できれば要件が十分に満たされるような規模のプロダクトでは、DDDのメリットは薄くなると言えると思います。

また、フレームワーク自体の思想と相反してしまう場合もあるため、使用するフレームワークやプロダクトとして重視することを整理して採用を判断することが大切です。

例えば、Railsのようなフレームワークは、MVCの思想を中心に設計されており、この思想の上で開発速度を向上することに重点を置いています。
そのため、DDDのような設計思想を採用することは、フレームワークの思想と相反してしまうため、採用することによって開発速度が低下する可能性があります。
Railsのメリットを活かしづらく、また型システムを持たないためレイヤードアーキテクチャの採用が難しく、DDDとしても中途半端な状態になりやすいため工夫しないと採用が難しいと思います。

Laravelの場合は、Railsに影響を受けたフレームワークですが、Railsほどのブラックボックスではなく、近年のPHPでは型システムのサポートも充実しているため、DDDを採用することで得られるメリットを享受しやすいと考えています。Laravelが優れている、というよりはRailsと比較すると異なる思想での開発を許容する文化があるフレームワークであると思っています。

DDDを既存のプロジェクトにどう導入するか

個人的には、戦術的設計から取り入れていくのが良いと考えています。
まずはコードベースで改善できる部分を改善していき、その過程でドメインモデルについての考察が深まり、その頃には改善における変更容易性のメリットを活かしてドメインモデルを洗練していくことができると思います。

まとめ

ソフトウェアというのは、時間の経過に従って新規作成より仕様変更のほうが工数がかかってしまうという状況が往々にして発生します。
プロダクトの成長に伴って、増加していく仕様変更の工数の増加を抑え、読みやすいコードを実現することはソフトウェアの価値を高めることにつながり、結果としてビジネスのスピードに追従できるソフトウェアを実現することにつながります。
DDDは、体系化されていてかつ採用実績の多い、ソフトウェアの設計思想のフレームワークともいえます。
個別のデザインパターンのメリットを活かしながらクラス分割するための指針としてまとまっており、ソフトウェア全体を通して一貫した方針でのクラス分割が可能になります。
まずは実装パターンのエッセンスを取り入れるところから、DDDの扉を開いてみてはいかがでしょうか。

参考記事

DDD基礎解説:Entity、ValueObjectってなんなんだ

ドメイン駆動設計で保守性をあげたリニューアル事例 〜 ショッピングクーポンの設計紹介

「プログラマー脳」を読んで得たエッセンス

はじめに

今回は、書籍「プログラマー脳 ~優れたプログラマーになるための認知科学に基づくアプローチ」を読んで、職業エンジニアとして業務を行うなかで、よりいいコードを書くために意識しておくべきことのヒントをいくつか得たのでまとめておきたいと思います。

本書の概要

本書は、プログラマーとしてのスキルや、習慣を向上させるために、脳の働きを理解するというアプローチで執筆された書籍です。
大規模なソフトウェアシステムの開発や、初学者に対する教育、自身のさらなる能力の向上に役立つ知識がまとめられています。

構成としては、4つのパート、13のチャプターから構成されています。

パートは以下の通りです。

  1. コードをよりよく読むために
  2. コードについて考える
  3. よりよいコードを書くために
  4. コーディングにおける共同作業

特徴としては、具体的なプログラマーが直面する課題を挙げつつ、それらに対する認知科学的な知見や実験の結果が説明されています。
簡単な例から始まり、読み進めていくと以前の章で出てきた知見についても触れながら、新しい知見を説明していくような流れが多いため、頭から順番に読み進めることをおすすめします。

本書から得たエッセンス

ここでは、具体的な例とともに、本書から得たエッセンスを紹介していきます。
知見の詳細や、根拠についてはぜひ本書を手にとって実際に確認していただくとして、この記事では知見の概要と私自身がこれから行動を変えるためのヒントとして取り入れたものを紹介していきます。

コードのチャンク化

プログラマーがコードを読む時に利用する短期記憶には容量が存在します。
The Magical Number Seven, Plus or Minus Twoという有名な論文で、私自身もこの本を読む前から聞いたことがありました。
平均的な人間の脳の短期記憶は7±2個の情報を保持できるというものです。いわゆるミラーの法則です。
さて、書籍のなかで紹介されていた短期記憶に関する実験で、個人的にかなり強く印象に残っているものがあります。

平均的なレベルのチェスプレイヤーのグループと熟練したチェスプレイヤーのグループにチェスの盤面を記憶させるという実験です。 その後、記憶した盤面をそれぞれ再現してもらい、正答率を比べて評価します。

結果は

  • 1回目の実験では、熟練したプレイヤーは平均的なレベルのプレイヤーよりも遥かに上手に盤面を再現できた
  • ただし、2回目の実験でなるべく非現実的な盤面を記憶させると、熟練したプレイヤーも平均的なレベルのプレイヤーと同じような正答率になった

このような結果への考察として、一回目の実験では、熟練したプレイヤーはチェスの戦略的な局面に紐づけて記憶していました。たとえば「シシリアンディフェンスのオープニングだが、ナイトが2マス左にいる」のような記憶の仕方です。

つまり、シシリアンディフェンスのオープニングの局面の情報が長期記憶に記憶されており、その情報を利用することで短期記憶の容量を少ししか使わずに記憶することができたのです。

この情報を組み合わせるまとまりを「チャンク(塊)」と呼びます。

これはもちろんコードにも当てはまります。

本書では、「チャンク化」しやすいコードのために、デザインパターンの活用を推奨しています。
また、コメントを書くことで初心者のプログラマーをサポートするだけでなく、開発者がコードをチャンク化するのにも役立つということが紹介されています。

これを読んで、私自身明確に答えを持っていなかったコメントの是非についてある程度の考えを持つことができました。

プログラマーの間でたびたび話題になるコメント論争は、「コードを読めばわかるような内容はコメントを書くな」「コメントは書けば書くほどいい」のような論争です。

チャンク化の観点から考えると、チャンク化できる量が極端に少ないものは書かなくてもいいが、チャンク化できる量が多いものはコメントを書くことでチャンク化を促進できるということになります。

もちろん、大前提としてコードはユニットテスト等で動作させて確認することができるので正しさを担保できますが、コメントはそうではないのでコメント自身が誤っているということもあります。

これはプロジェクト内でコメントも合わせてメンテナンスするという最低限のルールを設ける必要があります。

コメントは、明日以降の自分に対して書くものであり、実際に担当するのが誰であれ、プロジェクトを新しく担当する不慣れな初心者に向けて書くくらいの気持ちで書くといいと思いました。

複雑なコードの読み方

どうして複雑なコードを読むのが大変なのか、ということについても本書では説明されています。

理由としては短期記憶領域の不足、ワーキングメモリの不足、というものが挙げられています。

複雑なコードの読み方として、「(逆)リファクタリング」が紹介されています。

全体として保守性が高いコードが、必ずしも読みやすいとは限りません。多くのメソッド呼び出しがあり、多くのファイルが関連しているようなコードの場合、保守性は高くなっていても、読み進めるためにはあちこちに定義された関数をスクロールや検索で探す必要があります。

ここでは、読みやすさのためにメソッドをインライン化することで余計な認知的負荷を減らし、読みやすくすることを「認知的リファクタリング」と呼んでいます。

2つ目のプログラミング言語を学ぶのは最初の言語を学ぶよりも、なぜ簡単なのか

本書では、2つ目のプログラミング言語を学ぶのは最初の言語を学ぶよりも、なぜ簡単なのかということについても説明されています。

何かを学習した時、その知識が別の領域でも役にたつことがあります。本書ではこれを「転移」と呼んでいます。

例として、チェッカーを知っていればチェスを学ぶのが簡単になる、というようにJavaを知っていれば変数、ループ、クラス、メソッドなどの基本的な概念をすでに知っているので新しくPythonを学ぶのが簡単になります。

ただし、この転移には2つのタイプがあります。

一つ目は「正の転移」と呼ばれるもので、すでに学習した知識が、新たなタスクを実行する際の学習に対して良い影響を与えるものです。

脳はこの時、長期記憶がすでに持っている他の領域のために作られたメンタルモデルを元にして新しい領域に対するメンタルモデルを構築できるため、何について調べればいいかをすぐに理解できます。

一方で、もう一つのタイプは「負の転移」と呼ばれるもので、すでに学習した知識が、新たなタスクを実行する際の学習に対して悪い影響を与えるものです。

たとえば、Javaは変数を初期化しないと使えません。初期化していない場合はコンパイラが警告を出してくれますが、Pythonの場合は初期化しなくても使えてしまいます。このため、Javaを学んだ後にPythonを学ぶと、正しくないコードを書いてしまい、バグに繋がる可能性があります。

バグは、基本的にこのような負の転移から生まれる「誤認識」によって生まれます。

プログラマーが行うことのできる対策は多くないですが、できることもあります。

  • 自分が正しいと確信していることでも、間違っている可能性があることを認識すること
  • 自分がいつ間違った思い込みをしているのか、どう考えるのが正しいのか常に誤認識を意識をして勉強する

個人的な経験からは特に二つ目の対策に対して重要視しています。

プログラマーは、自分の誤認識が正しいものに修正されたとき、スッキリすると同時に、正しいものを知った途端になぜ間違えていたのかを忘れてしまいます。
どうしてこんな簡単なことがわからなかったのか、むしろ正しい方法以外あり得ないとさえ思えてしまいます。
この書籍を読んでから、少しでも悩んでいることはメモを残すようにしています。
大袈裟ではなく、解決したその瞬間に、何を悩んでいたのかを完全に忘れてしまうことがよく発生していることに気づくことができたからです。
この気づきは、自分自身の考え方の癖や、新しいことを学習する際に正の転移のために利用しがちな長期記憶の領域についても考えることができるようになりました。
たとえば、私の場合はOOPユニットテスト、DDD周りについて深く学んでいることもあって、無意識に当て嵌めながら学習していることが多いようで、これはReactを学習する時にかなり大きなメンタルモデルの転換が必要になったことで気づきました。
プログラマーとして、これから先も既存の知識をベースに学習していくことは変わらないので、自分自身の癖を把握しておくことはとても重要だと感じました。

まとめ

今回は、書籍「プログラマー脳 ~優れたプログラマーになるための認知科学に基づくアプローチ」から得られた知見や、私自身がこれから行動を変えるためのヒントとして取り入れたものを紹介しました。

今回紹介した内容はほんの一部で、より一般的な命名の重要性や作業の割り込みが与える悪影響など、さまざまなトピックについても認知科学的なアプローチで解説されています。
また、具体的なアプローチの方法として、コードへの印付けやドキュメントやテストによるアプローチなども紹介されています。

私自身がエンジニアになって、経験的になんとなく感じていることを、認知科学的なアプローチで実験結果を交えて説明されているので、とても納得感がありました。

もちろん実験結果が必ず正しいわけではないとは思いますが、経験則への裏付けとしてある程度まとまった知見が一冊にまとまっている点が評価でき、とても参考になりました。

DB基礎 物理設計周り

はじめに

WALプロトコル

WALプロトコルは、Write Ahead Logの略で、データベースのトランザクション処理において、データの更新前にログを書き込むことを指します。

基本的な動作は以下の2点です。

以前ブログにまとめたこともありますが、過去問に取り組む中でかなり頻出する知識なので改めてまとめておきます。

トランザクションは、COMMITもしくはROLLBACKが実行されたときに終了します。 しかし、多くのDBMSの内部処理ではデータファイルへ変更を反映する前に、トランザクションの終了をユーザに通知します。 ということは、COMMIT後のデータファイルへの反映前のタイミングで障害が発生した場合への対策として、WALプロトコルが用いられます。

バックアップとリカバリの手法

データベースのバックアップとリカバリは、データの安全性と整合性を確保するために不可欠です。

言葉の定義としては以下のようになります。

バックアップ:重要なデータベースのコピーを作成し、保存するプロセスです。これにより、データ損失や障害発生時に元の状態に復元できます。 リカバリ:バックアップからデータベースを復元するプロセスです。これは、システムの障害やデータの破損、紛失時に行われます。

1. バックアップのタイプ: ホットバックアップとコールドバックアップ

バックアップの方法は主に、システムが稼働中か停止中かによって、ホットバックアップとコールドバックアップに分けられます。

1.1 ホットバックアップ

  • 定義: システムが稼働中に、無停止でバックアップを作成できる手法です。

  • 特徴:

    • トランザクションの仕組みを利用してバックアップを作成。(mysqldumpでInnoDBテーブルをバックアップする等)
    • ロックを利用してバックアップを作成。(mysqldumpでMyISAMテーブルをバックアップする等)
    • OSやHWのスナップショット機能も利用可能。
    • 独自の方法でバックアップを作成するケースもあります。

1.2 コールドバックアップ

  • 定義: システムが停止状態でバックアップを作成する手法です。
  • 特徴: ディレクトリ以下のディレクトリとファイルをすべてコピーする。

脱線 MySQLInnoDBMyISAMの関係性

MySQLに関連する情報を調べているとよく目にするInnoDBMyISAMという単語について、簡単にまとめておきます。

MySQLは、ウェブアプリケーションを中心に広く使用されているオープンソースリレーショナルデータベース管理システムRDBMS)です。MySQLは複数のストレージエンジンをサポートしています。これらのストレージエンジンの中で、InnoDBMyISAMが最も知られています。

InnoDB

  • 主な機能: トランザクション(一連の操作の単位)をサポートし、ACID特性を満たします。これにより、データの安全性と整合性が確保されます。
  • 特徴: 外部キー制約と行レベルのロッキングをサポート。これにより、データベースのパフォーマンスと同時実行性が向上します。
  • 向いているケース: 高度なデータ整合性が求められるアプリケーション。

詳細はMySQLの公式ドキュメントのInnoDBの紹介を参照してください。

MyISAM

  • 主な用途: シンプルな読み取り専用または読み取りが多いワークロードに適しています。
  • 特徴: フルテキスト検索が可能。ただし、トランザクションはサポートしていません。
  • 向いているケース: 読み取りが主な操作となるアプリケーションや、トランザクションの整合性が重要でないシステム。

詳細はMySQLの公式ドキュメントのMyISAMの紹介を参照してください。
はじめにロックを用いてバックアップを行う際にmysqldumpでMyISAMテーブルをバックアップする等を例として書いたのはトランザクションのサポートがなく、ロックの仕組みを利用してバックアップを作成する必要があるためです。

mysqldump

バックアップ際に登場したmysqldumpというのは、MySQLデータベースのバックアップを作成ためのコマンドラインツールです。これを使用すると、データベースの内容をテキストファイルにエクスポートできます。このテキストファイルを利用して、データの復元や移行が可能です。

mysqldumpの使用方法やオプションについては、MySQLの公式ドキュメントのmysqldump — データベースバックアップツールを参照してください。

2. バックアップの形式: 論理バックアップと物理バックアップ

バックアップは、データの内容や形式によって、論理バックアップと物理バックアップに分類されます。

2.1 論理バックアップ

定義

論理バックアップは、データベースからデータだけを抜き出して作成するバックアップです。このバックアップ手法は、特定のデータや構造を抽出し、SQLコマンドとしてエクスポートします。

SQLコマンドとしてエクスポートとは、

CREATE TABLE example_table (
    id INT PRIMARY KEY,
    name VARCHAR(255)
);

のようなCREATE TABLE文であったり、INSERT, UPDATE文のような形式でエクスポートすることを指します。

利点

  1. 編集可能性: 論理バックアップファイルはテキストファイルであるため、必要に応じて編集可能です。これにより、バックアップから特定のデータのみをリストアすることができます。

  2. 移植性: 論理バックアップは、他のDBMSへの移植が容易です。これにより、データベース間でのデータ移動やマイグレーションがスムーズに行えます。

欠点

  1. ファイルサイズ: 物理バックアップに比べ、論理バックアップのファイルサイズが大きくなる可能性があります。

  2. 時間コスト: バイナリーテキスト変換が入るため、バックアップとリストアに時間を要することがあります。

2.2 物理バックアップ

定義

物理バックアップは、データベースの物理的なファイルを直接コピーするバックアップ手法です。この方法は、データベースファイル、テーブルスペース、ログファイルなどを含みます。

利点

  1. サイズと速度: 物理バックアップは、データベースの物理的なファイルをコピーするため、バックアップ、リストアの速度が速く、最小限のサイズで作成可能です。

  2. 完全性: 物理バックアップはデータベースの完全なコピーを提供するため、データベースの完全なリカバリが可能です。

欠点

  1. 互換性: 異なるデータベース管理システム間、または異なるバージョン間での互換性がない場合があります。

  2. 柔軟性の欠如: 物理バックアップは、個々のテーブルやデータのリストアが困難であることがあります。全体のリストアが必要となる場合があります。

実践的な例

  • 論理バックアップ: mysqldumpユーティリティは、MySQLデータベースの論理バックアップを作成するためのツールです。これを使用することで、SQLコマンドとしてのバックアップを作成できます。
  • 物理バックアップ: mysqlbackupコマンド(MySQL Enterprise Backupの一部)を使用して、MySQLデータベースの物理バックアップを作成できます。

詳細な情報や手順については、MySQLの公式ドキュメントを参照してください。

3. 増分バックアップ差分バックアップ

データの保全と迅速なリストアのために、バックアップ戦略として増分バックアップ差分バックアップが存在します。これらの方法は、データ量の増加とストレージコストの増大に対処するために用いられます。

3.1 増分バックアップ

  • 定義: 増分バックアップは、前回のバックアップ(増分もしくは完全バックアップ)以降に変更、追加、削除されたデータのみをバックアップする。
  • 利点:
    • ストレージ使用量が少ない。
    • バックアップの作成時間が短い。
  • 欠点:
    • リストアの際に、全ての増分バックアップファイルと最後の完全バックアップが必要。
    • 多くのバックアップセットが関与するため、リストアの時間と複雑性が増加する可能性がある。

3.2 差分バックアップ

  • 定義: 差分バックアップは、最後に作成された完全バックアップ以降に変更された全てのデータをバックアップする。
  • 利点:
    • リストアが簡単で、リストア時間が短縮される。差分バックアップと最後の完全バックアップのみが必要。
    • 完全バックアップよりも迅速に作成できる。
  • 欠点:
    • 増分バックアップに比べて、ストレージ使用量が多くなる可能性がある。
    • 毎日の差分バックアップが完全バックアップに近づくにつれて、バックアップの作成時間が長くなる。

3.3 フルバックアップ

  • 定義: フルバックアップは、指定されたデータソースの全てのデータをコピーするバックアップ方法です。
  • 利点:
    • リストアが簡単で、他の任意のバックアップなしでリストアできる。
    • バックアップデータの整合性が高い。
  • 欠点:
    • 他のバックアップ方法に比べて、ストレージ使用量が多い。
      • バックアップの作成に時間がかかる可能性がある。

参考

MySQL入門(バックアップ編)

所感

バックアップ等の仕組みについては障害が発生してからでは遅いので、事前にしっかりと知識をつけておく必要があると感じました。 重要な知識だからこそ、過去問でも頻出するのだと思います.