tl; dr
ジェネリックスをサポートしない言語( PHP )で、特殊化のためのパラメーター型の不変性を克服するためにどのような戦略が存在しますか?
注:型理論/安全性/分散などについての私の理解がより完全であったと言えるといいのですが。私はCS専攻ではありません。
状況
Consumer
拡張したい抽象クラスがあります。定義が必要なConsumer
抽象メソッドを宣言します。consume(Argument $argument)
問題はないはずです。
問題
Consumer
と呼ばれるあなたの専門家は、あらゆるタイプのをSpecializedConsumer
扱う論理的なビジネスを持っていません。代わりに、 (およびそのサブクラス)を受け入れる必要があります。メソッドのシグネチャがに変わります。Argument
SpecializedArgument
consume(SpecializedArgument $argument)
abstract class Argument { }
class SpecializedArgument extends Argument { }
abstract class Consumer {
abstract public function consume(Argument $argument);
}
class SpecializedConsumer extends Consumer {
public function consume(SpecializedArgument $argument) {
// i dun goofed.
}
}
リスコフの置換原則を破り、型安全性の問題を引き起こしています。うんこ。
質問
さて、これはうまくいきません。しかし、この状況を考えると、型安全性の問題とLSPの違反を克服するために、どのようなパターンまたは戦略が存在しますが、それでもとの型の関係を維持SpecializedConsumer
しConsumer
ますか?
答えを「やっかいなことだ、画板に戻す」にまで絞り込めることは、まったく問題ないと思います。
考慮事項、詳細、およびエラッタ
了解しました。当面の解決策は、 「でメソッドを定義しない
consume()
Consumer
でください」と表示されます。わかりました、それは理にかなっています。なぜなら、メソッド宣言は署名と同じくらい良いからです。意味的にはconsume()
、パラメータリストが不明な場合でも、がないことは私の脳を少し傷つけます。おそらくもっと良い方法があります。私が読んでいることから、パラメーター型の共分散をサポートしている言語はほとんどありません。PHPはその1つであり、ここでは実装言語です。さらに複雑なことに、ジェネリックスを含む創造的な「ソリューション」を見てきました。PHPでサポートされていない別の機能。
Wikiの分散(コンピューターサイエンス)から-共変引数タイプが必要ですか?:
これにより、実際の要件をモデル化するために引数の型が共変である必要がある状況で問題が発生します。人を表すクラスがあるとします。人は医者に診てもらうことができるので、このクラスにはメソッドvirtualvoidがあるかもしれません
Person::see(Doctor d)
。Person
ここで、クラスのサブクラスを作成するとしますChild
。つまり、aChild
は人です。次に、、のサブクラスを作成したい場合がDoctor
ありPediatrician
ます。子供が小児科医のみを訪問する場合は、型システムでそれを実施したいと思います。ただし、単純な実装は失敗します。aChild
はであるためPerson
、だけでなく、Child::see(d)
任意のをとる必要があります。Doctor
Pediatrician
記事は続けて言う:
この場合、ビジターパターンを使用してこの関係を強化できます。C ++で問題を解決する別の方法は、ジェネリックプログラミングを使用することです。
繰り返しますが、ジェネリックは問題を解決するために創造的に使用することができます。とにかく中途半端な実装をしているので、ビジターパターンを調べていますが、記事で説明されているほとんどの実装は、PHPでサポートされていないもう1つの機能であるメソッドのオーバーロードを利用しています。
<too-much-information>
実装
最近の議論のために、私が含めることを怠った特定の実装の詳細を拡張します(のように、私はおそらくあまりにも多くを含めます)。
簡潔にするために、目的が十分に明確である(明確である必要がある)メソッド本体は除外しました。私はこれを簡潔にしようとしましたが、私は言葉になりがちです。コードの壁を捨てたくなかったので、説明はコードブロックの後に/先行します。編集権限があり、これをクリーンアップしたい場合は、実行してください。また、コードブロックはプロジェクトからのコピーパスタではありません。何かが意味をなさない場合、それは意味をなさないかもしれません。明確にするために私に怒鳴ります。
元の質問に関して、以降、Rule
クラスはでConsumer
あり、Adapter
クラスはArgument
です。
ツリー関連のクラスは次のように構成されています。
abstract class Rule {
abstract public function evaluate(Adapter $adapter);
abstract public function getAdapter(Wrapper $wrapper);
}
abstract class Node {
protected $rules = [];
protected $command;
public function __construct(array $rules, $command) {
$this->addEachRule($rules);
}
public function addRule(Rule $rule) { }
public function addEachRule(array $rules) { }
public function setCommand(Command $command) { }
public function evaluateEachRule(Wrapper $wrapper) {
// see below
}
abstract public function evaluate(Wrapper $wrapper);
}
class InnerNode extends Node {
protected $nodes = [];
public function __construct(array $rules, $command, array $nodes) {
parent::__construct($rules, $command);
$this->addEachNode($nodes);
}
public function addNode(Node $node) { }
public function addEachNode(array $nodes) { }
public function evaluateEachNode(Wrapper $wrapper) {
// see below
}
public function evaluate(Wrapper $wrapper) {
// see below
}
}
class OuterNode extends Node {
public function evaluate(Wrapper $wrapper) {
// see below
}
}
つまり、それぞれにオブジェクトInnerNode
が含まれRule
、それぞれにオブジェクトのみが含まれます。各()をブール値に評価します。それぞれが合格した場合、は合格し、それはに追加され、評価のために子に降りる()、または単にオブジェクトのとオブジェクトを返します。Node
OuterNode
Rule
Node::evaluate()
Rule
Node::evaluateEachRule()
true
Rule
Node
Command
Wrapper
OuterNode::evaluateEachNode()
true
InnerNode
OuterNode
はWrapper
; オブジェクトはオブジェクトをWrapper
プロキシし、Request
オブジェクトのコレクションを持っていAdapter
ます。Request
オブジェクトはHTTPリクエストの表現です。オブジェクトは、特定のオブジェクトで特定の用途に使用するためAdapter
の特殊なインターフェイスです(特定の状態を維持しRule
ます) 。(ここでLSPの問題が発生します)
Command
オブジェクトはアクション(実際にはきちんとパッケージ化されたコールバック)であり、オブジェクトに追加されます。Wrapper
すべてが言われ、実行Command
されると、オブジェクトの配列が順番に起動され、Request
(特に)を渡します。
class Request {
// all teh codez for HTTP stuffs
}
class Wrapper {
protected $request;
protected $commands = [];
protected $adapters = [];
public function __construct(Request $request) {
$this->request = $request;
}
public function addCommand(Command $command) { }
public function getEachCommand() { }
public function adapt(Rule $rule) {
$type = get_class($rule);
return isset($this->adapters[$type])
? $this->adapters[$type]
: $this->adapters[$type] = $rule->getAdapter($this);
}
public function commit(){
foreach($this->adapters as $adapter) {
$adapter->commit($this->request);
}
}
}
abstract class Adapter {
protected $wrapper;
public function __construct(Wrapper $wrapper) {
$this->wrapper = $wrapper;
}
abstract public function commit(Request $request);
}
したがって、特定のユーザーランドRule
は、期待されるユーザーランドを受け入れますAdapter
。Adapter
リクエストに関する情報が必要な場合はWrapper
、元のの整合性を維持するために、を介してルーティングされRequest
ます。
Wrapper
オブジェクトを集約すると、Adapter
既存のインスタンスが後続のオブジェクトに渡されるRule
ため、の状態は次のオブジェクトにAdapter
保持されRule
ます。ツリー全体が通過すると、Wrapper::commit()
が呼び出され、集約された各Adapter
オブジェクトは、必要に応じて元のオブジェクトに対してその状態を適用しますRequest
。
Command
次に、オブジェクトの配列と変更されたが残りますRequest
。
一体何がポイントですか?
ええと、私は多くのPHPフレームワーク/アプリケーションで一般的な典型的な「ルーティングテーブル」を再現したくなかったので、代わりに「ルーティングツリー」を使用しました。AuthRule
任意のルールを許可することで、 (たとえば)をすばやく作成してに追加できNode
、。を渡さずにブランチ全体にアクセスすることはできなくなりAuthRule
ます。理論的には(私の頭の中では)、それは魔法のユニコーンのようなものであり、コードの重複を防ぎ、ゾーン/モジュールの編成を強制します。実際には、私は混乱して怖がっています。
なぜ私はこのナンセンスの壁を去ったのですか?
さて、これは私がLSPの問題を修正する必要がある実装です。それぞれRule
がに対応し、Adapter
それは良くありません。ツリーを構築する際の型の安全性などを確保するために、それぞれの関係を維持したいのですが、サブタイプのシグネチャが変更されるため、抽象でRule
キーメソッド()を宣言できません。evaluate()
Rule
別の注意点として、私はAdapter
作成/管理スキームの整理に取り組んでいます。Rule
それを作成するのはの責任であるかどうかなど。
</too-much-information>