30

tl; dr

ジェネリックスをサポートしない言語( PHP )で、特殊化のためのパラメーター型の不変性を克服するためにどのような戦略が存在しますか?

注:型理論/安全性/分散などについての私の理解がより完全であったと言えるといいのですが。私はCS専攻ではありません。


状況

Consumer拡張したい抽象クラスがあります。定義が必要なConsumer抽象メソッドを宣言します。consume(Argument $argument)問題はないはずです。


問題

Consumerと呼ばれるあなたの専門家は、あらゆるタイプのをSpecializedConsumer扱う論理的なビジネスを持っていません。代わりに、 (およびそのサブクラス)を受け入れる必要があります。メソッドのシグネチャがに変わります。ArgumentSpecializedArgumentconsume(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の違反を克服するために、どのようなパターンまたは戦略が存在しますが、それでもとの型の関係を維持SpecializedConsumerConsumerますか?

答えを「やっかいなことだ、画板に戻す」にまで絞り込めることは、まったく問題ないと思います。


考慮事項、詳細、およびエラッタ


<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、それぞれにオブジェクトのみが含まれます。各()をブール値に評価します。それぞれが合格した場合、は合格し、それはに追加され、評価のために子に降りる()、または単にオブジェクトのとオブジェクトを返します。NodeOuterNodeRuleNode::evaluate()RuleNode::evaluateEachRule()trueRuleNodeCommandWrapperOuterNode::evaluateEachNode()trueInnerNodeOuterNode

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は、期待されるユーザーランドを受け入れますAdapterAdapterリクエストに関する情報が必要な場合は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>

4

2 に答える 2

13

この質問に正しく答えるために、私たちは本当に一歩下がって、あなたが解決しようとしている問題をより一般的な方法で見なければなりません(そしてあなたの質問はすでにかなり一般的でした)。

本当の問題

本当の問題は、継承を使用してビジネスロジックの問題を解決しようとしていることです。LSP違反があり、さらに重要なことに、ビジネスロジックをアプリケーションの構造に緊密に結合しているため、これは機能しません。

したがって、継承はこの問題を解決する方法として出ています(上記および質問で述べた理由のため)。幸いなことに、使用できる構成パターンは多数あります。

さて、あなたの質問がどれほど一般的であるかを考えると、あなたの問題に対する確実な解決策を特定することは非常に難しいでしょう。それでは、いくつかのパターンを調べて、それらがこの問題をどのように解決できるかを見てみましょう。

ストラテジー

戦略パターンは、私が最初に質問を読んだときに最初に頭に浮かんだものです。基本的に、実装の詳細と実行の詳細を分離します。これにより、さまざまな「戦略」が存在できるようになり、呼び出し元は特定の問題に対してどれをロードするかを決定します。

ここでの欠点は、発信者が正しい戦略を選択するために戦略について知っている必要があることです。しかし、それはまた、異なる戦略をより明確に区別することを可能にするので、それはまともな選択です...

指示

コマンドパターンも、ストラテジーと同じように実装を切り離します。主な違いは、ストラテジーでは、発信者がコンシューマーを選択することです。コマンドでは、それは他の誰か(おそらく工場またはディスパッチャー)です...

各「専門消費者」は、特定のタイプの問題のロジックのみを実装します。その後、他の誰かが適切な選択をします。

責任の連鎖

適用される可能性のある次のパターンは、Chain ofResponsibilityPatternです。これは、消費者がどちらを呼び出すかを決定する代わりに、要求を処理するまで各戦略が順番に呼び出されることを除いて、上記の戦略パターンに似ています。したがって、あなたの例では、より一般的な議論を取りますが、それが特定の議論であるかどうかを確認してください。そうである場合は、リクエストを処理します。それ以外の場合は、次の人に試してもらいましょう...

ここでもブリッジパターンが適切な場合があります。これはある意味でストラテジーパターンに似ていますが、ブリッジの実装が実行時ではなく構築時にストラテジーを選択するという点で異なります。したがって、実装ごとに異なる「コンシューマー」を構築し、詳細を依存関係として内部で構成します。

ビジターパターン

あなたはあなたの質問でビジターパターンについて言及したので、私はそれをここで言及すると思います。訪問者は構造を横断するように設計された戦略パターンに非常に似ているため、このコンテキストで適切かどうかはわかりません。トラバースするデータ構造がない場合、ビジターパターンは、戦略パターンにかなり似たものになるように抽出されます。制御の方向が違うので公平に言いますが、最終的な関係はほとんど同じです。

その他のパタ​​ーン

結局、それはあなたが解決しようとしている具体的な問題に本当に依存します。各「コンシューマー」が異なるリクエストタイプ(XML、HTML、JSONなど)を処理するHTTPリクエストを処理しようとしている場合、最適な選択は、の幾何学的領域の検索を処理しようとしている場合とは大きく異なる可能性があります。ポリゴン。もちろん、両方に同じパターンを使用することもできますが、実際には同じ問題ではありません。

そうは言っても、この問題メディエーターパターン(複数の「コンシューマー」がデータを処理する機会が必要な場合)、ステートパターン(「コンシューマー」が過去の消費データに依存する場合)でも解決できます。または、アダプタパターン(特殊なコンシューマで別のサブシステムを抽象化する場合)...

要するに、答えるのは難しい問題です。なぜなら、解決策がたくさんあるので、どれが正しいかを言うのは難しいからです...

于 2012-07-16T00:44:38.027 に答える
5

私が知っている唯一の戦略はDIY戦略です。Argument関数定義で単純なものを受け入れ、それが十分に専門化されているかどうかをすぐに確認します。

class SpecializedConsumer extends Consumer {
    public function consume(Argument $argument) {
        if(!($argument instanceof SpecializedArgument)) {
            throw new InvalidArgumentException('Argument was not specialized.');
        }
        // move on
    }
}
于 2012-07-15T18:07:54.590 に答える