-
Notifications
You must be signed in to change notification settings - Fork 0
02 Core Concepts
Woof の最大の特徴である「純粋関数としての Web アプリケーション」を実現するためには、プログラミングにおいて発生しやすい「予測不能な状態の変化」をコントロールする必要があります。
この章では、Woof の美しく安全なデータフローを支える 3 つの核心的な仕組みについて解説します。
Web アプリケーションの開発では、システムの現在時刻の取得や、乱数の生成、ファイルシステムへのアクセスなど、実行するたびに結果が変わったり、外部のシステムに影響を与えたりする処理 (副作用) がどうしても避けられません。
PHP の組み込み関数 (time() や mt_rand() など) をビジネスロジックの中で直接呼び出してしまうと、「ある特定の時刻にだけ発生するバグ」のテストを書くことが極めて困難になります。
Woof では、これらの外的要因に依存する処理を直接呼び出すことを禁止し、すべて Environment (環境コンテキスト) という単一のオブジェクトの中に隔離しています。
// NG: 組み込み関数を直接使う (テスト時に時刻を固定できない)
$time = time();
// OK: Environment を経由して取得する (テスト時に時刻を固定可能)
$time = $env->now();Web アプリケーションの実行時には WebEnvironment クラスが渡されます。この中には以下の抽象化されたインターフェースが格納されており、開発者は常にこれらを経由してシステムにアクセスします。
-
Clock: 時刻を取得します。テスト時には時間を固定する
FixedClockに差し替えることができます。 -
Random: 乱数を生成します。テスト時には任意の配列を返す
ArrayRandomに差し替えることができます。 - Config / Resources / DataStorage: 設定値やファイルへのアクセスを提供します。テスト時には空のオブジェクト (Null Object) を渡すことで、ファイルシステムへの不要なアクセスを遮断できます。
コードの規模が大きくなると、「どこかの処理で、いつの間にかオブジェクトの中身が書き換えられていた」というバグ (状態の予期せぬ変更) が発生しやすくなります。
これを防ぐため、Woof の主要なデータクラス (Request や Response 、 Locale など) は、一度生成されたら絶対に中身が変更されない 不変 (Immutable) なオブジェクトとして設計されています。
不変なオブジェクトを構築するため、Woof では Builder パターンを多用します。例えば、レスポンスを作成する場合は ResponseBuilder を使用し、各種パラメータをセットした最後に build() メソッドを呼び出します。
use Woof\Http\ResponseBuilder;
use Woof\Http\Status;
use Woof\Http\Response\TextBody;
$builder = new ResponseBuilder();
$builder->setStatus(Status::getOK());
$builder->setBody(new TextBody("Hello"));
// build() を呼んだ時点で変更不可能な Response オブジェクトが生成される
$response = $builder->build(); 「不変性」と「副作用の隔離」は安全なアプリケーションをもたらしますが、毎回 Builder クラスを初期化し、Cookie やセッションの保存処理を個別に記述するのは少し冗長に感じるかもしれません。
そこで実際の Controller 開発では、これらの操作を劇的にシンプルにする Woof\Web\Operator クラスを使用するのがベストプラクティスです。
Operator は、Controller が受け取った Request と Environment を内部に保持し、メソッドチェーン (流れるようなインターフェース) を使ってレスポンスの構築からセッションの操作までを一気に記述できる強力なファサード (窓口) です。
use Woof\Http\Status;
use Woof\Web\Operator;
// Operator を使った洗練されたレスポンス構築
return (new Operator($request, $env))
->setStatus(Status::getOK())
->setCookie("visited", "1")
->setView(new MyView())
->build();