前稿でInterpreterパターン風のMiddlewareパターンの設計を紹介した。前稿の設計では、Middlewareパターンの特性である次の3つのうち、2と3が満たせていない課題があった。
- クライアントコードとハンドラの間に割って入り、入力を加工する
- ハンドラとクライアントコードの間に割って入り、出力を加工する
- クライアントコードとハンドラの間に割って入り、入力がハンドラに届く前に、早期に出力を返す
本稿ではその課題を解決したパターン、つまりちゃんとしたMiddlewareパターンの設計と実装を紹介する。
Middlewareパターンとは
Middlewareパターンの目的は、「ミドルウェア」と呼ばれるガワの処理で核となるハンドラを包むことで、プログラム本来の振る舞いを拡張可能にすること。MVCウェブフレームワークでは、コントローラの入出力となるHTTPリクエストやレスポンスをアプリケーション全体で統一的に加工する用途などに採用されている。
ミドルウェアができることは次の3つ:
- クライアントコードとハンドラの間に割って入り、入力を加工する
- ハンドラとクライアントコードの間に割って入り、出力を加工する
- クライアントコードとハンドラの間に割って入り、入力がハンドラに届く前に、早期に出力を返す
Middlewareパターン
設計
実装
概念を実装する
Middlewareパターンの概念であるInput
、Output
、Middleware
、Handler
をコードに起こす。入出力を表現したクラスであるInput
とOutput
は抽象的な名付けになっているが、例えばHTTPを扱うドメインなら、その界隈の言葉を借りてHttpRequest
とHttpResponse
のような具体的な名前でも構わない。
class Input
{
}
class Output
{
}
interface Middleware
{
public function process(Input $input, Handler $next): Output;
}
interface Handler
{
public function handle(Input $input): Output;
}
Middleware
は、パイプラインを継続する場合、$next->process()
を呼ぶ。中断する場合は、呼ばずにOutput
をreturn
する。ここがパイプラインの中断ができなかった前稿の設計から改善されたところである。
クライアントコード利便性のためのクラスを実装する
次に、クライアントコードがMiddlewareパターンを扱いやすくするための補助的なクラスPipelineBuilder
を作る。これは、ミドルウェアとハンドラを結合しパイプラインを形成するコードを宣言的に書けるようにしてくれるクラスだ。
final class PipelineBuilder
{
/**
* @var Middleware[]
*/
private $middlewares = [];
// ミドルウェアを登録するメソッド
public function use(Middleware $middleware): self
{
array_unshift($this->middlewares, $middleware);
return $this;
}
// 登録されたミドルウェアとハンドラを合成したハンドラを作るメソッド
public function build(Handler $handler): Handler
{
foreach ($this->middlewares as $middleware) {
$handler = new MiddlewareHandler($middleware, $handler);
}
return $handler;
}
}
このクラスがあることでクライアントコードは次のように宣言的に書くことができる。
$pipeline = (new PipelineBuilder)
->use(new OuterMiddleware())
->use(new InnerMiddleware())
->build(new ConcreteHandler());
$output = $pipeline->handle(new Input());
パイプラインを形成するためには、Middleware
とHandler
は同一視できなければならない。そこで、MiddlewareのインターフェイスをHandlerのインターフェイスに変換するアダプターを実装する。このクラスは、クライアントコードへ提供するものではなくPipelineBuilder
で使うことを想定している。
final class MiddlewareHandler implements Handler
{
/**
* @var Middleware
*/
private $middleware;
/**
* @var Handler
*/
private $handler;
public function __construct(Middleware $middleware, Handler $handler)
{
$this->middleware = $middleware;
$this->handler = $handler;
}
public function handle(Input $input): Output
{
return $this->middleware->process($input, $this->handler);
}
}
ミドルウェアとハンドラの実装
あとは、入出力の加工や中核的な処理など、具体的なコードを書くだけだ。
// 外側のミドルウェア
final class OuterMiddleware implements Middleware
{
public function process(Input $input, Handler $next): Output
{
return $next->handle($input); // ここで$inputやOutputを加工する
}
}
// 内側のミドルウェア
final class InnerMiddleware implements Middleware
{
public function process(Input $input, Handler $next): Output
{
return $next->handle($input); // ここで$inputやOutputを加工する
}
}
// ハンドラ
final class ConcreteHandler implements Handler
{
public function handle(Input $input): Output
{
return new Output(); // ここで中核的な処理を行う
}
}
クライアントコード
クライアントコードでは、作ったミドルウェアクラスとハンドラクラスを組み合わせてパイプラインを作り、パイプラインを実行するコードを書く。
$pipeline = (new PipelineBuilder)
->use(new OuterMiddleware())
->use(new InnerMiddleware())
->build(new ConcreteHandler());
$output = $pipeline->handle(new Input());