【デザインパターンの美しさ デザインの原理と考え方:仕様書とリファクタリング】 37|実践2(後編):IDジェネレータープロジェクトの各関数の例外処理コードをリファクタリングする

前のレッスンでは、エラー コード、NULL 値、空のオブジェクト、例外オブジェクトを返すなど、いくつかの異常な状況に対処する方法を説明しました。最も一般的に使用される例外オブジェクトについては、2 種類の例外のアプリケーション シナリオと、関数によってスローされた例外の 3 つの処理方法 (直接飲み込む、そのままスローする、新しい例外にラップする) にも焦点を当てます。

また、前回のレッスンの冒頭で、ID ジェネレーターのコードの例外処理について 4 つの質問も出しました。今日は、最後のクラスで述べた理論的知識と組み合わせて、1 つのクラスの時間を使用して、これらの質問に 1 つずつ答えます。

早速、今日のコンテンツを正式に開始しましょう。

generate() 関数のリファクタリング

まず、generate() 関数について見てみましょう。ローカル マシン名の取得に失敗した場合、関数は何を返しますか? そのような戻り値は妥当ですか?

  public String generate() {
    String substrOfHostName = getLastFiledOfHostName();
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }

ID は、ローカル名、タイムスタンプ、乱数の 3 つの部分で構成されます。タイムスタンプと乱数の生成関数は間違いませんが、ホスト名の取得に失敗する場合があります。現在のコード実装では、ホスト名の取得に失敗し、substrOfHostName が NULL の場合、generate() 関数は「null-16723733647-83Ab3uK6」のようなデータを返します。ホスト名の取得に失敗し、substrOfHostName が空の文字列の場合、generate() 関数は「-16723733647-83Ab3uK6」のようなデータを返します。

異常な状況で上記の 2 つの特別な ID データ形式を返すことは合理的ですか? これは実際に言うのは難しいです。特定のビジネスがどのように設計されているかを確認する必要があります。ただし、呼び出し元に例外を明示的に通知することをお勧めします。したがって、ここで特別な値をスローするのではなく、チェック例外をスローすることをお勧めします。

この設計思想に従って、generate() 関数をリファクタリングします。リファクタリング後のコードは次のようになります。

  public String generate() throws IdGenerationFailureException {
    String substrOfHostName = getLastFiledOfHostName();
    if (substrOfHostName == null || substrOfHostName.isEmpty()) {
      throw new IdGenerationFailureException("host name is empty.");
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }

getLastFiledOfHostName() 関数のリファクタリング

getLastFiledOfHostName() 関数の場合、関数内で UnknownHostException を飲み込む必要がありますか (try-catch してログを出力します)、それとも例外をスローし続ける必要がありますか? 上向きにスローされる場合、UnknownHostException をそのままスローするか、新しい例外にカプセル化する必要がありますか?

  private String getLastFiledOfHostName() {
    String substrOfHostName = null;
    try {
      String hostName = InetAddress.getLocalHost().getHostName();
      substrOfHostName = getLastSubstrSplittedByDot(hostName);
    } catch (UnknownHostException e) {
      logger.warn("Failed to get the host name.", e);
    }
    return substrOfHostName;
 }

現在の処理方法は、ホスト名の取得に失敗した場合、getLastFiledOfHostName() 関数が NULL 値を返すというものです。先に述べたように、NULL 値を返すか、異常なオブジェクトを返すかは、データ取得の失敗が正常な動作であるか異常な動作であるかによって異なります。ホスト名の取得に失敗すると、後続のロジックの処理に影響を与えることになるため、予期しない動作になるため、異常な動作です。NULL 値を返すよりも、ここで例外をスローする方が適切です。

UnknownHostException を直接スローするか、新しい例外に再パッケージ化するかは、その関数が例外とビジネス関係にあるかどうかによって異なります。getLastFiledOfHostName() 関数はホスト名の最後のフィールドを取得するために使用されます. UnknownHostException 例外はホスト名の取得に失敗したことを示します. この 2 つはビジネスに関連しているため、新しい例外に再パッケージ化せずに UnknownHostException を直接スローできます.

上記の設計思想に従って、getLastFiledOfHostName() 関数をリファクタリングします。リファクタリングされたコードは次のようになります。

 private String getLastFiledOfHostName() throws UnknownHostException{
    String substrOfHostName = null;
    String hostName = InetAddress.getLocalHost().getHostName();
    substrOfHostName = getLastSubstrSplittedByDot(hostName);
    return substrOfHostName;
 }

getLastFiledOfHostName() 関数を変更したら、それに応じて generate() 関数も変更する必要があります。generate() 関数で getLastFiledOfHostName() によってスローされた UnknownHostException をキャッチする必要があります。この例外をキャッチしたら、どうすればよいでしょうか?

以前の分析によると、ID の生成に失敗した場合は、呼び出し元に明示的に通知する必要があります。したがって、generate() 関数で UnknownHostException 例外を飲み込むことはできません。次に、それをそのままスローするか、新しい例外スローにカプセル化する必要がありますか?

私たちは後者を選びます。generate() 関数では、UnknownHostException をキャッチし、それを新しい例外 IdGenerationFailureException に再ラップしてスローする必要があります。これには 3 つの理由があります。

  • 呼び出し元が generate() 関数を使用する場合、ランダムな一意の ID が生成されることを知る必要があるだけで、ID がどのように生成されるかは気にしません。つまり、これは実装ではなく抽象化に依存するプログラミングです。generate() 関数が UnknownHostException を直接スローすると、実際には実装の詳細が公開されます。
  • コードのカプセル化の観点から、比較的低レベルの例外である UnknownHostException を上位レベルのコード、つまり generate() 関数を呼び出すコードに公開したくありません。さらに、呼び出し元がこの例外を受け取ると、その例外が何を表しているのか理解できず、その処理方法もわかりません。
  • UnknownHostException 例外は、ビジネス コンセプトに関しては、generate() 関数とは相関関係がありません。

上記の設計思想に従って、generate() 関数を再度リファクタリングします。リファクタリングされたコードは次のようになります。

  public String generate() throws IdGenerationFailureException {
    String substrOfHostName = null;
    try {
      substrOfHostName = getLastFiledOfHostName();
    } catch (UnknownHostException e) {
      throw new IdGenerationFailureException("host name is empty.");
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }

getLastSubstrSplittedByDot() 関数のリファクタリング

getLastSubstrSplittedByDot(String hostName) 関数で、hostName が NULL または空の文字列の場合、この関数は何を返す必要がありますか?

  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }

理論的に言えば、パラメータの受け渡しの正確性はプログラマによって保証されるべきであり、NULL 値または空文字列の判断と特別な処理を行う必要はありません。呼び出し元は、NULL 値または空の文字列を getLastSubstrSplittedByDot() 関数に渡してはなりません。合格した場合、それはコードのバグであり、修正する必要があります。しかし、そうは言っても、プログラマーが NULL 値または空の文字列を渡さないという保証はありません。では、NULL 値か空の文字列かを判断する必要がありますか?

関数がクラスに対してプライベートであり、クラス内でのみ呼び出される場合、それは完全に独自の制御下にあり、このプライベート関数を呼び出すときに NULL 値または空の文字列を渡さないようにすることができます。したがって、private 関数で NULL 値または空文字列を判断する必要はありません。関数が公開されている場合、誰がどのように呼び出すかを制御することはできません (同僚が怠慢で NULL 値を渡す可能性があり、この状況も存在します)。コードの堅牢性を最大限に高めるために、 public 関数内で NULL 値または空文字列の判断を行うのが最善です。

では、getLastSubstrSplittedByDot() は保護されており、プライベート関数でもパブリック関数でもないので、NULL 値か空の文字列かを判断する必要がありますか?

protected に設定されている理由は、単体テストの記述を容易にするためです。ただし、単体テストでは、入力が NULL 値または空の文字列の場合など、いくつかのコーナー ケースをテストする必要がある場合があります。したがって、ここでも NULL 値または空文字列の判定ロジックを追加した方がよいでしょう。多少の冗長性はありますが、さらにテストを追加しても問題ありません。

この設計思想に従って、getLastSubstrSplittedByDot() 関数をリファクタリングします。リファクタリング後のコードは次のようになります。

  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    if (hostName == null || hostName.isEmpty()) {
      throw IllegalArgumentException("..."); //运行时异常
    }
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }
按照上面讲的,我们在使用这个函数的时候,自己也要保证不传递 NULL 值或者空字符串进去。所以,getLastFiledOfHostName() 函数的代码也要作相应的修改。修改之后的代码如下所示:
 private String getLastFiledOfHostName() throws UnknownHostException{
    String substrOfHostName = null;
    String hostName = InetAddress.getLocalHost().getHostName();
    if (hostName == null || hostName.isEmpty()) { // 此处做判断
      throw new UnknownHostException("...");
    }
    substrOfHostName = getLastSubstrSplittedByDot(hostName);
    return substrOfHostName;
 }

generateRandomAlphameric() 関数のリファクタリング

generateRandomAlphameric(int length) 関数で、長さ < 0 または長さ = 0 の場合、この関数は何を返す必要がありますか?

  @VisibleForTesting
  protected String generateRandomAlphameric(int length) {
    char[] randomChars = new char[length];
    int count = 0;
    Random random = new Random();
    while (count < length) {
      int maxAscii = 'z';
      int randomAscii = random.nextInt(maxAscii);
      boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
      boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
      boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
      if (isDigit|| isUppercase || isLowercase) {
        randomChars[count] = (char) (randomAscii);
        ++count;
      }
    }
    return new String(randomChars);
  }
}

まず、長さ < 0 の場合を見てみましょう。負の長さのランダムな文字列を生成することは、非論理的で異常です。そのため、受信パラメータの長さが 0 未満の場合、IllegalArgumentException をスローします。

長さ = 0 の場合をもう一度見てみましょう。length = 0 は異常な動作ですか? それはあなたが自分自身をどのように定義するかにかかっています。IllegalArgumentException 例外をスローする異常な動作として定義するか、通常の動作として定義して、入力パラメーターの長さ = 0 の場合に関数が空の文字列を直接返すようにすることができます。どちらの処理方法を選択する場合でも、最も重要な点は、関数のコメントで length = 0 のときにどのようなデータが返されるかを明確に示すことです。

リファクタリング後の RandomIdGenerator コード

以上で、RandomIdGenerator クラスの各関数の例外処理コードのリファクタリングは終了です。見やすいように、リファクタリングされたコードを再配置してここに投稿します。リファクタリングのアイデアと一致しているかどうかを比較して確認できます。

public class RandomIdGenerator implements IdGenerator {
  private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
  @Override
  public String generate() throws IdGenerationFailureException {
    String substrOfHostName = null;
    try {
      substrOfHostName = getLastFiledOfHostName();
    } catch (UnknownHostException e) {
      throw new IdGenerationFailureException("...", e);
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
  }
  private String getLastFiledOfHostName() throws UnknownHostException{
    String substrOfHostName = null;
    String hostName = InetAddress.getLocalHost().getHostName();
    if (hostName == null || hostName.isEmpty()) {
      throw new UnknownHostException("...");
    }
    substrOfHostName = getLastSubstrSplittedByDot(hostName);
    return substrOfHostName;
  }
  @VisibleForTesting
  protected String getLastSubstrSplittedByDot(String hostName) {
    if (hostName == null || hostName.isEmpty()) {
      throw new IllegalArgumentException("...");
    }
    String[] tokens = hostName.split("\\.");
    String substrOfHostName = tokens[tokens.length - 1];
    return substrOfHostName;
  }
  @VisibleForTesting
  protected String generateRandomAlphameric(int length) {
    if (length <= 0) {
      throw new IllegalArgumentException("...");
    }
    char[] randomChars = new char[length];
    int count = 0;
    Random random = new Random();
    while (count < length) {
      int maxAscii = 'z';
      int randomAscii = random.nextInt(maxAscii);
      boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
      boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
      boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
      if (isDigit|| isUppercase || isLowercase) {
        randomChars[count] = (char) (randomAscii);
        ++count;
      }
    }
    return new String(randomChars);
  }
}

キーレビュー

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

今日の内容はより実践的で、前のクラスで学んだ理論的知識の応用です。今日の実戦から、どのような高度なソフトウェア設計と開発のアイデアを学びましたか? 私はレンガを投げて翡翠を始めるためにここにいます。以下の 3 点を要約します。

  • どんなにシンプルなコードでも、どんなに完璧なコードでも、熟慮を重ねる限り、最適化の余地は常にあります。
  • 内部スキルが十分でなく、理論的知識が十分でない場合、オープンソース プロジェクトのコードのどこが優れているかを理解することは困難です。前回の理論研究がないのと同じように、今日は少しずつリファクタリングし、説明し、分析します。最終的にリファクタリングされた RandomIdGenerator のコードを提供するだけで、その設計の本質を本当に学ぶことができますか?
  • レッスン 34 の最初の Xiao Wang の IdGenerator コードと最後の RandomIdGenerator コードを比較すると、一方は「使える」もので、もう一方は「使いやすい」ものであり、大きく異なります。プログラマーとして、少なくともコードを追求する必要があります。

クラスディスカッション

40 行未満の非常に単純な ID ジェネレーター コードの複数の反復リファクタリングを行うために 4 つの講義を費やしました。「キー レビュー」で述べた点に加えて、この反復的なリファクタリング プロセスから、さらに価値のあることを学びましたか?

おすすめ

転載: blog.csdn.net/qq_32907491/article/details/130184068