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

ファイル並べ替えの特定の例と組み合わせて、戦略パターンの設計意図とアプリケーションシナリオについて詳しく説明しましょう

さらに、デザインパターンがどのように「作成」されるかを示すために、段階的に分析およびリファクタリングします。今日の調査では、設計の原則とアイデアは実際には設計パターンよりも普遍的で重要であることがわかります。コードの設計の原則とアイデアを習得すると、新しい設計パターンを自分で作成することもできます。

問題と解決策

そのような要求があり、ファイルをソートする機能を実現するための小さなプログラムを作成したいとします。ファイルには整数のみが含まれ、隣接する番号はコンマで区切られます。このような小さなプログラムを作成する場合、どのようにそれを達成しますか?あなたはそれをインタビューの質問と考え、あなた自身でそれを考え、そして以下の私の説明を見ることができます。

これは非常に単純ではないと言うかもしれません。ファイルの内容を読み取り、それをコンマで数値に分割し、それらをメモリアレイに配置してから、並べ替えアルゴリズム(高速並べ替えなど)を記述します。または、プログラミング言語が提供する並べ替え機能を直接使用して配列を並べ替え、最後に配列内のデータをファイルに書き込みます。

しかし、ファイルが大きい場合はどうなりますか?たとえば、サイズは10GBです。メモリが限られているため(たとえば、8GBのみ)、ファイル内のすべてのデータを一度にメモリにロードすることはできません。現時点では、外部の並べ替えアルゴリズムを使用する必要があります(詳細については、他の「ソート」関連の章の「データ構造とアルゴリズムの美しさ」の列)。

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

ファイルのサイズが1TBなどの非常に大きい場合、単一マシンのマルチスレッドソートであっても、これは非常に遅くなります。現時点では、実際の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クラスにスタックされているため、このクラスには多くのコードが含まれます。「コーディング仕様」の部分では、クラスのコードが多すぎると、読みやすさと保守性にも影響することにも言及しました。さらに、すべての並べ替えアルゴリズムは、Sorterのプライベート関数として設計されており、コードの再利用性にも影響します。

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

以前に話した設計の原則と考えをマスターしている限り、上記の問題については、リファクタリングする設計パターンが考えられなくても、それを解決する方法、つまり、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言語の場合、リフレクションを通じて戦略ファクトリクラスの変更を回避できます。具体的には、これを行います。構成ファイルまたはカスタムアノテーションを使用して、使用している戦略クラスをマークします。戦略ファクトリクラスは、構成ファイルを読み取るか、注釈でマークされた戦略クラスを検索し、リフレクションを通じてこれらの戦略クラスを動的にロードします。戦略オブジェクトを作成します。新しい戦略を追加するときは、新しく追加した戦略クラスを構成ファイルに追加するか、注釈を付けるだけで済みます。前のクラスのクラスディスカッションの質問を覚えていますか?この方法を使用して解決することもできます。

ソーターの場合、同じ方法で変更を回避できます。ファイルサイズ間隔とアルゴリズムの対応を構成ファイルに入れます。新しい並べ替えアルゴリズムを追加するときは、コードを変更せずに、構成ファイルを変更するだけで済みます。

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/110492845