クリーンコードを学びました!

Posted date at 2024-06-27

Udemy

Vanilla

 CodeMafiaさんのUdemy講座でクリーンコードを学びました。クリーンコードを実践することで、可読性と保守性の高いコードを記述できるようになります。


🚀講座について

詳細は以下のUdemyリンクをご確認ください。

説明文

動画再生11時間  林がかけた時間12.75時間


🚀クリーンコードとは

 クリーンコードとは端的に言うときれいなコード、またはコードをきれいに書く技術のことを言います。

■ 汚いコードに見られる特徴

 汚いコードには次のような特徴があります。

 ・変数名があいまい。

 ・コードが読みづらい。

 ・規則性がない。

 ・記述が冗長。

 ・拡張性がない。

 ・保守性が低い。

 ・テストコードが書きづらい。

 汚いコードに良くあるケースはコードの記述に一貫性がなく何回も読み直さないと処理の内容が頭に入って来ないような記述になっているケースです。

 

■ きれいなコードに見られる特徴

 一方で、きれいなコード(クリーンコード)には次のような特徴があります。

 ・変数が適切な名前になっている。

 ・楽に読むことができる。

 ・一貫した規則性がある。

 ・冗長な記述がない。

 ・拡張性に優れる。

 ・既存機能の改修がしやすい。

 ・テストコードが書きやすい。

 処理の内容が追いやすく、コードからその処理の意図が伝わってくるようなコードはきれいなコードと言うことが出来ます。


🚀クリーンコードの例

  分かり易いものをいくつかピックアップして紹介します。

命名規則

 プロジェクト毎に使用するケースを統一して、冗長なく、意味のある適切な長さの変数名をつけます。

#ケバブケース-単語と単語をハイフンで繋ぐ :user-name 用途:url、html要素 #スネークケース-単語と単語をアンダースコアで繋ぐ 例:USER_PASSWORD、get_user_list 用途:url、パス、定数 、関数名、データベース定義 #パスカルケース:単語の先頭を大文字にする 例:PascalCase、Users 用途:クラス名、コンポーネント名 #キャメルケース:単語の先頭を小文字にする 例:uerName、getUserList 用途:変数名、関数名 /*----------------------------------------------------*/ #意味のない変数名 ×const hms ="10:20:45"; ⇒ 〇const currentTime = "10:20:45"; /*----------------------------------------------------*/ #冗長な記述 ×const UserData  ⇒ 〇User ×const UserInfo ⇒ 〇User /*----------------------------------------------------*/ #うその変数名 ×const findNumber=(list)=>{ return list.filter((item)=>typeof item ==="number"); }; const findNumberArray=(list)=>{ return list.filter((item)=>typeof item ==="number"); }; /*----------------------------------------------------*/ #変数名を省略するコツ idx-index srvc-serveice /*----------------------------------------------------*/ #開発で使用されるもの _privateMethod、_privateProp:アンダースコア⇒クラスのプライベート変数

コメント

本来はコメントではなく、コードで意図を表せることがベストです。

#悪いコメント ##コメントではなくコードで意図を示す × //未成年ならtrueを返す function getBoolean(num){ return num <18; } function isAdult(age){ return age <18; } ##冗長なコメント // タイプ --- ① const type = { ADD: "ADD", // 追加 --- ①,③ REMOVE: "REMOVE", // 削除 --- ①,③ UPDATE: "UPDATE", // 更新 --- ①,③ CLEAR: "CLEAR", // クリア --- ①,③ }; ## 悪いコメントの一覧 1. なんの付加情報ももたらさない意味のないコメント 2. 冗長なコメント 3. 形式的なコメント - メンバ、プロパティ一つ一つへのコメントなど 4. コメントアウトされたコード 5. 位置取りのための強調コメント - 場合によっては追加可能 - 他の場所への誘導。使用上の注意点など 6. 改修履歴コメント 7. バージョン管理のためのコメント - バージョン管理システムで管理しましょう /*----------------------------------------------------*/ #良いコメント ##関数を使用する際にに役立つ説明 /** * @param {"ja" | "en" | "fr" | "zh" } lang * @returns {string} */ function getGreeting(lang) { return greetings[lang]; } ##TODOに関するコメント /** * @param { Payload } payload --- ④ * @param { Array<Item> } oldValue * @returns */ function trigger(payload, oldValue) { // ====================================================== // triggerには個別の処理は実装しないでください。 // 個別の処理を実装する場合には○○の方に実装してください。 --- ②, ③ // ====================================================== / ## 良いコメントの一覧 1. TODO コメント 2. 使用上の注意 3. 使用上の警告 4. 公開するライブラリのための API コメント 5. コピーライトコメント 6. パフォーマンスなどに関わるコメント 7. 書き方としてややこしくなるのが避けられない処理へのコメント - 正規表現の意図を表すコメントなど

変数の定義

 スコープは常に最小にし、関数の内部で直接グローバル変数を呼ばないようにします。

#マジックナンバーを避ける × setTimeout(()=>{ ... },500); //要素のフェードインが完了するのを待っている。という意図が読み取れる。 const FADE_DURATION = 500; setTimeout(()=>{ ... },FADE_DURATION); /*----------------------------------------------------*/ #配列の添え字を多用しない × const pattern = /[\/\.\-]/; const date = "1996/03/01"; const splitDate = date.split(pattern); console.log(`${splitDate[0]}${splitDate[1]}${splitDate[2]}`); const pattern = /[\/\.\-]/; const date = "1996/03/01"; const [year, month, day] = date.split(pattern); console.log(`${year}${month}${day}`); /*----------------------------------------------------*/ #変数を使いまわさない × const PI = 3.14; let size = 30; size = PI * size * size; console.log(`円の面積は${size}です`); const PI = 3.14; const radius = 30; const circularArea = PI * radius * radius; /*----------------------------------------------------*/ #スコープを広げない × const PRICE_TABLE = { kidOrSenior: 300, junior: 500, adult: 700, // vip: 0 }; function getEntryPrice(age) { if (typeof age !== "number" || age < 0) throw new Error("値が正しくありません"); if (age <= 6 || age >= 60) return PRICE_TABLE.kidOrSenior; if (age <= 15 && age >= 7) return PRICE_TABLE.junior; return PRICE_TABLE.adult; } function getEntryPrice(age) { const PRICE_TABLE = { kidOrSenior: 300, junior: 500, adult: 700, }; if (typeof age !== "number" || age < 0) throw new Error("値が正しくありません"); if (age <= 6 || age >= 60) return PRICE_TABLE.kidOrSenior; if (age <= 15 && age >= 7) return PRICE_TABLE.junior; return PRICE_TABLE.adult; } /*----------------------------------------------------*/ #レキシカルスコープを使用しない-関数の引数を通して値を受け渡す × // 関数 greeting から見たレキシカルスコープ const user = { name: "田中" }; function greeting() { console.log(user.name); } function changeUserName(name) { user.name = name; } changeUserName("小林"); greeing(); const user = { name: "田中" }; // <- 関数 greeting から見たレキシカルスコープ function greeting(user) { console.log(user.name); } function changeUserName(user, name) { // <- 関数 changeName から見たレキシカルスコープ user.name = name; } function updateUserProfile(user, inputs) { // 何らかの処理 changeUserName(user, inputs.name); } function displayUserProfile(user) { // 何らかの処理 greeing(user); }

関数の定義

 具体的な名前で定義します。同じ処理は繰り返し書かない方がいいが、目的が違う場合は共通化しない方が良いです。

#具体的な名前を付ける # 関数はなるべく小さくし、一つの責務のみ行う function log() {} function print() {} function search(keyword) {} function sendServerLog() {} function renderProfilePage() {} function searchByFavorite(favorite) {} /*----------------------------------------------------*/ # 関数はなるべく小さくし、一つの責務のみ行う × function validate(type, value) { if (type === "mail") { const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); return { isError: !isValid, message: isValid ? "" : "Invalid email address", }; } else if (type === "tel") { const isValid = value.match(/^\d{2,4}-\d{2,4}-\d{3,4}$/); return { isError: !isValid, message: isValid ? "" : "Invalid tel number", }; } else if (type === "name") { const isValid = value.match(/^[a-zA-Z]{2,}$/); return { isError: !isValid, message: isValid ? "" : "Invalid name", }; } } validate("mail", "hoge@example.com"); validate("tel", "090-1234-5678"); validate("name", "hoge"); function validateMail(value) { const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); return { isError: !isValid, message: isValid ? "" : "Invalid email address", }; } function validateTel(value) { const isValid = value.match(/^\d{2,4}-\d{2,4}-\d{3,4}$/); return { isError: !isValid, message: isValid ? "" : "Invalid phone number", }; } function validateName(value) { const isValid = value.match(/^[a-zA-Z]{2,}$/); return { isError: !isValid, message: isValid ? "" : "Invalid name", }; } validateMail("hoge@example.com"); validateTel("090-1234-5678"); validateName("hoge"); /*----------------------------------------------------*/ ## DRY(ドライコード)を意識する 「Don't Repeat Yourself」は同じコードを繰り返し書いてはいけないという原則です。 × const drinks = ["lemonade", "soda", "tea", "water"]; const foods = ["beans", "chicken", "rice"]; function printDrink(drinks) { for (let i = 0; i < drinks.length; i++) { console.log(drinks[i]); } } function printFood(foods) { for (let i = 0; i < foods.length; i++) { console.log(foods[i]); } } printDrink(drinks); printFood(foods); const drinks = ["lemonade", "soda", "tea", "water"]; const foods = ["beans", "chicken", "rice"]; function printArray(array) { for (let i = 0; i < array.length; i++) { console.log(array[i]); } } printArray(drinks); printArray(foods);

繰り返し処理

 forループは何でもできてしまうので避けます。高階関数を使うことで読み手に意図が伝わり易くなります。

#高階関数を使う(forループは1番高速だが、可読性にかけるため) ##forEach関数(配列の処理) × const fruits = ["apple", "tomato", "banana"]; for (let i = 0; i < fruits.length; i++) { // ループの終了条件やカウンタなどの設定が必要 console.log(fruits[i]); } const fruits = ["apple", "tomato", "banana"]; fruits.forEach(function (fruit) { // 初期設定不要で効率よく記述ができる console.log(fruit); }); /*----------------------------------------------------*/ ##map関数(配列の生成) × const fruits = ["apple", "melon", "banana"]; let fruitsJuice = []; for (let i = 0; i < fruits.length; i++) { fruitsJuice[i] = fruits[i] + "juice"; } console.log(fruitsJuice); const fruits = ["apple", "melon", "banana"]; const fruitsJuice = fruits.map(function (data) { return data + "juise"; //戻り値として加工する内容を記述する }); /*----------------------------------------------------*/ ##filter関数(配列の要素の抽出) × const nations = [ { name: "Japan", region: "Asia" }, { name: "Italy", region: "Europe" }, { name: "China", region: "Asia" }, { name: "France", region: "Europe" }, ]; const resultNations = []; for (let i = 0; i < nations.length; i++) { if (nations[i].region === "Asia") { resultNations.push(nations[i]); } } console.log(resultNations); const nations = [ { name: "Japan", region: "Asia" }, { name: "Italy", region: "Europe" }, { name: "China", region: "Asia" }, { name: "France", region: "Europe" }, ]; const resultNations = nations.filter(function (nation) { return nation.region === "Asia"; }); console.log(resultNations);

条件分岐

 見ずらい条件分岐は関数化します。早期リターンを使用し、ネストを回避します。

#複雑な条件式は関数に切り出す × if (gender === "male" && (age === 25 || age === 42 || age === 61)) { console.log("厄年です"); } else if ( gender === "female" && (age === 19 || age === 33 || age === 37 || age === 61) ) { console.log("厄年です"); } function isUnluckyYear(age, gender) { const UNLUCKY_YEAR = { male: [25, 42, 61], female: [19, 33, 37, 61], }; return UNLUCKY_YEAR[gender].find((num) => num === age); } if (isUnluckyYear(age, gender)) { console.log("厄年です"); } /*----------------------------------------------------*/ #定数を左辺に配置する × if (value === 10) // 厳密比較 if (value == 10) // 型変換あり if (value = 10) // 代入 if ( 10 === value ) if ( 10 == value ) if ( 10 = value ) // エラー /*----------------------------------------------------*/ #switch 文よりも連想配列(オブジェクト)を利用する × function sendInviteEmail(loginEmail) { let emailContent; switch (loginEmail) { case "js-taro@example.com": emailContent = getEmailContent("JS タロウ"); sendEmail("js-taro@example.com", emailContent); break; case "python-taro@example.com": emailContent = getEmailContent("PYTHON タロウ"); sendEmail("python-taro@example.com", emailContent); break; case "java-taro@example.com": emailContent = getEmailContent("JAVA タロウ"); sendEmail("java-taro@example.com", emailContent); } } function getEmailContent(fullname) { return `${fullname} 様、こんにちは。○○サービスにようこそ。`; } const EMAIL_LIST = { "js-taro@example.com": "JS タロウ", "python-taro@example.com": "PYTHON タロウ", "java-taro@example.com": "JAVA タロウ", }; function sendInviteEmail(loginEmail) { const fullname = EMAIL_LIST[loginEmail]; let emailContent = getEmailContent(fullname); sendEmail(loginEmail, emailContent); } /*----------------------------------------------------*/ #早期 return を使う⇒ネストを無くす × function isActiveUser(user) { if (user != null) { if ( user.startDate <= today && (user.endDate == null || today <= user.endDate) ) { if (user.stopped) { return false; } else { return true; } } else { return false; } } else { return false; } } function isActiveUser(user) { if (user === null) return false; if (user.startDate > today) return false; if (!(user.endDate == null || today <= user.endDate)) return false; if (user.stopped) return false; return true; }

🚀まとめ

 上記は理解しやすい例ですが、より発展的な内容も学びました。デザインパターンに通じる、クラスを使用する上での原則についても学びました。

 アプリはリリースしてからがスタートで、機能追加や変更がどんどん行われていきます。自分のコードが技術的負債にならないように、クリーンコードを意識して、開発していきます。

 これまではどちらかというと「動けば正解」でしたが、これからは、原則はこうで、それは知ったうえで自分はこう書いた、といえるようにしたいと思います。

←ホームに戻る