訪問者パターンに関するいくつかの考え

最近、私はよく知られている「Visitor」パターン(Visitor、以下、Visitorと呼びます)を頻繁に使用する必要があります。 以前、私はそれを無視し、松葉杖、コードの余分な複雑さだと考えていました。 この記事では、何が良いのか、私の意見では、このパターンの何が悪いのか、どのタスクを解決するのに役立つのか、その使用を単純化する方法についての考えを共有します。 コードはC#になります。 興味があれば-猫の下でお願いします。











これは何ですか



まず、これがどのようなパターンで、どのような用途に使用されているかを少し思い出しましょう。 それに慣れている人は斜めに見ることができます。 幾何学的形状の階層を持つライブラリがあるとしましょう。







次に、面積の計算方法を学ぶ必要があります。 どうやって? はい、問題ありません。 メソッドをIFigureに追加して実装します。 現在、ライブラリがアルゴリズムのライブラリに依存していることを除いて、すべてが素晴らしいです。



次に、コンソールに各図の説明を表示する必要がありました。 そして、図形を描画します。 適切なメソッドを追加することにより、ライブラリを膨らませ、同時にSRPとOCPに重大な違反を犯します。



どうする? もちろん、別のライブラリで、必要な問題を解決するクラスを作成します。 彼らはどの特定の数字が彼らに与えられたかをどのようにして知るのでしょうか? 型キャスト!



public void Draw(IFigure figure) { if (figure is Rectangle) { /////// return; } if (figure is Triangle) { /////// return; } if (figure is Triangle) { /////// return; } }
      
      





間違いを見ましたか? そして、私は彼女が実行時にのみ気づいた。 ダウンキャスティングは、広く認識されている悪い形式、LSPに違反する方法などです。...型システムが「箱から出して」問題を解決する言語がありますが(マルチメソッドを参照)、C#はそれらに適用されません。



ビジター別名ビジターが救助に来ます。 一番下の行はこれです:抽象化の特定の実装のそれぞれを操作するためのメソッドを含むビジタークラスがあります。 そして、各具体的な実装には、1つのことを行うメソッドが含まれています。それは、ビジターの対応するメソッドに渡されます。







少しわかりにくいですね。 一般に、訪問者の主な欠点の1つは、誰もがすぐに入場しないことです(私は自分で判断します)。 すなわち その使用は、システムの複雑さのしきい値をわずかに増加させます。



どうしたの? ご覧のとおり、すべてのロジックは私たちの幾何学的な形の外、そして訪問者の中にあります。 実行時の型キャストなし-各図の方法の選択は、コンパイル時に決定されます。 少し前に遭遇した問題を回避することができました。 すべてが素晴らしいように思えますか? もちろん違います。 欠点がありますが、それらについて-最後に。



調理オプション



VisitおよびAcceptVisitorメソッドはどのタイプの値を返す必要がありますか? クラシックバージョンでは、それらは無効です。 面積を計算する場合はどうすればよいですか? ビジターでプロパティを作成して値を割り当てることができ、Visitを呼び出した後に読み取ります。 ただし、AcceptVisitorメソッドがすぐに結果を返す方がはるかに便利です。 私たちの場合、結果の型はdoubleですが、これが常に当てはまるわけではないことは明らかです。 ビジターとAcceptVisitorメソッドをジェネリックにします。



 public interface IFigure { T AcceptVisitor<T>(IFiguresVisitor<T> visitor); } public interface IFiguresVisitor<out T> { T Visit(Rectangle rectangle); T Visit(Triangle triangle); T Visit(Circle circle); }
      
      





このようなインターフェイスは、すべての場合に使用できます。 非同期操作の場合、結果のタイプはタスクです。 何も返す必要がない場合、戻り値の型は、関数型言語ではユニットと呼ばれるダミー型にすることができます。 C#では、Reactive Extensionsなどの一部のライブラリでも定義されています。



オブジェクトの種類に応じて、プログラムの1か所でのみ簡単なアクションを実行する必要がある場合があります。 たとえば、実際には、テスト例を除き、どこにでも図の名前を表示する必要はほとんどありません。 または、一部の単体テストでは、形状が円形または長方形であることを判断する必要があります。 さて、そのような原始的なケースごとに、新しいエンティティを作成するために-専門の訪問者? 別の方法で行うことができます:



 public class FiguresVisitor<T> : IFiguresVisitor<T> { private readonly Func<Circle, T> _ifCircle; private readonly Func<Rectangle, T> _ifRectangle; private readonly Func<Triangle, T> _ifTriangle; public FiguresVisitor(Func<Rectangle, T> ifRectangle, Func<Triangle, T> ifTrian-gle, Func<Circle, T> ifCircle) { _ifRectangle = ifRectangle; _ifTriangle = ifTriangle; _ifCircle = ifCircle; } public T Visit(Rectangle rectangle) => _ifRectangle(rectangle); public T Visit(Triangle triangle) => _ifTriangle(triangle); public T Visit(Circle circle) => _ifCircle(circle); }
      
      





 public double CalcArea(IFigure figure) { var visitor = new FiguresVisitor<double>( r => r.Height * r.Width, t => { var p = (tA + tB + tC) / 2; return Math.Sqrt(p * (p - tA) * (p - tB) * (p - tC)); }, c => Math.PI * c.Radius * c.Radius); return figure.AcceptVisitor(visitor); }
      
      





ご覧のとおり、パターンマッチングに似たものが見つかりました。 C#7に追加されたものではなく、実際には単なる粉のダウンキャストですが、コンパイラによって入力および制御されます。



しかし、12個の数字があり、1つまたは2つ、そして残りの部分に対して特別な何かを実行する必要がある場合はどうでしょうか。 ダースの同一の式をコンストラクターにコピーするのは面倒でandいです。 この構文はどうですか?



 string description = figure .IfRectangle(r => $"Rectangle with area={r.Height * r.Width}") .Else(() => "Not rectangle");
      
      





 bool isCircle = figure .IfCircle(_=>true) .Else(() => false);
      
      





最後の例では、「is」演算子の真のアナログを取得しました! 他のすべてのソースと同様に、図のセットに対するこのファクトリーの実装はgithubにあります。 質問が請う-それぞれの場合、この定型句を書くのは何ですか? はい または、T4とRoslynを使用して、コードジェネレーターを作成できます。 記事が公開されるまでにこれを行うことを計画していましたが、十分な時間がありませんでした。



短所



もちろん、訪問者には十分な欠点と制限があります。 少なくともIFifgureのAcceptVisitorメソッドを使用します。 ジオメトリとは何の関係がありますか? はい、いいえ。 この場合も、SRPに違反しています。



次に、もう一度図を見てください。







誰もが誰もが知っている閉じたシステムがあります。 各タイプの階層はビジターについて知っています-ビジターはすべてのタイプについて知っています-したがって、各タイプは他のすべてについて推移的に知っています! 新しいタイプ(この例では形状)を追加すると、実際にはすべての人に影響します。 また、これは前述のオープンクローズ原則の直接違反です。 コードを変更できる場合、大きなプラスもあります。新しい数字を追加すると、コンパイラは訪問者のインターフェイスとその実装に適切なメソッドを追加するように強制します。何も忘れません。 しかし、著者ではなくライブラリユーザーのみであり、階層を変更できない場合はどうでしょうか。 まさか。 訪問者の構造を他の誰かと拡張することはできません。 パターンのすべての定義において、確立された階層の存在下で使用されることは、何の理由もありません。 したがって、幾何学的形状の拡張可能なライブラリを設計する場合、ビジターを使用することはできません。



合計



Visitorパターンは、コードを変更できる場合に非常に便利です。 ダウンキャストから逃れることができ、その「非拡張性」により、コンパイラは新しく追加されたすべての型のすべてのハンドラーを確実に追加できます。



新しいタイプを追加して拡張できるライブラリを作成している場合、ビジターは使用できません。 それで何? はい、同じダウンキャスティングがC#7のパターンマッチングにまとめられています。または、もっと面白いものを思いつきます。 うまくいけば、それについて書きます。



そして、もちろん、私はコメントで意見やアイデアを読んでうれしいです。

ご清聴ありがとうございました!



All Articles