(jp) =
前もっての注意: この投稿は、絶対的な真実の情報源としてではなく、思考練習として書きました。 人々が反対し、その理由を教えてくれるのを楽しみにしているので、遠慮なく返信してください。

おそらく、実行時にアルゴリズムを選択できるようにする動作パターンである戦略パターンを以前に使用したことがあるでしょう。
古典的な例を考えてみましょう。ユーザーは、XML、JSON、または配列のいずれかの形式で何らかの入力を提供します。 その入力をきれいな JSON 文字列に解析する必要があります。
したがって、これらすべての入力:
'"title":"test"'
'<title>test</title>'
['title' => 'test']
これに変換します:
"title": "test"
もう 1 つ要件があります。これらの戦略は拡張可能である必要があります。 開発者は、他の種類の入力 (YAML、インターフェース、反復可能なオブジェクトなど、必要なものは何でも) を処理するための独自の戦略を追加できるようにする必要があります。
古典的な解決策とその問題を見てみましょう。
通常、すべての戦略が実装する必要のあるある種のインターフェースを導入することから始めます。
interface ParserInterface
public function canParse(mixed $input): bool;
public function parse(mixed $input): mixed;
各戦略は、特定の入力で実行できるかどうかを定義し、実際の実装を提供する必要があります。
次に、そのインターフェースのいくつかの実装を提供できます。
final class ArrayParser implements ParserInterface
public function canParse(mixed $input): bool
return is_array($input);
public function parse(mixed $input): mixed
return json_encode($input, JSON_PRETTY_PRINT);
final class JsonParser implements ParserInterface
public function canParse(mixed $input): bool
return
is_string($input)
&& str_starts_with(trim($input), '')
&& str_ends_with(trim($input), '');
public function parse(mixed $input): mixed
return json_encode(
json_decode($input),
JSON_PRETTY_PRINT
);
final class XmlParser implements ParserInterface
public function canParse(mixed $input): bool
return
is_string($input)
&& str_starts_with(trim($input), '<')
&& str_ends_with(trim($input), '>');
public function parse(mixed $input): mixed
return json_encode(
simplexml_load_string(
$input,
"SimpleXMLElement",
LIBXML_NOCDATA
),
JSON_PRETTY_PRINT
);
完全な開示: これらは非常に単純な実装です。 での戦略検出 canParse メソッドは単に入力文字列の最初と最後の文字を調べるだけで、おそらく確実ではありません。 また、XML デコードが正しく機能しません。 しかし、この例のためには十分です。
次のステップは、開発者がパブリック API として使用できるクラスを提供することです。これは、下でさまざまな戦略を使用します。 一連の戦略実装を追加することによって構成され、1 つを公開します。 parse 外部へのメソッド:
final class Parser
private array $parsers = [];
public function __construct()
$this
->addParser(new ArrayParser)
->addParser(new JsonParser)
->addParser(new XmlParser);
public function addParser(ParserInterface $parser): self
$this->parsers[] = $parser;
return $this;
public function parse(mixed $input): mixed
foreach ($this->parsers as $parser)
if ($parser->canParse($input))
return $parser->parse($input);
throw new Exception("Could not parse given input");
これで終わりですよね? ユーザーは今私たちを使用することができます Parser そのようです:
$parser = new Parser();
$parser->parse('"title":"test"');
$parser->parse('<title>test</title>');
$parser->parse(['title' => 'test']);
出力は常にかなりの JSON 文字列になります。
さて… 反対側から見てみましょう: 既存のパーサーを独自の機能で拡張したい開発者: Request クラスを JSON 文字列に変換します。 この正確な理由から、戦略パターンを使用してパーサーを設計しました。 だから、十分に簡単です:
final class RequestParser implements ParserInterface
public function canParse(mixed $input): bool
return $input instanceof Request;
public function parse(mixed $input): mixed
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
そして、パーサーが IoC コンテナーのどこかに登録されていると仮定しましょう。次のように追加できます。
Container::singleton(
Parser::class,
fn () => (new Parser)->addParser(new RequestParser);
);
これで完了です。
ただし… 1つの問題を見つけましたか? 以前にこの方法で戦略パターンを使用したことがある場合 (多くのオープン ソース パッケージが適用されています)、既にアイデアがあるかもしれません。
それは私たちの中にあります RequestParser::parse 方法:
public function parse(mixed $input): mixed
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
ここでの問題は、実際の型についての手がかりがないことです。 $input. 私たちはそれが Request チェックインのためオブジェクト canParse、しかしもちろん私たちのIDEはそれを知りません。 そのため、docblock を提供することで、少し手助けする必要があります。
public function parse(mixed $input): mixed
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
または、 instanceof 再び確かめる:
public function parse(mixed $input): mixed
if (! $input instanceof Request)
return json_encode([
'method' => $input->method,
'headers' => $input->headers,
'body' => $input->body,
], JSON_PRETTY_PRINT);
私たちがどのように設計したかにより、 ParserInterface、それを実装したい開発者は、2 つの作業を行う必要があります。
final class RequestParser implements ParserInterface
public function canParse(mixed $input): bool
return $input instanceof Request;
public function parse(mixed $input): mixed
if (! $input instanceof Request)
この種のコードの重複は世界の終わりではありません。せいぜいマイナーな不便です。 ほとんどの開発者は目をつぶることさえありません。
でもやるよ。 パッケージのメンテナーとして、パブリック API をできるだけ直感的で摩擦のないものにしたいと考えています。 私にとって、これは静的な洞察が開発者エクスペリエンスの重要な部分であることを意味し、このパーサーをどのように設計したかによってコードのユーザーが妨げられることを望んでいません。
それでは、この問題を解決するためのいくつかの方法について説明しましょう。
# もう重複しない
分割したために重複の問題が発生した場合 canParse と parse おそらく最も簡単な解決策は、単純に…分割しないことですか?
明示的な条件を使用する代わりに、解析できない場合に例外をスローするような方法で戦略クラスを設計するとどうなるでしょうか?
interface ParserInterface
public function parse(mixed $input): mixed;
final class RequestParser implements ParserInterface
public function parse(mixed $input): mixed
if (! $input instanceof Request)
throw new CannotParse;
ジェネリック パーサー クラスは次のように変更されます。
final class Parser
public function parse(mixed $input): mixed
foreach ($this->parsers as $parser)
try
return $parser->parse($input);
catch (ParseException)
continue;
throw new Exception("Could not parse given input");
もちろん、今は「例外とは何か」といううさぎの穴を開いています。例外を使用して、このようにプログラム フローを制御することが許可されているかどうかもわかりません。 私の個人的な意見は「はい、間違いなく」です。 でのみ機能するメソッドに文字列を渡すためです。 Request オブジェクトは実際には 例外 ルールに。 少なくとも、それが私の定義です。
一部の人々は戻ることを選択するかもしれません null 例外をスローする代わりに、それは私にはもっと間違っているように感じますが: null この特定のメソッドが入力を処理できなかったことを伝えません。 実際には、 null 要件によっては、このパーサーからの有効な結果になる可能性が非常に高くなります。 いいえ、いいえ null 私のため。
しかし、私はこれを読んだときにおそらく何人かの人々が持っている意見を共有しています: null または、例外をスローすることは、最もクリーンなソリューションとは思えません。 ほんの一握りの開発者だけが気にするかもしれない詳細を修正するという唯一の目的でこの旅に乗り出す場合は、他のオプションも検討し、ウサギの穴にさらに深く潜り込むかもしれません.
# 種類
無効な入力を防ぐために、この手動チェックを作成しました。 $input instanceof Request; しかし、PHP がこれらの種類のチェックを自動化する方法があることをご存知でしたか? その組み込み型システム! PHP が舞台裏で私たちのためにできることを、わざわざ手動で書き直す必要があるのでしょうか? 単にヒントを入力してみませんか Request?
final class RequestParser implements ParserInterface
public function parse(Request $input): mixed
2 つの問題があるため、できません。
- からパラメーターの型を絞り込むことは許可されていません
mixedにRequestPHP と私たちのParserInterface; と - すべての入力を専用の型として表現できるわけではありません。XML と JSON はどちらも文字列であり、あいまいさがあります。
それで、話の終わり? うーん… 私たちはすでにうさぎの穴の奥深くにいるので、試してみるのもいいかもしれません。
前述の 2 つの問題が問題ではないことを想像することから始めましょう: 実際に、各戦略の受け入れられた入力を検出し、その情報に基づいて適切な戦略を選択できるようにパーサーを設計できますか?
きっとできます! 最も簡単な解決策は、すべての戦略をループし、入力を渡してみて、処理できない場合は続行することです。 残りは PHP の型システムに任せます。
final class Parser
public function handle(mixed $input): mixed
foreach ($this->parsers as $parser)
try
return $parser->parse($input);
catch (TypeError)
continue;
throw new Exception("Could not parse given input");
私は実際、どのメソッドがどの入力を受け入れることができるかを判断しようとする、あらゆる種類のランタイム リフレクションよりもこのアプローチを好みます。 実行時に PHP の型チェッカーを再作成しようとしないでください. このアプローチが機能するための唯一の実際の要件は、戦略メソッドに副作用がなく、常に適切に入力のヒントを入力することです。 これは、プログラミングにおける私の個人的な土台の 1 つです。したがって、この原則を前提としたコードを書くことに何の問題もありません。
わかりましたので、メソッド シグネチャに基づいて、任意の入力を正しい戦略に一致させることができます。 しかし、まだ最初の 2 つの問題に対処する必要があります。
最初のものは、これを書くことが許可されていないということです:
final class RequestParser implements ParserInterface
public function parse(Request $input): mixed
の署名を定義したため、 parse 私たちの中で ParserInterface そのようです:
interface ParserInterface
public function parse(mixed $input): mixed;
パラメータの型を絞り込むことはできません。広げることしかできません。 それは反変性と呼ばれます。
一方で、私たちの戦略が取ることができると言うインターフェースがあります どれか 入力のタイプ (mixed); しかし一方で、私たちは戦略クラスを持っています。 明確な 入力の種類。
もしも うさぎの穴をさらに掘り下げたい場合は、インターフェースが実際に真実を伝えていないという結論以外に結論はありません。 どれか 一種の入力なので、インターフェイスにそれを教えてもらうのは意味がありません。 このインターフェイスは本質的に嘘をついているので、それを維持する理由はありません。
まあ、実際には:そこに は このインターフェイスを使用する理由: ドキュメントに依存することなく、独自の戦略を追加する方法を開発者が理解できるようにします。 開発者がこのメソッド シグネチャを見ると、次のようになります。
final class Parser
public function addParser(ParserInterface $parser): self
$this->parsers[] = $parser;
return $this;
実装する必要があることは明らかです ParserInterface カスタム戦略が機能するために。 したがって、このインターフェースをなくすことは、良いことよりも害を及ぼす可能性があると言えます。
がある 1 私が考えることができる解決策は、この問題に対抗することができます: callables を受け入れることです。
public function addParser(callable $parser): self
$this->parsers[] = $parser;
return $this;
callable です 特別な 関数とクロージャーだけでなく、呼び出し可能なオブジェクトもカバーするため、PHP で入力します。 ここで唯一欠けているのは、callable がどのように見えるべきかを、コードから (確実に) 判断できないことです。
処理できるあらゆる種類の入力を受け入れる必要があるという規則を確立しましたが、追加の docblock を提供せずに、コードを拡張する開発者にそれを伝える方法はありません。 これは間違いなくこのアプローチの欠点であり、このアプローチを採用しない十分な理由になるかもしれません。
個人的には気にしません。最初にあったコードの重複と手動の型検証は、docblock を読まなければならないことよりも私を悩ませていると思います:
public function addParser(callable $parser): self
$this->parsers[] = $parser;
return $this;
次に、2 つ目の問題があります。すべてを型で表現できるわけではありません。 例: JSON パーサーと XML パーサーの両方が一致する必要があります。 string JSON または XML のいずれかであり、それらをヒントとして入力することはできません。 私は2つの解決策を考えることができます。
- でいくつかの手動チェックを行います
parseこれらのエッジケースのメソッド、およびスローTypeErrorそれらが一致しない場合。 また - 導入
JsonStringとXmlStringカスタムクラスとして、ファクトリに最初にそれらの生の文字列を適切な型に変換させます。
最初のオプションは次のようになります。
final class JsonParser
public function __invoke(string $input): string
if (
! str_starts_with(trim($input), '')
)
throw new TypeError("Not a valid JSON string");
return json_encode(
json_decode($input),
JSON_PRETTY_PRINT
);
final class XmlParser
public function __invoke(string $input): string
if (
! str_starts_with(trim($input), '<')
2つ目は、カスタムクラスを持っています JsonString と XmlString、次のようになります。
final class JsonParser
public function __invoke(JsonString $input): string
return json_encode(
json_decode($input),
JSON_PRETTY_PRINT
);
final class XmlParser
public function __invoke(XmlString $input): string
return json_encode(
simplexml_load_string(
$input,
"SimpleXMLElement",
LIBXML_NOCDATA
),
JSON_PRETTY_PRINT
);
ただし、文字列を適切な型に変換するためのファクトリも導入する必要があることを忘れないでください。これは、かなりのオーバーヘッドを意味します。
最後に、 callable 別の利点があります。ユーザーは、呼び出し可能なクラスを使用することに縛られていません。 ニーズとテスト方法によっては、クロージャを追加するだけで済みます。
Container::singleton(
Parser::class,
fn () => (new Parser)->addParser(
fn (Request $request) => json_encode([
'method' => $request->method,
'headers' => $request->headers,
'body' => $request->body,
], JSON_PRETTY_PRINT)
);
);
このアプローチの欠点はありますか? 絶対。 コードの重複が多かった元のソリューションにもマイナス面があるのと同じように。 個人的には、開発者の経験の観点から、次のように考えています。 動的戦略を実装する元の方法に代わるものを検討する価値があります。 そして、その恩恵を受けるいくつかのプロジェクトを想像することができます。
どう思いますか? 経由でお知らせください ツイッター または電子メール; あなたがそう思うなら、私はゆっくりと夢中になっていると言うのをためらわないでください!
//platform.twitter.com/widgets.js