デザインパターン|ビジターパターンからパターンマッチングまで

序文


ソフトウェア開発の分野では、発生する問題は毎回異なる可能性があります。eコマースビジネスに関連するもの、基盤となるデータ構造に関連するもの、パフォーマンスの最適化に重点を置くものがあります。ただし、コードレベルで問題を解決するためのアプローチには、いくつかの共通点があります。誰かがこれらの共通点を要約しましたか?

もちろんあります。1994年、Erich Gamma、Richard Helm、Ralph Johnson、John Vlissidesが共同で、業界で非常に重要な本「デザインパターン:再利用可能なオブジェクト指向ソフトウェアの要素」を発行しました。この本は人々を開発分野に導きます。さまざまな問題の共通性について一連の抽象化が行われ、23の非常に古典的なデザインパターンが形成されました。多くの問題は、これら23の設計パターンの1つ以上に抽象化できます。デザインパターンは非常に用途が広いため、開発者が使用するユニバーサル言語にもなり、デザインパターンに抽象化されたコードは、理解と保守が容易になりました。

全体として、デザインパターンは3つのカテゴリに分類されます。


1.作成パターン:オブジェクトの作成と再利用に関連する設計パターン
2.構造パターン:オブジェクトの結合と構築に関連する設計パターン
3.動作パターン:オブジェクト間の動作に関連する設計パターン

この記事で説明するデザインパターンは、ビジターパターンです。これは、動作パターンの一種であり、同様の動作を持つオブジェクトをどのように組み合わせて展開するかという問題を解決するために使用されます。具体的には、ビジターパターンの使用シナリオ、メリットとデメリット、ビジターパターンに関連するダブルディスパッチテクノロジーを紹介します。また、記事の最後では、Java 14でリリースされたばかりのパターンマッチングを使用して、以前のビジターパターンで解決された問題を解決する方法について説明します。

問題


マッププログラムがあるとすると、以下に示すように、建物(Building)、工場(Factory)、学校(School)など、マップ上に多くのノードがあります。

interface Node {
    String getName();
    String getDescription();
    // 其余的方法这里忽略......
}


class Building implements Node {
    ...
}


class Factory implements Node {
    ...
}


class School implements Node {
    ...
}

新しい要件があります。ノードを描画する関数を追加する必要があります。考えてみれば、それは非常に簡単です。ノードにメソッドdraw()を追加すると、残りの実装クラスがこのメソッドを個別に実装します。しかし、これには問題があります。今回はdraw()メソッドを追加しました。次回はexportメソッドを追加しますか?さらに、インターフェースを再度変更する必要があります。コンポーネントを接続するブリッジとして、インターフェイスは可能な限り安定している必要があり、頻繁に変更しないでください。したがって、インターフェイスのスケーラビリティを可能な限り高くし、インターフェイスを頻繁に変更することなく、インターフェイスの機能範囲を最大化できるようにする必要があります。少し計量した後、次の解決策を思いつきました。

初期ソリューション


新しいクラスDrawServiceを定義し、その中にすべての描画ロジックを記述します。コードは次のとおりです。

public class DrawService {
    public void draw(Building building) {
        System.out.println("draw building");
    }
    public void draw(Factory factory) {
        System.out.println("draw factory");
    }
    public void draw(School school) {
        System.out.println("draw school");
    }
    public void draw(Node node) {
        System.out.println("draw node");
    }
}

これはクラス図です:

問題は今解決したと思うので、少しテストした後、仕事から家に帰ります。

public class App {
    private void draw(Node node) {
        DrawService drawService = new DrawService();
        drawService.draw(node);
    }


    public static void main(String[] args) {
        App app = new App();
        app.draw(new Factory());
    }
}

クリックして実行、出力:

draw node

これはどうですか?コードをもう一度詳しく見てみましょう。「Factoryオブジェクトを渡しました。drawfactoryを出力するはずです」。真剣に、あなたはいくつかの情報をチェックしに行きました、そしてあなたは理由を見つけました。

理由を説明する


その理由を理解するために、まずエディターの2つの可変型バインディングモードを理解します。

★  動的/遅延バインディング

このコードを見てみましょう

class NodeService {
    public String getName(Node node) {
        return node.getName();
    }
}

プログラムがNodeService :: getNameを実行するとき、対応する実装クラスのgetNameメソッドを呼び出せるように、パラメーターNodeのタイプ(Factory、School、Building)を判別する必要があります。プログラムはコンパイルフェーズ中にこの情報を取得できますか?当然のことながら、ノードの種類は動作環境によって変化する可能性があり、他のシステムから送信される可能性もあります。コンパイル段階でこの情報を取得することは不可能です。プログラムが実行できることは、最初にそれを開始し、getNameメソッドを実行するときに、ノードのタイプを調べてから、対応するタイプのgetName()実装を呼び出して結果を取得することです。(コンパイル時ではなく)実行時に呼び出すメソッドの決定は、動的/遅延バインディングと呼ばれます。

★  静的/早期バインディング

別のコードを見てみましょう

public void drawNode(Node node) {
    DrawService drawService = new DrawService();
    drawService.draw(node);
}

drawService.draw(node)を実行すると、コンパイラはノードのタイプを認識しますか?実行時に認識されている必要があるのに、なぜファクトリを渡すのに、ドローファクトリの代わりにドローノードを出力するのでしょうか。この問題は、プログラムの観点から考えることができます。DrawServiceには4つの描画メソッドしかなく、パラメーターの種類はFactory、Building、School、Nodeです。呼び出し元がCityを通過した場合はどうなりますか?結局のところ、呼び出し元は渡すためにCityクラスを実装できます。この場合、プログラムはどのメソッドを呼び出す必要がありますか?draw(City)メソッドはありません。これを防ぐために、プログラムはコンパイルフェーズでDrawService :: draw(Node)メソッドを使用することを直接選択します。呼び出し元から渡される実装に関係なく、DrawService :: draw(Node)メソッドを使用して、プログラムの安全な操作を保証します。(実行時ではなく)コンパイル時に呼び出すメソッドの決定は、静的/早期バインディングと呼ばれます。これは、描画ノードを出力する理由も説明しています。

最終的な解決策


これは、コンパイラが変数の型を知らないためであることがわかります。この場合、コンパイラにそれがどの型であるかを伝えることができます。これはできますか?もちろんこれは可能です。事前に変数タイプを確認します。

if (node instanceof Building) {
    Building building = (Building) node;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) node;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) node;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

このコードは実行可能ですが、作成するのが非常に面倒です。呼び出し元にノードタイプを決定させ、呼び出すメソッドを選択させる必要があります。より良い解決策はありますか?はい、それがビジターパターンです。ビジターパターンはダブルディスパッチと呼ばれるメソッドを使用します。このメソッドは、ルーティング作業を呼び出し元からそれぞれの実装クラスに転送できるため、クライアントはこれらの面倒な判断ロジックを記述する必要がありません。まず、実装されたコードがどのように見えるかを見てみましょう。

interface Visitor {
    void visit(Node node);
    void visit(Factory factory);
    void visit(Building building);
    void visit(School school);
}


class DrawVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("draw node");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("draw factory");
    }


    @Override
    public void visit(Building building) {
        System.out.println("draw building");
    }


    @Override
    public void visit(School school) {
        System.out.println("draw school");
    }
}


interface Node {
    ...
    void accpet(Visitor v);
}


class Factory implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Factory类型的,并且知道Visitor::visit(Factory)方法确实存在,
         * 因此会直接调用Visitor::visit(Factory)方法
         */
        v.visit(this);
    }
}


class Building implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Building类型的,并且知道Visitor::visit(Building)方法确实存在,
         * 因此会直接调用Visitor::visit(Building)方法
         */
        v.visit(this);
    }
}


class School implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是School类型的,并且知道Visitor::visit(School)方法确实存在,
         * 因此会直接调用Visitor::visit(School)方法
         */
        v.visit(this);
    }
}

発信者はこのように使用できます

Visitor drawVisitor = new DrawVisitor();
Factory factory = new Factory();
factory.accept(drawVisitor);

ビジターパターンは、実際には上記のifインスタンスをエレガントに実装しているため、呼び出し元のコードははるかにクリーンであり、全体的なクラス図は次のようになっていることがわかります。

なぜダブルディスパッチと呼ばれるのですか?


ビジターパターンがこの問題をどのように解決するかを理解した後、一部の学生は興味を持つかもしれませんが、ビジターパターンで使用されるテクノロジーがダブルディスパッチと呼ばれるのはなぜですか?ダブルディスパッチとは正確には何ですか?ダブルディスパッチを理解する前に、まずシングルディスパッチと呼ばれるものを理解しましょう

★  シングルディスパッチ

さまざまなランタイムクラスの実装に応じて、さまざまな呼び出し方法を選択します。これは、次のようなシングルディスパッチと呼ばれます。

String name = node.getName();

Factory :: getName、School :: getName、またはBuilding :: getNameを呼び出していますか?これは主に、ノードの実装クラスであるシングルディスパッチに依存します:ルーティングのレイヤー

★  ダブルディスパッチ

今見たビジターパターンコードを確認します

node.accept(drawVisitor);

ルーティングには2つの層があります。

  • acceptの特定の実装方法を選択します(Factory :: accept、School :: acceptまたはBuilding :: accept)

  • 特定の訪問方法を選択します(この例では、DrawVisit :: visitは1つだけです)

2つのルートを実行した後、対応するロジックが実行されます。これは、ダブルディスパッチと呼ばれます。



ビジターパターンの利点


1.ビジターパターンは、インターフェイスを頻繁に変更することなく、インターフェイスのスケーラビリティを可能な限り向上させることができます(1回だけ変更する必要があります:acceptメソッドを追加します)    

上記の描画例でも、新しい要件があり、ノード情報を表示する機能を追加する必要があるとします。もちろん、従来の方法では、ノードに新しいメソッドshowDetails()を追加しますが、インターフェイスを変更する必要はなく、新しいビジターを追加するだけで済みます。

class ShowDetailsVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("node details");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("factory details");
    }


    @Override
    public void visit(Building building) {
        System.out.println("building details");
    }


    @Override
    public void visit(School school) {
        System.out.println("school details");
    }
}


// 调用方这么使用
Visitor showDetailsVisitor = new ShowDetailsVisitor();
Factory factory = new Factory();
factory.accept(showDetailsVisitor); // factory details

この例から、Visitor Patternの一般的な使用シナリオを確認できます。これは、インターフェイスメソッドを頻繁に追加する必要があるシナリオでの使用に非常に適しています。たとえば、4つのクラスA、B、C、D、3つのメソッドx、y、z、水平描画メソッド、垂直描画クラスがあり、次の図を取得できます。

               x      y      z
    A       A::x   A::y   A::z
    B       B::x   B::y   B::z
    C       C::x   C::y   C::z

通常の状況では、テーブルは垂直方向に拡張されます。つまり、実装メソッドではなく実装クラスを追加することに慣れています。ビジターパターンは、水平方向の拡張という別のシナリオに適しています。実装クラスを追加するのではなく、インターフェイスメソッドを頻繁に追加する必要があります。ビジターパターンを使用すると、インターフェイスを頻繁に変更することなく、この目標を達成できます。

2.ビジターパターンは、複数の実装クラスで1つのロジックを簡単に共有できます

すべての実装メソッドは1つのクラス(DrawVisitorなど)で記述されるため、各インターフェイスの実装でこのロジックを繰り返し記述する代わりに、各タイプ(Factory / Building / Schoolなど)で同じロジックを簡単に使用できます。クラス。

ビジターパターンのデメリット


  • ビジターパターンは、ドメインモデルのカプセル化を破ります

通常の状況では、FactoryのロジックをFactoryクラスに記述しますが、Visitor Patternでは、Factoryロジックの一部(drawなど)を別のクラス(DrawVisitor)に移動する必要があります。ドメインモデルのロジックは2つに分散しています。これは、ドメインモデルの理解と保守に不便をもたらします。

  • ビジターパターンはある程度、クラスロジックカップリングの実現を引き起こしました

実装クラス(Factory / School / Building)のすべてのメソッド(draw)は、すべて1つのクラス(DrawVisitor)で記述されます。これは、ある程度論理的な結合であり、コードの保守には役立ちません。

  • ビジターパターンは、クラス間の関係を複雑にし、理解しにくくします

Double Dispatchという名前が示すように、対応するロジックを正常に呼び出すには、2つのディスパッチが必要です。最初のステップはaccpetメソッドを呼び出すこと、2番目のステップはvisitメソッドを呼び出すこと、呼び出し関係はより複雑になり、コードビハインドメンテナは簡単にコードを台無しにすることができます。

パターンマッチング


これが別のエピソードです。Java 14ではパターンマッチング機能が導入されました。この機能はScala / Haskel分野に長年存在していましたが、Javaが導入されたばかりであるため、多くの学生はまだそれが何であるかを知りません。したがって、パターンマッチングとビジターパターンの関係を説明する前に、パターンマッチングとは何かを簡単に紹介しましょう。このコードを書いたことを覚えていますか?

if (node instanceof Building) {
    Building building = (Building) building;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) factory;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) school;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

パターンマッチングを使用すると、次のコードを簡略化できます。

if (node instanceof Building building) {
    drawService.draw(building);
} else if (node instanceof Factory factory) {
    drawService.draw(factory);
} else if (node instanceof School school) {
    drawService.draw(school);
} else {
    drawService.draw(node);
}

ただし、Javaのパターンマッチングはまだ少し面倒ですが、Scalaのパターンマッチングの方が優れている場合があります。

node match {
  case node: Factory => drawService.draw(node)
  case node: Building => drawService.draw(node)
  case node: School => drawService.draw(node)
  case _ => drawService.draw(node)
}

より簡潔であるため、多くの人がビジターパターンの代わりにパターンマッチングを提唱しています。個人的には、パターンマッチングの方がずっとシンプルに見えると思います。多くの人がパターンマッチングはスイッチケースの高度なバージョンであると考えていますが、実際はそうではありません。詳細については、ビジターパターンについて、TOUR OF SCALA-PATTERN MATCHING(https://docs.scala-lang.org/tour/pattern-matching.html)を参照してください。パターンマッチングとの関係は、Scalaのパターンマッチング=ステロイドのビジターパターンで見ることができます。この記事では繰り返しません。

参考資料:

  • Scalaのパターンマッチング=ステロイドのビジターパターン

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • ビジターデザインパターンはいつ使用する必要がありますか?

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • デザインパターン-行動パターン-訪問者

    https://refactoring.guru/design-patterns/visitor 

  • Java14でのinstanceofのパターンマッチング

    https://refactoring.guru/design-patterns/visitor 

タオ部門テクノロジー部門-産業およびインテリジェントオペレーション-人材の採用

私たちはAlibabaの運用ワークベンチのデータインサイトチームです。膨大な量のデータ、高性能のリアルタイムコンピューティングエンジン、および困難なビジネスシナリオがあります。618からダブル11まで、タオバオからトモールまで、データ分析からビジネスの沈殿まで、私たちは完璧を追求する意志と雰囲気をテクノロジー界の隅々まで広げます。技術的な追求と技術的な深さであなたの参加を楽しみにしています!

募集ポジション:Java技術の専門家、データエンジニアは、
あなたが興味を持っている場合は、にあなたの履歴書を送ってください[email protected]歓迎するピックアップ〜アップ

✿さらに  読む

著者| Yu Haining(Jing Fan)

編集|オレンジ

生産|アリババの新しい小売技術

おすすめ

転載: blog.csdn.net/Taobaojishu/article/details/111503210