デザイン パターンの美しさ 69 - ビジター モード (パート 2): ダブル ディスパッチをサポートする言語にビジター モードが必要ないのはなぜですか?

69|ビジター パターン (パート 2): ダブル ディスパッチをサポートする言語にビジター パターンが必要ないのはなぜですか?

前回の授業では、ビジターパターンの原理と実装を学び、ビジターパターン誕生の思考過程を復習しました。一般的に言えば、このモードのコード実装は比較的難しいため、アプリケーション シナリオは多くありません。アプリケーション開発の観点からは、それは私たちの研究の焦点では​​ありません。

しかし、私のコラムを読むことは、単に知識を習得することではなく、問題を分析・解決する能力と論理的思考力を鍛えることであると、以前から繰り返し申し上げてきました。訪問者モードを使用する 導入として、これら 2 つの問題について一緒に説明し、深く考えてもらうことを望んでいます。

  • 二重ディスパッチをサポートする言語に Visitor パターンが必要ないのはなぜですか?
  • ビジター パターン以外に、前のレッスンの例を実装する方法はありますか?

早速、今日から本格的に勉強を始めましょう!

二重ディスパッチをサポートする言語に Visitor パターンが必要ないのはなぜですか?

実際、訪問者のパターンに関して言えば、ほとんどの本や資料では、中国語でダブル ディスパッチに翻訳されるダブル ディスパッチについて説明しています。ビジターモードを学ぶためにこの概念を理解する必要はありませんが、前回の説明では触れませんでしたが、他の本や資料を閲覧する際にこの概念に引っかからないようにするためには、理解する必要があると思います。ここでそれについて話しましょう。

また、Double Dispatch を学ぶことで、ビジター パターンの理解も深まると思います。また、今日の記事のタイトルにある疑問を解決するのにも役立つと思います。 ? ? 面接ではこんな質問も!

ダブル発送があるので、対応するシングル発送があります。いわゆるシングル ディスパッチは、実行するオブジェクトのメソッドがオブジェクトの実行時の型に従って決定されることを示します。オブジェクトに対して実行するメソッドは、メソッド パラメータのコンパイル時の型に従って決定されます。いわゆるダブル ディスパッチは、オブジェクトのランタイム タイプに従って決定される、オブジェクトが実行されるメソッドを参照します。実行するオブジェクトのメソッドは、メソッド パラメータのランタイム タイプに従って決定されます。

「派遣」という言葉をどう理解する?オブジェクト指向プログラミング言語では、メソッド呼び出しを一種のメッセージパッシング、つまり「ディスパッチ」として理解できます。オブジェクトが別のオブジェクトのメソッドを呼び出すとき、それはメッセージを送信することと同じです。このメッセージには、少なくともオブジェクト名、メソッド名、およびメソッド パラメータが含まれている必要があります。

「シングル」と「ダブル」という言葉をどう理解する?"Single" と "Double" は、どのオブジェクトのどのメソッドが実行されるかを指し、いくつかの要因のランタイム タイプに関連しています。さらに説明しましょう。Single Dispatch が「Single」と呼ばれるのは、どのオブジェクトのどのメソッドが実行されるかは、「オブジェクト」の実行時の型のみに関連するためです。Double Dispatch が「Double」と呼ばれる理由は、どのオブジェクトのどのメソッドが実行されるかが、「オブジェクト」と「メソッドのパラメーター」の両方の実行時の型に関連するためです。

プログラミング言語の文法メカニズムに特有の、Single Dispatch と Double Dispatch は、ポリモーフィズムと関数のオーバーロードに直接関係しています。現在主流のオブジェクト指向プログラミング言語 (Java、C++、C# など) は、Single Dispatch のみをサポートし、Double Dispatch はサポートしていません。

次に、Java 言語を例に取りましょう。

Java はポリモーフィズムをサポートしており、コードは実行時にオブジェクトの実際の型 (つまり、前述の実行時型) を取得し、実際の型に基づいて呼び出すメソッドを決定できます。Java は関数のオーバーロードをサポートしていますが、Java によって設計された関数のオーバーロードの文法規則は、実行時に関数に渡されるパラメーターの実際の型に従って、どのオーバーロードされた関数を呼び出すかを決定することではなく、コンパイル時に、宣言された型に従って呼び出すことです。関数に渡されるパラメーター (つまり、上記のコンパイル時の型) によって、呼び出すオーバーロードされた関数が決まります。つまり、どのオブジェクトのどのメソッドが実行されるかは、オブジェクトの実行時型のみに関連し、パラメータの実行時型とは関係ありません。したがって、Java 言語はシングル ディスパッチのみをサポートします。

これはより抽象的です。具体的に説明するために例を挙げましょう。コードは次のとおりです。

public class ParentClass {
  public void f() {
    System.out.println("I am ParentClass's f().");
  }
}

public class ChildClass extends ParentClass {
  public void f() {
    System.out.println("I am ChildClass's f().");
  }
}

public class SingleDispatchClass {
  public void polymorphismFunction(ParentClass p) {
    p.f();
  }

  public void overloadFunction(ParentClass p) {
    System.out.println("I am overloadFunction(ParentClass p).");
  }

  public void overloadFunction(ChildClass c) {
    System.out.println("I am overloadFunction(ChildClass c).");
  }
}

public class DemoMain {
  public static void main(String[] args) {
    SingleDispatchClass demo = new SingleDispatchClass();
    ParentClass p = new ChildClass();
    demo.polymorphismFunction(p);//执行哪个对象的方法,由对象的实际类型决定
    demo.overloadFunction(p);//执行对象的哪个方法,由参数对象的声明类型决定
  }
}

//代码执行结果:
I am ChildClass's f().
I am overloadFunction(ParentClass p).

上記のコードでは、31 行目の polymorphismFunction() 関数は、ChildClass の f() 関数である p の実際の型の f() 関数を実行します。コードの 32 行目の overloadFunction() 関数は、オーバーロードされた関数の overloadFunction(ParentClass p) と一致します。これは、p の宣言された型に従って一致するオーバーロードされた関数を決定することです。

Java 言語が Double Dispatch をサポートしていると仮定すると、次のコードの 37 行目 (前のレッスンの 2 番目のコードから抜粋。前のレッスンの説明と合わせて理解することをお勧めします) はエラーを報告しません。コードは、実行時のパラメーター (resourceFile) の実際のタイプ (PdfFile、PPTFile、WordFile) に従って、extract2txt の 3 つのオーバーロードされた関数のどれを使用するかを決定します。次に、次のコード実装は正常に実行でき、ビジター モードは必要ありません。これは、Double Dispatch をサポートする言語が Visitor パターンを必要としない理由にもなります。

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }
  //...
}
//...PPTFile、WordFile代码省略...
public class Extractor {
  public void extract2txt(PPTFile pptFile) {
    //...
    System.out.println("Extract PPT.");
  }

  public void extract2txt(PdfFile pdfFile) {
    //...
    System.out.println("Extract PDF.");
  }

  public void extract2txt(WordFile wordFile) {
    //...
    System.out.println("Extract WORD.");
  }
}

public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      extractor.extract2txt(resourceFile);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

訪問者パターン以外に、前のセクションの例の他の実装はありますか?

前回のクラスでは、例を使用して、訪問者パターンを段階的に設計する方法を示しました。その例を一緒にもう一度見てみましょう。Web サイトから多くのリソース ファイルをクロールしましたが、それらには PDF、PPT、Word の 3 つの形式があります。このリソース ファイルのバッチを処理するツールを開発する必要があります。これには、テキスト コンテンツの抽出、リソース ファイルの圧縮、ファイル メタ情報の抽出などが含まれます。

実際、このツールを開発するための多くのコード設計と実装のアイデアがあります。訪問者パターンを説明するために、前回のレッスンで訪問者パターンを使用して実装することにしました。実際には、他の実装方法もあり、たとえば、ファクトリ パターンを使用して、extract2txt() インターフェース関数を含む Extractor インターフェースを定義することもできます。PdfExtractor、PPTExtractor、および WordExtractor クラスは、Extractor インターフェイスを実装し、それぞれの extract2txt() 関数で Pdf、PPT、および Word 形式のファイルのテキスト コンテンツ抽出を実装します。ExtractorFactory ファクトリ クラスは、さまざまなファイル タイプに応じてさまざまな Extractor を返します。

この実装のアイデアは実際にはもっと単純です。コードを直接見てみましょう。

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
  public abstract ResourceFileType getType();
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }

  @Override
  public ResourceFileType getType() {
    return ResourceFileType.PDF;
  }

  //...
}

//...PPTFile/WordFile跟PdfFile代码结构类似,此处省略...

public interface Extractor {
  void extract2txt(ResourceFile resourceFile);
}

public class PdfExtractor implements Extractor {
  @Override
  public void extract2txt(ResourceFile resourceFile) {
    //...
  }
}

//...PPTExtractor/WordExtractor跟PdfExtractor代码结构类似,此处省略...

public class ExtractorFactory {
  private static final Map<ResourceFileType, Extractor> extractors = new HashMap<>();
  static {
    extractors.put(ResourceFileType.PDF, new PdfExtractor());
    extractors.put(ResourceFileType.PPT, new PPTExtractor());
    extractors.put(ResourceFileType.WORD, new WordExtractor());
  }

  public static Extractor getExtractor(ResourceFileType type) {
    return extractors.get(type);
  }
}

public class ToolApplication {
  public static void main(String[] args) {
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      Extractor extractor = ExtractorFactory.getExtractor(resourceFile.getType());
      extractor.extract2txt(resourceFile);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

リソース ファイルの圧縮、テキスト コンテンツの抽出機能と同様のコード実装などの新しい機能を追加する必要がある場合は、Compressor インターフェイス、PdfCompressor、PPTCompressor、WordCompressor の 3 つの実装クラスを追加し、それらの CompressorFactory ファクトリ クラスを作成するだけです。 . . 変更する必要があるのは、最上位の ToolApplication クラスだけです。基本的に「拡張にオープン、変更にクローズ」という設計原則に準拠しています。

リソース ファイル処理ツールの例では、ツールが多くの機能を提供するわけではなく、ごくわずかしか提供しない場合は、ファクトリ モードの実装を使用することをお勧めします。結局のところ、コードはより明確で理解しやすくなります。反対に、ツールが多くの機能を提供する場合 (12 を超えるなど) は、ビジター モードを使用することをお勧めします。コードの保守性に影響します。

キーレビュー

では、今日はここまでです。集中する必要があることをまとめて一緒に確認しましょう。

一般的に、ビジターモードはわかりにくく、適用シナリオが限定され、特に必要というわけではないので、プロジェクトでの使用はお勧めしません。したがって、前のレッスンのリソース ファイルの処理の例では、設計と実装にファクトリ パターンを使用することをお勧めします。

さらに、今日はダブルディスパッチに注目しました。オブジェクト指向プログラミング言語では、メソッド呼び出しはメッセージ パッシング (ディスパッチ) として理解できます。オブジェクトが別のオブジェクトのメソッドを呼び出すとき, それはそれにメッセージを送信することと同じです. このメッセージには, 少なくともオブジェクト名, メソッド名, メソッドパラメータが含まれていなければなりません.

いわゆるシングル ディスパッチは、オブジェクトのランタイム タイプに応じて決定されるオブジェクトが実行されるメソッドを参照し、メソッド パラメータのコンパイル時のタイプに応じて決定されるオブジェクトのどのメソッドが実行されます。 . いわゆるダブル ディスパッチは、オブジェクトのランタイム タイプに従って決定される、オブジェクトが実行されるメソッドを参照します。実行するオブジェクトのメソッドは、メソッド パラメータのランタイム タイプに従って決定されます。

プログラミング言語の文法メカニズムに特有の、Single Dispatch と Double Dispatch は、ポリモーフィズムと関数のオーバーロードに直接関係しています。現在主流のオブジェクト指向プログラミング言語 (Java、C++、C# など) は、Single Dispatch のみをサポートし、Double Dispatch はサポートしていません。

クラスディスカッション

  1. ビジター パターンは操作をオブジェクトから分離していますか? オブジェクト指向の設計原則に違反していますか? この問題についてどう思いますか。
  2. Single Dispatch を説明するコード例で、SingleDispatchClass のコードを次のように変更し、他のコードは変更しない場合、DemoMain の出力はどうなりますか? なんでこんな結果に?
public class SingleDispatchClass {
  public void polymorphismFunction(ParentClass p) {
    p.f();
  }

  public void overloadFunction(ParentClass p) {
    p.f();
  }

  public void overloadFunction(ChildClass c) {
    c.f();
  }
}

メッセージを残して、あなたの考えを私と共有してください。何かを得た場合は、この記事を友達と共有してください。

おすすめ

転載: blog.csdn.net/fegus/article/details/130519357