デザイン パターンの美しさ 61 - 戦略パターン (パート 2): さまざまなサイズのファイルの並べ替えをサポートする小さなプログラムを実装するには?

61 | 戦略モード (パート 2): さまざまなサイズのファイルの並べ替えをサポートする小さなプログラムを実装するには?

前回の授業では主にストラテジーパターンの原理と実装を紹介し、ストラテジーパターンを使ってif-elseやswitch-caseの分岐判定ロジックを取り除く方法を紹介しました。本日は、戦略パターンの設計意図と適用シナリオについて、「ファイルの並べ替え」の具体例を交えて詳しくお話します。

また、今日の説明では、デザインパターンがどのように「作成」されるかを、段階的な分析と再構築を通じて説明します。今日の学習を通じて、デザインの原則とアイデアは、実際にはデザイン パターンよりも普遍的で重要であることがわかります.コードのデザイン原則とアイデアを習得した後、私たちは自分で新しいデザイン パターンを作成することさえできます.

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

問題と解決策

そのような要件があると仮定し、ファイルをソートする機能を実現する小さなプログラムを書きたいと考えています。ファイルには整数のみが含まれており、隣接する数値はコンマで区切られています。このような小さなプログラムを作成するとしたら、どのように実装しますか? インタビューの質問として受け取って、自分で考えてから、以下の説明を読んでください。

これは非常に単純ではありませんか、ファイルの内容を読み取り、カンマで 1 つずつ数値に分割し、メモリ配列に入れ、何らかのソート アルゴリズム (クイック ソートなど) を記述します。 、または、プログラミング言語が提供するソート機能を直接使用して配列をソートし、最後に配列内のデータをファイルに書き込みます。

しかし、ファイルが巨大な場合はどうなりますか? たとえば、10GB のサイズがある場合、メモリには制限があるため (たとえば 8GB のみ)、ファイル内のすべてのデータを一度にメモリにロードすることはできません。ソート アルゴリズム (詳細については、コラム「データ構造とアルゴリズムの美しさ」の他の「ソート」関連の章を参照してください)。

ファイルのサイズが 100GB のように大きい場合、マルチコア CPU を活用するために、外部ソートに基づいてファイルを最適化し、マルチスレッド同時ソートの機能を追加できます。 MapReduce の「スタンドアロン バージョン」。

ファイルが 1 TB のように非常に大きい場合、単一マシンのマルチスレッド ソートであっても、非常に遅いと見なされます。現時点では、実際の MapReduce フレームワークを使用して、複数のマシンの処理能力を利用して、並べ替えの効率を向上させることができます。

コードの実装と分析

ソリューションのアイデアは完成しています。理解するのは難しくありません。次に、ソリューションのアイデアをコードの実装に変換する方法を見てみましょう。

まず、最も単純で直接的な方法で実装します。以下に特定のコードを投稿しました。最初に見てください。アルゴリズムではなく設計パターンについて話しているため、次のコード実装では、設計パターンに関連するスケルトン コードのみを示し、各並べ替えアルゴリズムの具体的なコード実装は示しません。興味があれば、自分で実装できます。

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    if (fileSize < 6 * GB) { // [0, 6GB)
      quickSort(filePath);
    } else if (fileSize < 10 * GB) { // [6GB, 10GB)
      externalSort(filePath);
    } else if (fileSize < 100 * GB) { // [10GB, 100GB)
      concurrentExternalSort(filePath);
    } else { // [100GB, ~)
      mapreduceSort(filePath);
    }
  }

  private void quickSort(String filePath) {
    // 快速排序
  }

  private void externalSort(String filePath) {
    // 外部排序
  }

  private void concurrentExternalSort(String filePath) {
    // 多线程外部排序
  }

  private void mapreduceSort(String filePath) {
    // 利用MapReduce多机排序
  }
}

public class SortingTool {
  public static void main(String[] args) {
    Sorter sorter = new Sorter();
    sorter.sortFile(args[0]);
  }
}

「コーディング仕様書」の部分で、関数の行数は多すぎず、1画面のサイズを超えないようにするのがよいと述べました。したがって、sortFile() 関数が長くなりすぎないようにするために、各ソート アルゴリズムを sortFile() 関数から分離し、4 つの独立したソート関数に分割します。

簡単なツールを開発するだけなら、上記のコードの実装で十分です。結局、コードの数は多くなく、その後の変更や拡張に対する要件も多くないため、どのように記述してもコードが保守不能になることはありません。ただし、大規模なプロジェクトを開発していて、ソート ファイルが機能モジュールの 1 つにすぎない場合は、コードの設計とコードの品質に懸命に取り組む必要があります。個々の小さな機能モジュールが適切に記述されている場合にのみ、プロジェクト全体のコードが悪くなることはありません。

先ほどのコードでは、各ソート アルゴリズムのコード実装を提供しませんでした。実際、自分で実装すると、各ソート アルゴリズムの実装ロジックがより複雑になり、コードの行数が増えることがわかります。すべてのソート アルゴリズムのコード実装は、Sorter のクラスに積み上げられているため、このクラスでは多くのコードが生成されます。「コーディング標準」の部分で、クラス内のコードが多すぎると可読性と保守性にも影響することについても言及しました。さらに、すべてのソート アルゴリズムはソーターのプライベート関数として設計されているため、コードの再利用性にも影響します。

コードの最適化とリファクタリング

前に説明した設計原則とアイデアを習得している限り、リファクタリングに使用する設計パターン、つまり、いくつかの分割に使用する設計パターンが思いつかなくても、上記の問題を解決する方法を知ることができるはずです。ソータークラスのコードが出てきて、より単一の責任を持つサブカテゴリに独立します。実際、分割は、クラスや関数のコードが多すぎたり、コードが複雑になったりする場合によく使用される方法です。このソリューションに従って、コードをリファクタリングします。リファクタリング後のコードは次のようになります。

public interface ISortAlg {
  void sort(String filePath);
}

public class QuickSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class ExternalSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class ConcurrentExternalSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class MapReduceSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg;
    if (fileSize < 6 * GB) { // [0, 6GB)
      sortAlg = new QuickSort();
    } else if (fileSize < 10 * GB) { // [6GB, 10GB)
      sortAlg = new ExternalSort();
    } else if (fileSize < 100 * GB) { // [10GB, 100GB)
      sortAlg = new ConcurrentExternalSort();
    } else { // [100GB, ~)
      sortAlg = new MapReduceSort();
    }
    sortAlg.sort(filePath);
  }
}

分割後、各クラスのコードが多すぎず、各クラスのロジックが複雑になりすぎず、コードの可読性と保守性が向上します。さらに、ソート アルゴリズムを独立したクラスとして設計し、特定のビジネス ロジック (コード内の if-else ロジック) から分離し、ソート アルゴリズムを再利用できるようにしました。このステップは、実際には戦略パターンの最初のステップであり、戦略の定義を分離することです。

実際、上記のコードは引き続き最適化できます。各ソート クラスはステートレスであり、使用するたびに新しいオブジェクトを再作成する必要はありません。したがって、ファクトリ パターンを使用して、オブジェクトの作成をカプセル化できます。この考えに従って、コードをリファクタリングします。リファクタリング後のコードは次のようになります。

public class SortAlgFactory {
  private static final Map<String, ISortAlg> algs = new HashMap<>();

  static {
    algs.put("QuickSort", new QuickSort());
    algs.put("ExternalSort", new ExternalSort());
    algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
    algs.put("MapReduceSort", new MapReduceSort());
  }

  public static ISortAlg getSortAlg(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }
    return algs.get(type);
  }
}

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg;
    if (fileSize < 6 * GB) { // [0, 6GB)
      sortAlg = SortAlgFactory.getSortAlg("QuickSort");
    } else if (fileSize < 10 * GB) { // [6GB, 10GB)
      sortAlg = SortAlgFactory.getSortAlg("ExternalSort");
    } else if (fileSize < 100 * GB) { // [10GB, 100GB)
      sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort");
    } else { // [100GB, ~)
      sortAlg = SortAlgFactory.getSortAlg("MapReduceSort");
    }
    sortAlg.sort(filePath);
  }
}

上記の 2 つのリファクタリングの後、現在のコードは実際に戦略パターンのコード構造に準拠しています。各部分が複雑になりすぎないように、戦略パターンを通じて戦略の定義、作成、および使用を分離します。ただし、Sorter クラスの sortFile() 関数には、まだ多数の if-else ロジックがあります。ここには if-else の論理分岐は多くなく、複雑でもないので、このように書いても問題ありません。しかし、どうしても if-else 分岐の判断をなくしたい場合は、方法があります。コードを直接お渡ししますので、一目でわかります。実際、これもルックアップ テーブル方式に基づいており、「algs」は「table」です。

public class Sorter {
  private static final long GB = 1000 * 1000 * 1000;
  private static final List<AlgRange> algs = new ArrayList<>();
  static {
    algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
    algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
    algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
    algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
  }

  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg = null;
    for (AlgRange algRange : algs) {
      if (algRange.inRange(fileSize)) {
        sortAlg = algRange.getAlg();
        break;
      }
    }
    sortAlg.sort(filePath);
  }

  private static class AlgRange {
    private long start;
    private long end;
    private ISortAlg alg;

    public AlgRange(long start, long end, ISortAlg alg) {
      this.start = start;
      this.end = end;
      this.alg = alg;
    }

    public ISortAlg getAlg() {
      return alg;
    }

    public boolean inRange(long size) {
      return size >= start && size < end;
    }
  }
}

現在のコードの実装はさらに洗練されています。変数部分を、ストラテジー ファクトリ クラスとソーター クラスの静的コード セクションに分離しました。新しいソート アルゴリズムを追加する場合、ストラテジー ファクトリ クラスと Sort クラスの静的コード セグメントを変更するだけでよく、他のコードを変更する必要がないため、コードの変更は最小限に抑えられ、集中化されます。

そうは言っても、新しいソート アルゴリズムを追加するときは、まだコードを変更する必要がありますが、これはオープン/クローズの原則に完全には準拠していません。開閉原理を完全に満たす方法はありますか?

Java 言語の場合、リフレクションによるポリシー ファクトリ クラスの変更を回避できます。具体的には、これを行います: 構成ファイルまたはカスタム注釈を使用して、どの戦略クラスがあるかをマークします; 戦略ファクトリ クラスは、構成ファイルを読み取るか、注釈によってマークされた戦略クラスを検索し、これらの戦略クラスを動的にロードします。リフレクション、戦略オブジェクトを作成する; 新しい戦略を追加するときは、新しく追加された戦略クラスを構成ファイルに追加するか、注釈でマークするだけで済みます。前回のクラスでのクラスディスカッションの質問を覚えていますか? この方法を使用して解決することもできます。

Sorter についても同様の方法で改変を回避できます。ファイル サイズの範囲とアルゴリズムの対応を構成ファイルに入れます。新しいソート アルゴリズムを追加する場合、コードではなく、構成ファイルを変更するだけで済みます。

キーレビュー

では、本日の内容は以上です。集中する必要があることをまとめて一緒に確認しましょう。

if-else 分岐判定となるとコードが悪いと思う人もいます。if-else 分岐判定が複雑でなく、コード数も多くなければ問題ないのですが、やはり if-else 分岐判定はほぼすべてのプログラミング言語で提供されている構文であり、存在理由があります。KISS の原則に従い、最高のデザインは可能な限りシンプルです。戦略モードを使用して n 個を超えるカテゴリを作成する必要があるのは、一種の過剰設計です。

ストラテジーモードというと、if-else分岐の判断ロジックを回避する機能だと思っている人もいます。実際、この理解は非常に一方的です。ストラテジー パターンの主な役割は、ストラテジーの定義、作成、および使用を分離し、コードの複雑さを制御して、各部分が複雑になりすぎず、コードの量が多くなりすぎないようにすることです。さらに、複雑なコードの場合、ストラテジー パターンはオープン/クローズの原則も満たすことができ、新しいストラテジーを追加する場合、コードの変更を最小限に抑えて一元化し、バグが発生するリスクを軽減します。

実際、デザインの原則とアイデアは、デザイン パターンよりも普遍的で重要です。コードの設計原則とアイデアを習得すると、特定の設計パターンを使用する必要がある理由をより明確に理解し、設計パターンをより適切に適用できるようになります。

クラスディスカッション

  1. 過去のプロジェクト開発において、戦略パターンを使用したことがありますか? 問題を解決するために使用していますか?
  2. コード内の if-else または switch-case 分岐ロジックを削除する必要があるのは、どのような状況ですか?

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

おすすめ

転載: blog.csdn.net/fegus/article/details/130519140
おすすめ