【デザインパターンの美しさ デザインの原理と考え方:仕様書とリファクタリング】 33 | セオリー5:コードの品質を手っ取り早く改善できる20のプログラミング仕様書(後編)

前の 2 つのクラスでは、名前付け、コメント、およびコード スタイルについて説明しましたが、今日は、コードの可読性を向上させるのに役立つ実践的なプログラミング スキルについて説明します。スキルのこの部分は比較的些細なことであり、それらを包括的にリストすることは困難です.私はより重要だと思う主要なもののいくつかを要約しました.実践でより多くのスキルを徐々に要約して蓄積する必要があります.

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

1. コードを小さな単位ブロックに分割する

ほとんどの人はコードを読む習慣があり、まず全体を見てから詳細を見ていきます。したがって、モジュール化された抽象的思考が必要であり、複雑なロジックの大きなブロックをクラスまたは関数に洗練し、詳細を隠して、コードを読む人が詳細で迷子にならないようにする必要があります。これにより、可読性が大幅に向上します。コードの。ただし、コード ロジックがより複雑な場合にのみ、実際にクラスまたは関数を改良することをお勧めします。結局、抽出された関数に 2 行または 3 行のコードしか含まれていない場合、コードを読み取るときにそれをスキップする必要があり、読み取りのコストが増加します。

ここで、さらに説明するために例を挙げます。コードは次のとおりです。リファクタリングする前に、invest() 関数で、時間処理に関する最初のコードがわかりにくいですか? リファクタリング後、ロジックのこの部分を関数に抽象化し、isLastDayOfMonth という名前を付けます. 名前から、その関数が明確に理解でき、今日が月末かどうかを判断できます. ここでは、複雑なロジック コードを関数に改良することで、コードの可読性を大幅に向上させました。

// 重构前的代码
public void invest(long userId, long financialProductId) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(date);
  calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
  if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
    return;
  }
  //...
}
// 重构后的代码:提炼函数之后逻辑更加清晰
public void invest(long userId, long financialProductId) {
  if (isLastDayOfMonth(new Date())) {
    return;
  }
  //...
}
public boolean isLastDayOfMonth(Date date) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(date);
  calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
  if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
   return true;
  }
  return false;
}

2. 関数パラメータが多すぎないようにする

個人的には、関数に含まれるパラメーターが 3 つまたは 4 つであれば問題ないと思いますが、5 つ以上の場合、パラメーターが少し多すぎると感じ、コードの可読性に影響を与え、不便になります。使用する。パラメータが多すぎる状況では、一般に 2 つの処理方法があります。

  • 関数が単一の責任を持っているかどうか、および複数の関数に分割することでパラメーターを減らすことができるかどうかを検討してください。サンプルコードは次のとおりです。
public void getUser(String username, String telephone, String email);
// 拆分成多个函数
public void getUserByUsername(String username);
public void getUserByTelephone(String telephone);
public void getUserByEmail(String email);
  • 関数パラメーターをオブジェクトにカプセル化します。サンプルコードは次のとおりです。
public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);
// 将参数封装成对象
public class Blog {
  private String title;
  private String summary;
  private String keywords;
  private Strint content;
  private String category;
  private long authorId;
}
public void postBlog(Blog blog);

さらに、関数が外部に公開されたリモート インターフェイスである場合、パラメーターをオブジェクトにカプセル化することで、インターフェイスの互換性を向上させることもできます。新しいパラメーターをインターフェイスに追加する場合、古いリモート インターフェイスの呼び出し元は、新しいインターフェイスと互換性があるようにコードを変更する必要がない場合があります。

3. 関数パラメータを使用してロジックを制御しない

内部ロジックを制御する関数でブール型の識別パラメータを使用しないでください。true の場合はこのロジックを使用し、false の場合は別のロジックを使用します。これは、単一責任の原則とインターフェイス分離の原則に明らかに違反しています。読みやすくするために、2 つの関数に分割することをお勧めします。説明のために例を挙げましょう。

public void buyCourse(long userId, long courseId, boolean isVip);
// 将其拆分成两个函数
public void buyCourse(long userId, long courseId);
public void buyCourseForVip(long userId, long courseId);

ただし、関数が影響範囲が限定されたプライベート関数である場合、または分割後の 2 つの関数が同時に呼び出されることが多い場合は、識別パラメーターを適切に保持することを検討できます。サンプル コードは次のとおりです。
// 2 つの関数呼び出しに分割します。

boolean isVip = false;
//...省略其他逻辑...
if (isVip) {
  buyCourseForVip(userId, courseId);
} else {
  buyCourse(userId, courseId);
}
// 保留标识参数的调用方式更加简洁
boolean isVip = false;
//...省略其他逻辑...
buyCourse(userId, courseId, isVip);

ブール型を識別パラメータとしてロジックを制御する場合のほかに、「パラメータがnullかどうかで」ロジックを制御する場合もあります。この場合、複数の関数に分割する必要もあります。分割後の機能の責任はより明確になり、誤って使用される可能性が低くなります。具体的なコード例は次のとおりです。

public List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
  if (startDate != null && endDate != null) {
    // 查询两个时间区间的transactions
  }
  if (startDate != null && endDate == null) {
    // 查询startDate之后的所有transactions
  }
  if (startDate == null && endDate != null) {
    // 查询endDate之前的所有transactions
  }
  if (startDate == null && endDate == null) {
    // 查询所有的transactions
  }
}
// 拆分成多个public函数,更加清晰、易用
public List<Transaction> selectTransactionsBetween(Long userId, Date startDate, Date endDate) {
  return selectTransactions(userId, startDate, endDate);
}
public List<Transaction> selectTransactionsStartWith(Long userId, Date startDate) {
  return selectTransactions(userId, startDate, null);
}
public List<Transaction> selectTransactionsEndWith(Long userId, Date endDate) {
  return selectTransactions(userId, null, endDate);
}
public List<Transaction> selectAllTransactions(Long userId) {
  return selectTransactions(userId, null, null);
}
private List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
  // ...
}

4. 機能設計は単一の責任を持つべきである

以前に単一責任の原則について説明したとき、クラスやモジュールなどのアプリケーション オブジェクトを対象としていました。実際、機能の設計では、単一責任の原則を満たさなければなりません。クラスやモジュールと比較して、関数の粒度は比較的小さく、コードの行数も少ないため、単一責任の原則を適用すると、クラスやモジュールに適用する場合ほど曖昧ではなくなります。できるだけシンプルに。

具体的なコード例は次のとおりです。

public boolean checkUserIfExisting(String telephone, String username, String email)  { 
  if (!StringUtils.isBlank(telephone)) {
    User user = userRepo.selectUserByTelephone(telephone);
    return user != null;
  }
  
  if (!StringUtils.isBlank(username)) {
    User user = userRepo.selectUserByUsername(username);
    return user != null;
  }
  
  if (!StringUtils.isBlank(email)) {
    User user = userRepo.selectUserByEmail(email);
    return user != null;
  }
  
  return false;
}
// 拆分成三个函数
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);

5. 深すぎるネスティング レベルを削除する

コードの入れ子が深すぎるのは、多くの場合、if-else、switch-case、および for ループの過度の入れ子が原因です。個人的には、ネスティングは 2 層を超えないようにすることをお勧めします.2 層を超えると、ネスティングを減らすことができるかどうかを検討する必要があります。入れ子が深すぎると、それ自体がわかりづらく、また、入れ子が深すぎると、コードが何度もインデントされやすくなり、入れ子内のステートメントが 1 行を超えて 2 行に折りたたまれ、コードのきれいさに影響します。コードです。

入れ子が深すぎる問題を解決する方法は比較的成熟しており、次の 4 つの一般的な考え方があります。

  • 冗長な if または else ステートメントを削除します。コード例は次のとおりです。
// 示例一
public double caculateTotalAmount(List<Order> orders) {
  if (orders == null || orders.isEmpty()) {
    return 0.0;
  } else { // 此处的else可以去掉
    double amount = 0.0;
    for (Order order : orders) {
      if (order != null) {
        amount += (order.getCount() * order.getPrice());
      }
    }
    return amount;
  }
}
// 示例二
public List<String> matchStrings(List<String> strList,String substr) {
  List<String> matchedStrings = new ArrayList<>();
  if (strList != null && substr != null) {
    for (String str : strList) {
      if (str != null) { // 跟下面的if语句可以合并在一起
        if (str.contains(substr)) {
          matchedStrings.add(str);
        }
      }
    }
  }
  return matchedStrings;
}
  • ネストを早期に終了するには、プログラミング言語によって提供される continue、break、および return キーワードを使用します。コード例は次のとおりです。
// 重构前的代码
public List<String> matchStrings(List<String> strList,String substr) {
  List<String> matchedStrings = new ArrayList<>();
  if (strList != null && substr != null){ 
    for (String str : strList) {
      if (str != null && str.contains(substr)) {
        matchedStrings.add(str);
        // 此处还有10行代码...
      }
    }
  }
  return matchedStrings;
}
// 重构后的代码:使用continue提前退出
public List<String> matchStrings(List<String> strList,String substr) {
  List<String> matchedStrings = new ArrayList<>();
  if (strList != null && substr != null){ 
    for (String str : strList) {
      if (str == null || !str.contains(substr)) {
        continue; 
      }
      matchedStrings.add(str);
      // 此处还有10行代码...
    }
  }
  return matchedStrings;
}
  • ネストを減らすために実行順序を調整します。具体的なコード例は次のとおりです。
// 重构前的代码
public List<String> matchStrings(List<String> strList,String substr) {
  List<String> matchedStrings = new ArrayList<>();
  if (strList != null && substr != null) {
    for (String str : strList) {
      if (str != null) {
        if (str.contains(substr)) {
          matchedStrings.add(str);
        }
      }
    }
  }
  return matchedStrings;
}
// 重构后的代码:先执行判空逻辑,再执行正常逻辑
public List<String> matchStrings(List<String> strList,String substr) {
  if (strList == null || substr == null) { //先判空
    return Collections.emptyList();
  }
  List<String> matchedStrings = new ArrayList<>();
  for (String str : strList) {
    if (str != null) {
      if (str.contains(substr)) {
        matchedStrings.add(str);
      }
    }
  }
  return matchedStrings;
}
  • ネストされたロジックを関数呼び出しにカプセル化して、ネストを減らします。具体的なコード例は次のとおりです。
// 重构前的代码
public List<String> appendSalts(List<String> passwords) {
  if (passwords == null || passwords.isEmpty()) {
    return Collections.emptyList();
  }
  
  List<String> passwordsWithSalt = new ArrayList<>();
  for (String password : passwords) {
    if (password == null) {
      continue;
    }
    if (password.length() < 8) {
      // ...
    } else {
      // ...
    }
  }
  return passwordsWithSalt;
}
// 重构后的代码:将部分逻辑抽成函数
public List<String> appendSalts(List<String> passwords) {
  if (passwords == null || passwords.isEmpty()) {
    return Collections.emptyList();
  }
  List<String> passwordsWithSalt = new ArrayList<>();
  for (String password : passwords) {
    if (password == null) {
      continue;
    }
    passwordsWithSalt.add(appendSalt(password));
  }
  return passwordsWithSalt;
}
private String appendSalt(String password) {
  String passwordWithSalt = password;
  if (password.length() < 8) {
    // ...
  } else {
    // ...
  }
  return passwordWithSalt;
}

さらに、ポリモーフィズムを使用して if-else および switch-case 条件判断を置き換える一般的に使用されるメソッドがあります。このアイデアにはコード構造の変更が含まれますが、これについては後の章で説明します。そのため、当面はここでは説明しません。

6. 説明変数の使い方を学ぶ

コードの可読性を向上させるためによく使用される説明変数は、次の 2 つです。

  • 定数はマジック ナンバーを置き換えます。サンプルコードは次のとおりです。
public double CalculateCircularArea(double radius) {
  return (3.1415) * radius * radius;
}
// 常量替代魔法数字
public static final Double PI = 3.1415;
public double CalculateCircularArea(double radius) {
  return PI * radius * radius;
}
  • 説明変数を使用して、複雑な式を説明します。サンプルコードは次のとおりです。
if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
  // ...
} else {
  // ...
}
// 引入解释性变量后逻辑更加清晰
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
  // ...
} else {
  // ...
} 

キーレビュー

では、本日の内容は以上です。今日お話ししたプログラミング スキルに加えて、最初の 2 つのレッスンでは、名前付けと注釈、およびコード スタイルについても説明しました。それでは、これら 3 つのレッスンの重要なポイントをまとめて確認しましょう。

1.ネーミングについて

  • ネーミングのポイントは、その意味を正確に表現できるかどうかです。さまざまなスコープの命名について、さまざまな長さを適切に選択できます。
  • クラス情報を使用して属性と関数の命名を簡素化し、関数情報を使用して関数パラメータの命名を簡素化できます。
  • 名前は読みやすく、検索可能である必要があります。珍しい、発音しにくい英語の単語を名前に使用しないでください。命名はプロジェクトの統一された仕様に準拠する必要があり、直感に反する命名は使用しないでください。
  • インターフェースには 2 つの命名方法があります: 1 つはインターフェースに接頭辞 "I" を付ける方法、もう 1 つはインターフェースの実装クラスに接尾辞 "Impl" を付ける方法です。抽象クラスの命名にも、接頭辞「Abstract」を付ける方法と付けない方法の 2 通りがあります。どちらの命名方法でもかまいませんが、キーはプロジェクト内で統一することです。

2.コメントについて

  • メモの内容は主に、何をするか、なぜ行うか、どのように行うかの 3 つの側面で構成されます。一部の複雑なクラスとインターフェースについては、「使用方法」を記述する必要がある場合もあります。

  • クラスと関数にはコメントを付け、できるだけ包括的かつ詳細に記述する必要があります。関数内のコメントは比較的少なく、一般に、コードの可読性を向上させるために、適切な名前付け、洗練された関数、説明変数、および要約コメントが使用されます。

3.コードスタイルについて

  • 関数とクラスの大きさは?関数のコードの行数は、1 画面のサイズ (50 行など) を超えてはなりません。クラスのサイズ制限を決定するのはより困難です。
  • コード行の最適な長さは? IDE の表示幅を超えないようにすることをお勧めします。もちろん、小さすぎることはできません。小さすぎると、わずかに長いステートメントが 2 行に折りたたまれてしまい、コードのクリーンさに影響し、読みにくくなります。
  • 空白行をうまく利用して単位ブロックを区切ってください。比較的長い関数の場合、ロジックを明確にするために、空白行を使用して各コード ブロックを区切ることができます。
  • 4スペースのインデントまたは2スペースのインデント? 特にコードの入れ子レベルが比較的深い場合は、スペースを節約できる 2 スペースのインデントを使用することを個人的にお勧めします。スペース 2 個のインデントとスペース 4 個のインデントのどちらを使用するかに関係なく、タブ キーを使用してインデントしないでください。
  • 中括弧は新しい行で開始する必要がありますか? 前のステートメントと同じ行に中括弧を配置すると、コード行が節約されます。しかし、新しい行で中括弧を開く方法では、左右の括弧を縦に並べることができ、どのコードがどのコード ブロックに属しているかが一目でわかります。
  • クラスのメンバーはどのように配置されていますか? Google Java プログラミング仕様では、依存クラスは小さいものから大きいもののアルファベット順に並べられています。クラスでは、最初にメンバー変数を記述し、次に関数を記述します。メンバー変数または関数の間には、最初に static メンバー変数または関数を記述し、次に通常の変数または関数を記述し、スコープのサイズに従って順番に配置します。

4.コーディングスキルについて

  • 複雑なロジックを関数とクラスに抽出します。
  • パラメータを複数の関数に分割するか、パラメータをオブジェクトとしてカプセル化して、多すぎるパラメータを処理します。
  • 関数内でパラメーターを使用してコード実行ロジックを制御しないでください。
  • 機能設計は、単一の責任を持つ必要があります。
  • 深すぎるネスト レベルを削除します。メソッドには、冗長な if または else ステートメントを削除する、continue、break、return キーワードを使用してネストを早期に終了する、実行順序を調整してネストを減らす、ネスト ロジックの一部を関数に抽象化するなどがあります。
  • マジック ナンバーをリテラル定数に置き換えます。
  • 説明変数を使用して複雑な式を説明し、コードの読みやすさを向上させます。

5.統一コーディング標準

これらの 3 つのセクションで述べたより詳細な知識ポイントに加えて、最後に、もう 1 つの非常に重要なこと、つまり、プロジェクト、チーム、さらには企業でさえ、統一されたコーディング標準を策定し、コード レビューを通じてその実装を監督する必要があります。品質はすぐに結果をもたらします。

クラスディスカッション

ここまでで、20 のコーディング標準すべてについて説明しました。私はあなたがどれだけマスターしたのだろうか?今日私が言及したもの以外に、コードの可読性を大幅に改善できるプログラミングのトリックはありますか?

おすすめ

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