🔍

【コメント求む!】『リーダブルコード』から学ぶ:ChatGPTによるコードレビューの検証

2024/04/17に公開

はじめに

https://qiita.com/AKB428/items/20e81ccc8d9998b5535d

読んで理解でするだけでなく、本の内容を形としてすぐにアウトプットできる方法はないか?
上記の良記事を読んでいてそのように思い、今回の検証を思いつきました。

具体的な検証について
ChatGPTにリーダブルコードを意識したプロンプトと整形してほしいコードをインプットすることで、本書の内容に沿ってアウトプットしてくれるのではないか?と期待し実験しました。

結論(プロンプト)

#Instructions 

You are a top-notch engineer. Based on the following 14 conditions and input statements, produce excellent output. Please utilize the book "Readable Code" to give us beautiful and readable code. Note that the code you want us to modify will be placed in the input statement. in japanese please. 

1. Pack Information into Names
Use specific verbs over general ones (e.g., send → dispatch, find → locate).
Avoid generic names like tmp or retval. Use names that reflect their function clearly.
Short names are acceptable in small scopes but avoid overly abbreviated or team-specific shorthand (e.g., avoid shortening string to str unless commonly understood).

2. Avoid Misleading Names
Ensure names cannot be misinterpreted. Repeatedly question if a name could mean something else.
Use prefixes like min_, max_, first_, last_, begin_, and end_ to clarify limits and ranges.
Reserve get for accessors only.

3. Align Indents
Consistently align indentations to improve readability, such as in arrays or argument lists.

4. Avoid Valueless Comments
Do not comment obvious things; use comments to explain "why" something is done, not "what" is done.
Avoid comments that state the obvious or provide no additional information.

5. Make Comments Precise and Informative
Use high-density, informative terms especially when technical or domain-specific language can clarify the intent.

6. Simplify Control Flow
Prefer if-else over ternary operators for better readability. Use simple loop constructs like while instead of do-while.
Early returns are encouraged to reduce nesting and simplify functions.

7. Break Down Large Expressions
Use explanatory variables to break down complex conditions into manageable parts.

8. Avoid Misuse of Short-Circuit Evaluation
Use logical operators clearly and avoid clever tricks that compromise readability.

9. Variables and Readability
Limit variable scope as narrowly as possible to reduce cognitive load.
Avoid global variables and minimize the use of member variables where feasible.

10. Extract Irrelevant Subproblems
Utilize utility functions for tasks that don't directly contribute to the primary logic.
Standard library functions often handle common tasks more efficiently.

11. Divide Tasks Among Functions
Separate distinct tasks into different functions to enhance modularity and testing.

12. Code with Intention
Articulate the logic in plain language before coding to clarify your intent and plan.

13. Write Short Code
The most readable code is minimal. Familiarize yourself with libraries and reuse code to avoid redundancy.

14. Readability and Testability
Design code for testability without compromising readability. Do not alter code purely for testing purposes.


#Input

# ここにチェックしたいコードを入力します(こちら削除してください)


#Output

記事内容の抽出

ChatGPTにプロンプトの元となった日本語訳です

https://qiita.com/AKB428/items/20e81ccc8d9998b5535d

記事も読ませていただきました。
その上で記事の内容を抽出したものが以下になります。


#条件:
1. 名前に情報を詰め込む
単語	代替案
send ->	deliver,dispatch,announce,distribute,route
find ->	search,etract,locate,recover
start -> launch,create,begin,open
make ->	create,set up,build,generate,compose,add,new
tmpやretvalなどの汎用的すぎる名前は避ける
i j k などはループの変数として理解できるため使ってもよいこととする
スコープが小さければ短い名前でもいい
長い名前を入力するのは問題じゃない(今はIDEやエディタが補完するため)
stringをstr、documentをdocと省略するのは問題ないが、チーム独自の省略規則はやめよう

2. 誤解されない名前
名前が「他の意味と間違えられることはないだろうか?」と何度も自問自答する
限界値を示すときは min_ max_を使う
範囲を示すときは first_ last_を使う
包括的範囲には begin_ end_ を使う
getXXXXは変数へのアクセッサの意味として認知されているのでそのような処理以外でこのメソッド名を使用しないこと

3. インデントを揃える

例:
command[] = {
             {"-t",      "-bcc",     "-x" },
             {"-g",      "-gt",      "-j" },
}

4. 価値のないコメントは避ける
コードからすぐに分かることをコメントに書かない
関数名が変だとか変数名が変だとか修正可能なものを補うコメント
THREAD_NUM = 8 など、なぜ8なのかはこのコードからはわからない。デフォルト値である理由をかくとよい

5. コメントは正確に
コメントは領域に対する情報の比率が高くなければならない
情報密度の高い言葉を使う(専門用語、コンピュータ用語)
参照される頻度の高い変数にはコメントを入れたほうが良い

6. 制御フローを読みやすくする
三項演算子より基本的にはif~elseが望ましいが、三項演算子で簡潔になるときには使用してよい
do~whileループより、whileループを適用する
関数から早く返す
関数で複数のreturnを使ってはいけないと思い込んでるのは間違い
早めにreturnすることで「ガード節」をつくる。
public boolean checkBadWord(String str) {
  if (str == null) {return false};

  //以降なんらかの処理
}
ネストを浅くする
if を入れ子にしない→理由:コードを読む人は条件を精神的にスタックにPUSHしないといけない
スレッド、シグナル、割り込みハンドラ、例外、関数ポインタ、無名関数、仮想メソッドはコードを追うのが難しくなるためこのようなコードが全体の割合を占めないように注意する必要がある。

7. 巨大な式を分割する
説明変数を用いて分割する
if line.split(':')[0].strip() == "root" { }
username =  line.split(':')[0].strip();
if username == "root"  {}

要約変数を用いて分割する
if (request.user.id == document.owner_id) {
  // ユーザーはこの文章を編集できる
}


if (request.user.id != document.owner_id) {
  // ユーザーはこの文章を編集できない
}
final boolean user_owns_document = (request.user.id != document.owner_id)

if(!user_owns_document) {
 // ユーザーはこの文章を編集できない 
}

if(user_owns_document) {
 // ユーザーはこの文章を編集できる
}

8. 短絡評価の悪用
assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());
bucket = FindBucket(key);

if(bucket != NULL) assert(!bucket->IsOccpuied());

引数の結果を返すOR演算子 (Python JavaScript Ruby)

x = a || b || c
上記の場合、先に真と判定された値が返却される。

x = false || "aaa" || "bbb"
puts x

>>> aaa

9. 変数と読みやすさ
制御フロー変数を削除する
変数のスコープを縮める
「グローバル変数を避ける」というのは当たり前のルールとして認知されている
これを発展させた考えとして 「変数のことが見えるコード行数をできるだけ減らす。」 = スコープを縮める →理由:変数を追うために読むコードが大量になる
メンバ変数も極力避ける
JavaScriptでプライベート変数を作る
変数を定義するときには常にvarキーワードをつける
PythonとJavaScriptのネストしないスコープ
定義の位置を下げる
変数は一度だけ書き込む

10. 無関係の下位問題を抽出する
メソッドの抽出
純粋なユーティリティコード
プログラムの各種言語には汎用的なタスクを処理するライブラリが実装されている、でもたまに欲しい機能がない時がある。
その場合はその関数を作り汎用コード化すればよい
ファイルを全部読み込むコード

// PHP
file_get_contents("filename")

// Python
open("filename").read()

11. タスクをできるだけ異なる関数に分割する。

12.コードに思いをこめる
コードを書く前にロジックを説明する文章を考えてコードを書く

13.短いコードを書く
鍵となる考え:最も読みやすいコードは何も書かれていないコードだ
汎用的なユーティリティを作って使いまわす、標準ライブラリのAPIや汎用ライブラリを広く知って使う。
たまには標準ライブラリのすべての関数・モジュール型の名前を15分かけてよんでみよう。
平均的なソフトウェアエンジニアが書く出荷用のコードは10行。この10行というのはよくテストされ文章化されてる製品用ということ、できるだけこの出荷用のコードを再利用したほうがよい。

14. テストと読みやすさ
テストしやすいようにコードを設計してください
テストのために本物のコードの読みやすさを犠牲にしたり、テスト都合で本物のコードに手を加えないでください

ChatGPTへプロンプトの整形を依頼

以下のプロンプトを使用しました

以下の14の条件について、プロンプトを改良したものを教えてください。冗長な箇所などもあると思いますので、改善をお願いします(すべて英語でお願いします)なお、以下の14の条件は、書籍「リーダブルコード」を使って美しく読みやすいコードを書くためのプロンプトです。出力していただいたプロンプトは、弊社のエンジニアチームで使用させていただきたいと思います。よろしくお願いいたします。

あくまでプロンプトなので、真実ではないこと多数記載している点はご了承ください。
上記のプロンプトをDeepLで翻訳して以下になりました。

Please send us your prompt refinements for the 14 conditions listed below. Please improve them as I am sure there are redundancies, etc.(all in English please )

Please note that the 14 conditions listed below are prompts for writing beautiful and readable code using the book "Readable Code".

We would like to use the prompts you output with our engineering team.

Thank you in advance for your cooperation.

プロンプトをもとにサンプルコードでいくつか実験

before1

良くないコードとして紹介されていたものを引用

https://qiita.com/shimataro999/items/ef6cd838d56f1fe87015#条件式は関数化して目的がわかるようにする

if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) { // 何を判定したい??
    // 何か処理
}

after1

ChatGPTにプロンプトを入力して提案された出力

/**
 * 指定された年が閏年かどうかを判断します。
 * 閏年の条件:
 * 1. 4で割り切れる年
 * 2. ただし、100で割り切れる年は除く
 * 3. ただし、400で割り切れる年は閏年とする
 * 
 * @param {number} year - 判断する年
 * @returns {boolean} - 閏年の場合はtrue、そうでない場合はfalse
 */
function isLeapYear(year) {
  const divisibleBy4 = year % 4 === 0;
  const divisibleBy100 = year % 100 === 0;
  const divisibleBy400 = year % 400 === 0;

  // 閏年の条件をチェック
  if (divisibleBy4 && !divisibleBy100 || divisibleBy400) {
    return true;
  } else {
    return false;
  }
}

// 使用例
// if (isLeapYear(year)) {
//     // 閏年の場合の処理
// }

主な改善点:

関数化と命名: 閏年判定ロジックを isLeapYear という明確な名前の関数に抽出しました。
コメントの改善: コードの意図や何をするかではなく、なぜその条件が必要かを説明するコメントを追加しました。
複雑な条件の分解: 閏年を判断する条件を変数に分割して、読み取りやすくしています。
コードの整列と可読性: 条件式を論理的に整理して、可読性を向上させました。

before2

良くないコードとして紹介されていたものを引用

変数は一度だけ書き込む

https://qiita.com/AKB428/items/20e81ccc8d9998b5535d#変数は一度だけ書き込む

var setFirstEmptyInput = function (new_value) {
    var found = false;
    var i = 1;
    var elem = document.getElementById('input' + i);
    while (elem !== null) {
        if (elem.value === '') {
            found = true;
            break;
        }
        i++;
        elem = document.getElementById('input' + i);
    }
    if (found) elem.value = new_value;
    return elem;
};

after2

ChatGPTにプロンプトを入力して提案された出力

/**
 * Updates the value of the first empty input field with the provided value.
 * Scans through consecutive input fields by incrementing IDs starting from 1 until it finds an empty one.
 * 
 * @param {string} newValue - The value to be assigned to the first empty input field.
 * @returns {HTMLElement | null} - The updated element or null if no empty input was found.
 */
function setFirstEmptyInput(newValue) {
    let index = 1;
    let currentElement = document.getElementById('input' + index);

    // Search for the first input field that is empty
    while (currentElement !== null) {
        if (currentElement.value === '') {
            currentElement.value = newValue;
            return currentElement;
        }
        index++;
        currentElement = document.getElementById('input' + index);
    }

    return null;
}

主な改善点:
関数名とパラメータ: 関数名をsetFirstEmptyInputに、パラメータをnewValueに変更して、その役割を明確にしました。

変数名: foundとelemをより記述的な名前(currentElement)に変更し、foundを完全に削除して、明確さを向上させるために直接リターンを使用しました。

コメント: 関数の振る舞い、パラメータ、戻り値の明確な説明を提供するために、関数の開始時にドキュメンテーションコメントを強化しました。

直接リターン: 空のフィールドが見つかった時点で直接修正された要素を返すことで、フラグの使用を減らし、可読性を向上させました。

コードの単純さ: ロジックは直接的で、明確な繰り返しと停止条件に焦点を当て、最初の空の入力フィールドを見つけて更新することに集中しています。

before3

JsだけでなくJavaでも実験
コードはこちらから引用
https://zenn.dev/suzuki_hoge/books/2022-12-colored-code-e73c0f9c56464c/viewer/1-difficulty

public void run(Request request) {
  var parsedRequest = parseCoupon(request)
  var sortedAndParsedRequest = sortNumbers(parsedRequest)
  
  var title = ""
  
  var price = sortedAndParsedRequest.getTotalPrice()
  
  if (sortedAndParsedRequest.isValidCoupon())
      price *= 0.95
  
  if (5 <= sortedAndParsedRequest.getCount())
      price *= 0.97
  
  var user = getUser(request.id)
  
  if (user.isPremiumPlan())
      title += "【 優先配送 】"
      deliver(user, sortedAndParsedRequest, true)
      if (10000 <= price)
          title += " 購入完了のお知らせ"
          sendSms(user, title, price)
  
  deliver(user, sortedAndParsedRequest, false)
  charge(user, price)
  sendMail(user, title, price)
}

after3

/**
 * ユーザーのリクエストに基づいて処理を行うメソッドです。
 * リクエストからクーポン情報を解析し、処理の流れを適切に制御します。
 *
 * @param request ユーザーからのリクエスト情報
 */
public void processRequest(Request request) {
    CouponDetails couponDetails = parseAndSortCoupon(request);
    double price = calculateDiscountedPrice(couponDetails);
    User user = fetchUserDetails(request.userId);

    String notificationTitle = generateNotificationTitle(user, price);
    executeDeliveryAndNotifications(user, couponDetails, notificationTitle, price);
}

private CouponDetails parseAndSortCoupon(Request request) {
    CouponDetails parsedCoupon = parseCoupon(request);
    return sortCouponNumbers(parsedCoupon);
}

private double calculateDiscountedPrice(CouponDetails couponDetails) {
    double price = couponDetails.getTotalPrice();
    if (couponDetails.isValidCoupon()) {
        price *= 0.95;
    }
    if (couponDetails.getCount() >= 5) {
        price *= 0.97;
    }
    return price;
}

private User fetchUserDetails(String userId) {
    return getUser(userId);
}

private String generateNotificationTitle(User user, double price) {
    String title = "";
    if (user.isPremiumPlan()) {
        title += "【 優先配送 】";
        if (price >= 10000) {
            title += " 購入完了のお知らせ";
        }
    }
    return title;
}

private void executeDeliveryAndNotifications(User user, CouponDetails couponDetails, String title, double price) {
    if (user.isPremiumPlan()) {
        deliver(user, couponDetails, true);
        if (price >= 10000) {
            sendSms(user, title, price);
        }
    }
    deliver(user, couponDetails, false);
    charge(user, price);
    sendEmail(user, title, price);
}

主な改善点:

関数の分割: 各タスクを個別の関数に分けてモジュール性とテストのしやすさを向上させました。
明確な命名: 変数と関数の名前を具体的にし、その役割が一目でわかるようにしました。
条件文の明確化: 複雑な条件文を簡素化し、早期リターンを活用してネストを減らしました。
ドキュメントコメントの追加: 各関数の目的とパラメータを説明するコメントを付けて、他の開発者がコードを理解しやすくしました。

まとめ

正直なところ、参考にした記事に記載されていたモデルコードとは多少のズレを感じましたが、改善の余地はありつつも、どのコードも改善前に比べて可読性は向上していると感じました。筆者のプロンプト組み立ての技術がまだ足りないようです。長いコードを入力すると同じ分量で出力が返ってくるのに対し、短いコードを入力すると予想以上に長い出力が返ってくるという問題がありました。この点について、より良いプロンプトの作成方法をご存知の方がいれば、ぜひコメントで教えていただければと思います。今後もコードレビューに関するAI技術に積極的に触れていきたいと考えています。

今回は記事を丁寧に読んでいただき、ありがとうございました!

Discussion