🪴

クリーンなコードを追い求めて: 私のプログラムコードを作る際の今のイメージ

2025/01/21に公開

※ この記事は TechCommit AdventCalendar2024 の24日目の記事です。

https://adventar.org/calendars/10584

https://www.tech-commit.jp/

はじめに

この記事は、変数や関数の命名がなぜ大事なのか。クリーンなコードとはどんな感じなのかを、今自分が持っているイメージを言語化したアウトプットです。

また、記事を書く際にAIにも添削をしてもらうので、この際だから自分の知識とAIの添削を見て、良いコードとは何かを自分自身でも考える良い機会として記事にしました ꉂꉂ( ᐛ )

若干、個人の経験からのノウハウが入ってしまってるので一般的には、違う意見の方も多いかもしれません。ご指摘いただくか、自分なりの記事を書いていただいて議論を活発にしていただけますと幸いです٩( ᐛ )و

自己紹介

Webアプリケーションエンジニア(フロントエンド)の求職中のものです。

  • 職歴: SE(Java 7年弱)、Web(PHP 1年半)ほど。全体で8年ほどのエンジニア経験です
  • 悩み: 本や知識を取り入れるのが遅く、年数に対して未熟な部分が多い現状です
  • 現状: 「リーダブルコード」や「Clean Code」などの本も読んだことはあり、時々YouTubeで海外の人のクリーンなコードについての動画を見たりしています。「オブジェクト指向」や「デザインパターン」、「ドメイン駆動設計」などを本を何冊か読んだものの、その真髄や本質をまだ捉えられずにいて、引き続き本を読んで勉強しているところです

将来もエンジニアでいる間、仕事や趣味でコードを書く中で、よりクリーンなコードを追い求めるので、今回のこれが確定というわけでなく、またやっていく中で変わったり、違ってたと思い違う形に直していくと思います。
こういうこと考えてる人間もいる、こういう考え方もある程度で読んでいただけますと幸いです٩( ᐛ )و

サンプルコードはまとまっていませんが、こちらでも見ることができます。
https://github.com/mae616/cleanCode/tree/main/2024

要は言ってしまえば、この1つの内容だけだと思っています

★ 英語ベースで直感的に理解・変更できるコードにする

  • 例(ECサイト):カートへの商品追加
cart.js
const cart = {
    pendingItems: [{ id: "SYOUHIN001", count: 1, price: 500 }],
    totalCount: 1,
    totalPrice: 500,
};

// 合計アイテム数の計算
const calcTotalCount = () => {
    cart.totalCount = cart.pendingItems.reduce(
        (prev, current) => prev + current.count,
        0
    );
};

// 合計価格の計算
const calcTotalPrice = () => {
    cart.totalPrice = cart.pendingItems.reduce(
        (prev, current) => prev + current.price * current.count,
        0
    );
};

// カートに購入予定アイテムを追加する関数
function addItemToCart(itemId, itemCount, itemPrice) {
    const existingItem = cart.pendingItems.find((item) => item.id === itemId);

    if (existingItem) {
        // 既存アイテムがあれば購入数を増加
        existingItem.count += itemCount;
    } else {
        // 新規アイテムを追加
        cart.pendingItems.push({
            id: itemId,
            count: itemCount,
            price: itemPrice,
        });
    }

    // 合計の再計算
    calcTotalCount();
    calcTotalPrice();
}

変数名と関数名が明確で、何の処理をしているのかわかるコードを心がけて書いてみました。

過去にOSSのコードやAlfredのWorkflowなどの海外の人のコードを見た時、私はRubyとかわからない言語はたくさんあります。そういう知らない言語のもので自分が何かカスタマイズしたい時、変数名や関数名などコードを読むだけで、大体どこを変更すれば分かり修正できるものになっていました。
私の中で、そういった「言語を知らなくても英語の知識や翻訳を使えば、何をしているのか分かるコード」というのが、「クリーンなコード」の一つのお手本(目指すべきところ)になっています。

「クリーンコード」などを読んで、「この英単語でなくこちらの英単語を使った方がいい」といった記述がありましたが、結局それは、「英語として処理の意味に適してる英単語を使いましょう」て内容だと自分は理解しています。

また、変数名や関数名はユビキタス言語を使えるなら、設計からコードも一貫してユビキタス言語で命名するのがいいと思っています。設計者やチーム全員、関わってる人の中で共通認識のある単語を使うのが一番いいと思っています。(命名の長さについても大事ですが、この記事では割愛します。他の方と同じ長すぎないのがいいですね、て意見です)

ちなみに日本語でコメントを書いてますが、どれだけ「英語ベースで直感的に理解・変更できるコード」にしたとしても、日本人は英語話者ではないので、日本語のコメントは必須だと思っています。コードがプログラミングとして何をしているかでなく、ドメインとしてこの部分は何の処理をしているかをコメントで表すようにしています。

その中にある細々とした、作る時のイメージ

(1) 命名により、変数とデータ(情報)が紐づく

  • 例(マッチングアプリ):ユーザーの好みデータ
javascript
// ユーザーの理想のタイプ
const userIdealType = {
    ageRange: { min: 25, max: 35 },
    hobbies: ["hiking", "reading"],
    locations: ["Tokyo"],
};

変数 と言うのは時に、情報、状態、再利用する設定 などを表します。
変数が情報を表すとき、命名により「なんの情報を表しているか?」を自分以外の人が読むときに適切に想像させることのできるコードを心がけています。

userIdealType は「ユーザーの理想のタイプの情報」であることを表しています。
変数名を適切に設定することで、もしこの userIdealType を使用している機能に遭遇したらあなたはどう思いますか?
"「ユーザーの理想のタイプの情報」を使用する機能...「マッチング」とか「検索」の時使うな"、と想像することができるのではないでしょうか?時には"この機能で「ユーザーの理想のタイプの情報」は使う必要はないのでは?"と疑問に思うこともできます。
変数の命名をわかりやすくすることで、データの内容がわかりコードの意図が明確になり、後からコードを読み返す際の負担を減らすことができます。

(2) 命名により、関数と動作(機能)が紐づく

  • 例(タスク管理アプリ):タスクの表示、タスクの追加 など
task.js
const uuid = () => crypto.randomUUID();

const taskStatus = Object.freeze({
    PENDING: "pending",
    ACTIVE: "active",
    COMPLETED: "completed",
});

const allTasks = [
    { id: uuid(), title: "牛乳を買う", status: taskStatus.PENDING },
    { id: uuid(), title: "大福を食べる", status: taskStatus.ACTIVE },
    { id: uuid(), title: "猫を洗う", status: taskStatus.COMPLETED },
];

// 完了以外のタスクを取得する
function getTasks() {
    const notCompleteTasks = allTasks.filter(
        (task) => task.status !== taskStatus.COMPLETED
    );
    return notCompleteTasks;
}

// 全てのタスクを取得する
function getAllTasks() {
    return allTasks;
}

// タスクを追加する
function addTask(newTaskTitle) {
    const newTask = {
        id: uuid(),
        title: newTaskTitle,
        status: taskStatus.PENDING,
    };
    allTasks.push(newTask);
    return newTask;
}

// タスクのステータスを更新する
function updateTaskStatus(targetTaskId, newStatus) {
    const targetTask = allTasks.find((task) => task.id === targetTaskId);
    if (!targetTask) {
        return null;
    }
    targetTask.status = newStatus;
    return targetTask;
}

// 使用例
console.log(getTasks());

addTask("昼寝をする");
console.log(getTasks());

関数名はその 関数 が実行する動作や機能を反映するものが理想です。名前を見て、関数の役割が明確に伝わることで、コードを読む人がすぐにその機能を理解できるようになります。しかし、命名があまりに詳細すぎると、仕様変更に柔軟に対応できなくなる可能性があります。

少し例え話をしましょう。
会社で「経理の担当」「事務の担当」「総務の担当」と居ます。会社はいろいろな担当の人が働くことで成り立ってます。

しかし、別の部署の人が「経理の担当」「事務の担当」「総務の担当」が実際具体的に何をしているか知っていますか?それは全てを知る必要のあることでしょうか?
私たちは「経理の担当」に《交通費などの請求書》を渡す、「事務の担当」に《出勤情報》を連絡する、「総務の担当」に《消費した備品》の連絡をする、それだけ知ってれば会社は回ります。「なんの担当」に「なんの情報を渡す」と「どう言う結果が返ってくる」だけわかっていれば、十分です。全てを知る必要がなく他の人が担当の箇所はその担当の人が手順や内容を知っていれば問題ありません。

コードでも詳細な部分は必要なときに読めばいいと思っています。そのため命名だけで全体を大まかに理解できるようなものを目指しています。関数名だけで詳細がわかるのではなく、関数名と変数名が組み合わさることで、コードの意図や動作を他の人が理解できるよう心がけています。

関数名の抽象度のバランスが難しくあまりに抽象的すぎても意味が伝わりづらくなるので、関数が担当するドメイン的な責任を明確に示す名前を付けることがベストかなと思っていますが、伝わりにくい場合は必要に応じてコメントを使って補足を加えるのも有用だと思います。

関数名や変数名は、チーム内やビジネスサイド、関係者全員が名前を見れば何をしているか想像できるような、共通の認識を持った命名することなどが理想だと考えています。

(3) オブジェクト指向による実世界の概念との紐づけ(モデル化)

  • 例(マッチングアプリ):ユーザーとマッチ条件のクラス
matching.js
// 理想のタイプ(マッチング条件)
class IdealType {
    #minAge;
    #maxAge;
    #hobbies;
    #locations;
    constructor(minAge, maxAge, hobbies, locations) {
        this.#minAge = minAge;
        this.#maxAge = maxAge;
        this.#hobbies = hobbies;
        this.#locations = locations;
    }

    // マッチするかどうかを判定
    match(candidate) {
        return (
            candidate.age >= this.#minAge &&
            candidate.age <= this.#maxAge &&
            this.#locations.includes(candidate.location) &&
            this.#hobbies.some((hobby) => candidate.hobbies.includes(hobby))
        );
    }
}

// 想い人(ユーザー、誰かを愛そうとする人って意味でここでは使ってます)
class LovedOne {
    constructor(name, age, hobbies, location) {
        this.name = name;
        this.age = age;
        this.hobbies = hobbies;
        this.location = location;
        this.idealType = null; // 理想のタイプは最初は設定されていない
    }

    // 理想のタイプを想像した!(理想のタイプを設定)
    imagineIdealType(idealType) {
        this.idealType = idealType;
    }
}

// キューピッドがマッチングする(マッチング機能)
class CupidMatching {
    #criteria;
    constructor(lovedOne) {
        if (!lovedOne || !lovedOne.idealType) {
            throw new Error("理想のタイプが設定されていません");
        }
        this.#criteria = lovedOne.idealType; // 想い人の理想のタイプを基準にする
    }

    // マッチング判定
    match(candidate) {
        if (!candidate) {
            throw new Error("ユーザー情報が無効です");
        }

        if (this.#criteria.match(candidate)) {
            return `${candidate.name}とのマッチング成立!`;
        } else {
            return `${candidate.name}とのマッチングは成立しませんでした。`;
        }
    }
}

// 使用例
const alice = new LovedOne("Alice", 28, ["music", "travel", "food"], "Tokyo");
alice.imagineIdealType(
    new IdealType(25, 30, ["music", "travel"], ["Tokyo", "Osaka"])
);
const cupid = new CupidMatching(alice); // Aliceはキューピッドにマッチングをお願いした!

const charlie = new LovedOne("Charlie", 32, ["dance", "art"], "Kyoto");
console.log(cupid.match(charlie)); // Charlieとのマッチングは成立しませんでした

const bob = new LovedOne("Bob", 29, ["dance", "travel", "cat"], "Tokyo");
console.log(cupid.match(bob)); // Bobとのマッチング成立!

オブジェクト指向 により、現実世界のヒト、モノ、コトをコード上でクラスやオブジェクトとして表すことができます。
上記の例はかなりふざけた例ですが、オブジェクト指向をこのような表現のヒト、モノ、コトで表すこともできます。現実世界をファンタジーで比喩した表現の命名ですね。

少しロマンチストな言葉で言うと「命名とは、表現すること」なのかもしれません。(『リファラジ』でも言われていた気がします)

LovedOneUser と表すこともできます。
何をどの言葉で表せば一番誰もが意図や役割がわかりやすく理解できるのか、読みやすく変更しやすくなるか、と言うことに思えます。ただ、それは個人でやることでなく、関係者で共通の認識を作る必要があります。

注意していること

[1] 変数の責任は一つに限定

  • 例(SNSアプリ):自分の投稿の読み込みといいね数の確認

❌ アンチパターン

fetchMyPosts.js
function fetchMyPostsUseCase() {
    // ログインしているかを確認
    let flag = checkIfLoggedIn(); // ログインしてるかの確認
    if (!flag) {
        return "ログインしていません";
    }

    flag = false; // いいねされた投稿があるかの判定用にフラグを再利用 !!フラグの意味が変わっている!!
    const myPosts = getMyPosts();

    // いいねされた投稿があるか確認
    let totalLikeCount = 0;
    for (const post of myPosts) {
        const likeCount = post.likeCount;
        if (likeCount > 0) {
            flag = true;
            totalLikeCount += likeCount;
        }
    }

    // メッセージの作成
    let message = "";
    if (flag) {
        message = `いいねされた投稿が${totalLikeCount}件あります`;
    }

    return { myPosts, message };
}
全文のコード
fetchMyPosts.js
import bcrypt from "bcrypt";

const uuid = () => crypto.randomUUID();

const users = [
    {
        id: uuid(),
        isAdmin: true,
        userName: "Eric",
        password: await encryptPassword("password1"),
    },
    {
        id: uuid(),
        isAdmin: false,
        userName: "Alice",
        password: await encryptPassword("password2"),
    },
];

const posts = [
    {
        id: "POST20241224001",
        date: "2024-12-24",
        author: "Eric",
        body: "こんにちは。今日は素晴らしい日ですね🎄",
    },
    {
        id: "POST20241224002",
        date: "2024-12-24",
        author: "Alice",
        body: "あなたの元にもサンタさんは来ましたか?🎅",
    },
];

const postLikes = [
    {
        id: `like${uuid()}`,
        postId: "POST20241224001",
        likedBy: "Alice",
    },
    {
        id: `like${uuid()}`,
        postId: "POST20241224001",
        likedBy: "Bob",
    },
];

let loggedInUser = null;

// パスワードの暗号化
async function encryptPassword(password) {
    return await bcrypt.hash(password, 10);
}

async function login(userName, password) {
    // array.find()の非同期版
    const asyncFind = async (array, predicate) => {
        for (const item of array) {
            if (await predicate(item)) {
                return item;
            }
        }
    };

    const matchUser = await asyncFind(
        users,
        async (user) =>
            user.userName === userName &&
            (await bcrypt.compare(password, user.password))
    );

    if (matchUser) {
        loggedInUser = matchUser;
        return true;
    }
    return false;
}

function checkIfLoggedIn() {
    return loggedInUser !== null;
}

function getPostLikeCount(postId) {
    const like = postLikes.filter((like) => like.postId === postId);

    if (like) {
        return like.length;
    }

    return 0;
}

function getMyPosts() {
    const fetchMyPost = posts.filter(
        (post) => post.author === loggedInUser.userName
    );

    const resultMyPost = fetchMyPost.map((post) => {
        return { ...post, likeCount: getPostLikeCount(post.id) };
    });

    return resultMyPost;
}

function fetchMyPostsUseCase() {
    // ログインしているかを確認
    let flag = checkIfLoggedIn(); // ログインしてるかの確認
    if (!flag) {
        return "ログインしていません";
    }

    flag = false; // いいねされた投稿があるかの判定用にフラグを再利用 !!フラグの意味が変わっている!!
    const myPosts = getMyPosts();

    // いいねされた投稿があるか確認
    let totalLikeCount = 0;
    for (const post of myPosts) {
        const likeCount = post.likeCount;
        if (likeCount > 0) {
            flag = true;
            totalLikeCount += likeCount;
        }
    }

    // メッセージの作成
    let message = "";
    if (flag) {
        message = `いいねされた投稿が${totalLikeCount}件あります`;
    }

    return { myPosts, message };
}

// 使用例
console.log("ログイン画面です");
const loggedInResult = await login("Eric", "password1"); // ログイン
console.log(`ログイン結果: ${loggedInResult}`);

console.log("\n他画面でユーザーのMyPostsを確認します");

const myPosts = fetchMyPostsUseCase();
console.log(myPosts);

上記はかなり無理やりな例ですが、変数の債務を一つにすることを心がけています。
使い終わった変数の再利用や曖昧な意図で変数を使用してしまうとコードの意図が不明確になり、数ヶ月後の自分や他の人が変更するときにバグを生みやすくなります。
単一の責任を持つことで適切な命名もしやすくなり、コードの意図が明確になり、拡張時にも意図が伝わりやすく新たな不具合やリファクタリングを避けることができます。

✅ 改善例

fetchPosts.js
function fetchPostsUseCase() {
    // ログインしているかを確認
-   let flag = checkIfLoggedIn(); // ログインしてるかの確認
-   if (!flag) {
+   let isLoggedIn = checkIfLoggedIn(); // ログインしてるかの確認
+   if (!isLoggedIn) {
        return "ログインしていません";
    }

-   flag = false; // いいねされた投稿があるかの判定用にフラグを再利用 !!フラグの意味が変わっている!!
    const myPosts = getMyPosts();
+   let hasLikedPosts = false; // いいねされた投稿があるかのフラグ

    // いいねされた投稿があるか確認
    let totalLikeCount = 0;
    for (const post of myPosts) {
        const likeCount = post.likeCount;
        if (likeCount > 0) {
-           flag = true;
+           hasLikedPosts = true;
            totalLikeCount += likeCount;
        }
    }

    let message = "";
-   if (flag) {
+   if (hasLikedPosts) {
        message = `いいねされた投稿が${totalLikeCount}件あります`;
    }

    return { myPosts, message };
}

[2] 関数の責任も一つに限定

  • 例(SNSアプリ):成人判定、割引適用の判定

❌ アンチパターン

payment.js
function isUnder20(num) {
    return num < 20;
}

// 成人かどうかのチェック
function checkAdult(user) {
    return !isUnder20(user.age);
}

// 割引処理
function getDiscountedPrice(totalPrice) {
    if (!isUnder20(totalPrice)) {
        // 割引処理
        return totalPrice * 0.8;
    }
    return totalPrice;
}
全文のコード
payment.js
function isUnder20(num) {
    return num < 20;
}

// 成人かどうかのチェック
function checkAdult(user) {
    return !isUnder20(user.age);
}

// 割引処理
function getDiscountedPrice(totalPrice) {
    if (!isUnder20(totalPrice)) {
        // 割引処理
        return totalPrice * 0.8;
    }
    return totalPrice;
}

// 使用例
const user = {
    age: 10,
};
const isAdult = checkAdult(user);
console.log(`ユーザーは ${isAdult ? "成人" : "未成年"} です`);

const totalPrice = 1000;
const discountedPrice = getDiscountedPrice(totalPrice);
console.log(`通常価格: ${totalPrice}円、割引後の価格: ${discountedPrice}`);

関数も債務を一つにします。これも変数と同様の理由で意図が伝わりやすくなります。
関数でよくある例として、曖昧な役割を持つ関数をアンチパターンとして挙げてみました。関数は再利用性も重要です。けれど、DRY(重複しないコード)を意識するあまり、責任が曖昧で複数の意図で至る所から再利用された(呼び出された)関数は、いざ何かの仕様が変わり変更する際に、再利用したことが返ってボトルネックになることもあります。たとえば、汎用的な関数が複数の用途で呼ばれると、変更が必要になった際にその影響範囲が広がり、予期しない不具合を生む可能性があります。
汎用的なユーティリティ関数ならその意図、何か固有の役割を持つならその関数が持つ責任(役割)が明確になる命名と内容にするようにしています。

✅ 改善例

payment.js

- function isUnder20(num) {
-    return num < 20;
- }
+ function isAdult(age) {
+    return age >= 20;
+ }

// 成人かどうかのチェック
function checkAdult(user) {
-    return !isUnder20(user.age);
+    return isAdult(user.age);
}

+ function isDiscount(totalPrice) {
+    return totalPrice >= 20;
+ }

// 割引処理
function getDiscountedPrice(totalPrice) {
-    if (!isUnder20(totalPrice)) {
+    if (isDiscount(totalPrice)) {
        // 割引処理
        return totalPrice * 0.8;
    }
    return totalPrice;
}

[3] クラスやコンポーネントの責任も一つに限定

  • 例(SNSアプリ):投稿クラス

☑️ 元のコード

userPost.js
class UserPost {
    constructor(userId, title, content) {
        this.userId = userId;
        this.title = title;
        this.content = content;
        this.likes = 0;
        this.comments = [];
        this.createdAt = new Date();
    }

    // 投稿に関する処理
    addComment(comment, userId) {
        this.comments.push({ comment, userId });
    }

    addLike() {
        this.likes++;
    }

    // ユーザー関連の処理(本来はUserクラスにあるべき)
    getUserName() {
        return users.find((user) => user.id === this.userId).userName;
    }

    // 通知関連の処理(本来はNotificationクラスにあるべき)
    sendAddPostNotification() {
        const notification = {
            userId: this.userId,
            userName: this.getUserName(),
            message: "新しい投稿が作成されました",
            createdAt: new Date(),
        };
        return notification;
    }

    // 分析関連の処理(本来はAnalyticsクラスにあるべき)
    trackPostMetrics() {
        const analytics = {
            event: "post_created",
            userId: this.userId,
            timestamp: this.createdAt,
        };
        return analytics;
    }
}
全文のコード
userPost.js
const users = [
    {
        id: "user1",
        userName: "Eric",
        password: "password1",
    },
    {
        id: "user2",
        userName: "Alice",
        password: "password2",
    },
];

class UserPost {
    constructor(userId, title, content) {
        this.userId = userId;
        this.title = title;
        this.content = content;
        this.likes = 0;
        this.comments = [];
        this.createdAt = new Date();
    }

    // 投稿に関する処理
    addComment(comment, userId) {
        this.comments.push({ comment, userId });
    }

    addLike() {
        this.likes++;
    }

    // ユーザー関連の処理(本来はUserクラスにあるべき)
    getUserName() {
        return users.find((user) => user.id === this.userId).userName;
    }

    // 通知関連の処理(本来はNotificationクラスにあるべき)
    sendAddPostNotification() {
        const notification = {
            userId: this.userId,
            userName: this.getUserName(),
            message: "新しい投稿が作成されました",
            createdAt: new Date(),
        };
        return notification;
    }

    // 分析関連の処理(本来はAnalyticsクラスにあるべき)
    trackPostMetrics() {
        const analytics = {
            event: "post_created",
            userId: this.userId,
            timestamp: this.createdAt,
        };
        return analytics;
    }
}

// 使用例
const userPost = new UserPost("user1", "title1", "content1");
const notification = userPost.sendAddPostNotification();
console.log(notification);

クラスやコンポーネントの責任(役割)も一つにします。これも同じ理由ですね。
オブジェクト指向プログラミングで「責任を一つにする」、つまり「どの振る舞いをどのクラスに持たせるか」はすごく難しいところです。クラスが表すべき概念や役割の範囲を明確にしないと、不要な依存関係(複雑な関係)が生まれ、変更時に予期しない影響が広がることになります。
基本的には、登場する概念が一つであればそのクラスに振る舞いを実装し、複数の概念が登場する場合は、独立したクラスに実装しています。この辺は今後さらに経験を積み重ねて精進したいと考えています。

✅ 改善例

userPost.js
- class UserPost {
-   constructor(userId, title, content) {
-       this.userId = userId;
+ // 投稿に関する処理
+ class Post {
+   constructor(user, title, content) {
+       this.userId = user.id;
        this.title = title;
        this.content = content;
        this.likes = 0;
        this.comments = [];
        this.createdAt = new Date();
    }


-   // 投稿に関する処理
-   addComment(comment, userId) {
-       this.comments.push({ comment, userId });
-   }
+   addComment(comment, commentedUser) {
+       this.comments.push({
+           comment,
+           userId: commentedUser.id,
+           userName: commentedUser.userName,
+       });
+   }

    addLike() {
        this.likes++;
    }
}
全文のコード
userPost.js
// ユーザーデータの永続化を担当するリポジトリクラス
class UserRepository {
    constructor() {
        // 初期データをセット
        this.users = new Map([
            [
                "user1",
                {
                    id: "user1",
                    userName: "Eric",
                    password: "password1",
                },
            ],
            [
                "user2",
                {
                    id: "user2",
                    userName: "Alice",
                    password: "password2",
                },
            ],
        ]);
    }

    getUserName(userId) {
        return this.users.get(userId).userName;
    }
}

// ユーザー関連の処理
class User {
    constructor(userRepository, userId) {
        this.userRepository = userRepository;
        this.id = userId;
        this.userName = userRepository.getUserName(userId);
    }
}

// 投稿に関する処理
class Post {
    constructor(user, title, content) {
        this.userId = user.id;
        this.title = title;
        this.content = content;
        this.likes = 0;
        this.comments = [];
        this.createdAt = new Date();
    }

    addComment(comment, commentedUser) {
        this.comments.push({
            comment,
            userId: commentedUser.id,
            userName: commentedUser.userName,
        });
    }

    addLike() {
        this.likes++;
    }
}

// 通知関連の処理
class PostNotification {
    constructor(post, user) {
        this.post = post;
        this.user = user;
    }

    sendAddPostNotification() {
        const notification = {
            userId: this.post.userId,
            userName: this.user.userName,
            title: this.post.title,
            message: "新しい投稿が作成されました",
            createdAt: this.post.createdAt,
        };
        return notification;
    }
}

// 分析関連の処理
class AnalyticsService {
    trackPostMetrics(post) {
        const analytics = {
            event: "post_created",
            userId: post.userId,
            timestamp: post.createdAt,
        };
        return analytics;
    }
}
// 使用例
const user = new User(new UserRepository(), "user1");
const post = new Post(user, "title1", "content1");
const notification = new PostNotification(post, user);
console.log(notification.sendAddPostNotification());

[4] 関心の分離とカプセル化

  • 例(SNSアプリ):会員費支払いのためのクレジットカードの番号のチェック

☑️ 元のコード

checkCreditCard.js
// Luhnアルゴリズムによるクレジットカード番号のチェック
function checkCreditCardNumber(cardNumber) {
    let isEven = cardNumber.length % 2 === 0; // 偶数

    let sum = 0;
    for (var i = cardNumber.length - 1; i >= 0; i--) {
        let digit = Number(cardNumber[i]);
        if (isEven) {
            digit *= 2;
            if (digit > 9) {
                const [ten, one] = String(digit).split("");
                digit = Number(ten) + Number(one);
            }
        }
        sum += digit;
        isEven = !isEven;
    }
    return sum % 10 === 0;
}
全文のコード
checkCreditCard.js
// Luhnアルゴリズムによるクレジットカード番号のチェック
function checkCreditCardNumber(cardNumber) {
    let isEven = cardNumber.length % 2 === 0; // 偶数

    let sum = 0;
    for (var i = cardNumber.length - 1; i >= 0; i--) {
        let digit = Number(cardNumber[i]);
        if (isEven) {
            digit *= 2;
            if (digit > 9) {
                const [ten, one] = String(digit).split("");
                digit = Number(ten) + Number(one);
            }
        }
        sum += digit;
        isEven = !isEven;
    }
    return sum % 10 === 0;
}

// 使用例
console.log(checkCreditCardNumber("79927398713")); // true
console.log(checkCreditCardNumber("79927398715")); // false

責任(役割、債務)を分離できる処理は関数化(一つの関数として分離)し、独立した状態にするようにしています。また、その関数に適切な名前を付けることで、その処理が何をするものなのかが一目で分かり、ソースコードが読みやすく管理しやすくなります。要は、関数化と適切な命名により、その処理の責任が明確になり、変更が必要ない部分には触れなくて済むことを他の開発者に伝えることができます。
直接声をかけたり補足資料を作ったりせず、ソースコードだけで開発者の意図を伝えるのが「クリーンなコード」の本質だと思っています。

関数化を細かくしすぎるとかえって読みにくくなることもあるため、バランスが大切で難しいところだと思っています。次の段落で挙げる「寿命を短く(関数のスコープを短く)」をすることで、コードの理解に必要な範囲を限定し、多くの人にとって読みやすく、理解しやすいコードになるようにしてその辺のバランスを取るようにしています。

✅ 改善例

checkCreditCard.js
// Luhnアルゴリズムによるクレジットカード番号のチェック
function checkCreditCardNumber(cardNumber) {
+   // 偶数桁の計算
+   const even = (digit) => {
+       let result = digit * 2;
+       if (result > 9) {
+           const [ten, one] = String(result).split("");
+           result = Number(ten) + Number(one);
+       }
+       return result;
+   };

    // 本処理
    let isEven = cardNumber.length % 2 === 0; // 偶数
    let sum = 0;
    for (var i = cardNumber.length - 1; i >= 0; i--) {
-       let digit = Number(cardNumber[i]);
-
-       if (isEven) {
-           digit *= 2;
-           if (digit > 9) {
-               const [ten, one] = String(digit).split("");
-               digit = Number(ten) + Number(one);
-           }
-       }
+       const digit = Number(cardNumber[i]);
+
+       const calDigit = isEven ? even(digit) : digit;
        sum += calDigit;
        isEven = !isEven;
    }
    return sum % 10 === 0;
}

[5] 変数と関数の寿命を短くする

  • 例(SNSアプリ):フォローする処理

☑️ 元のコード

Follow.js
// フォローする処理
function followUserUseCase(userId, targetUserId) {
    let isLoggedIn;
    let isFollowed;
    let isBlocked;
    let isFollowLimit;
    let loggedInUser;
    let targetUser;
    const FOLLOW_LIMIT = 5000;

    // すでにログインしているかの確認
    isLoggedIn = checkIfLoggedIn();
    if (!isLoggedIn) {
        return "ログインしていません";
    }

    // すでにフォローしているかの確認
    isFollowed = getFollowUsers().some(
        (followUser) =>
            (followUser.from === userId && followUser.to) === targetUserId
    );
    if (isFollowed) {
        return { error: "すでにフォローしています" };
    }

    // ブロックされていないかの確認
    isBlocked = getBlockUsers().some(
        (blockUser) =>
            blockUser.from === targetUserId && blockUser.to === userId
    );
    if (isBlocked) {
        return { error: "フォローできません" };
    }

    // フォロー数の制限チェック
    if (!canFollowMore(userId, FOLLOW_LIMIT)) {
        return { error: "フォロー上限に達しました" };
    }

    // フォロー処理の実行
    follows.push({
        id: uuid(),
        from: userId,
        to: targetUserId,
    });
    return { success: "フォローしました" };
}
全文のコード
Follow.js
import bcrypt from "bcrypt";

const uuid = () => crypto.randomUUID();

const users = [
    {
        id: "USER00000001",
        userName: "Eric",
        password: await encryptPassword("password1"),
        age: 30,
        biography: "私は猫です🐈",
        hobbies: ["野球", "読書"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000002",
        userName: "Alice",
        password: await encryptPassword("password2"),
        age: 25,
        biography: "私は犬です🐕",
        hobbies: ["映画鑑賞", "旅行"],
        isSecret: false,
        isDelete: true,
    },
    {
        id: "USER00000003",
        userName: "Bob",
        password: await encryptPassword("password3"),
        age: 20,
        biography: "私は魚です🐟",
        hobbies: ["料理", "ゲーム"],
        isSecret: false,
        isDelete: false,
    },
];

const blocks = [
    {
        id: uuid(),
        from: "USER00000001", // EricがBobをブロック
        to: "USER00000003",
    },
];

const follows = [
    {
        id: uuid(),
        from: "USER00000003", // BobがAliceをフォロー
        to: "USER00000002",
    },
];

let loggedInUser = null;

// パスワードの暗号化
async function encryptPassword(password) {
    return await bcrypt.hash(password, 10);
}

async function login(userName, password) {
    // array.find()の非同期版
    const asyncFind = async (array, predicate) => {
        for (const item of array) {
            if (await predicate(item)) {
                return item;
            }
        }
    };

    const matchUser = await asyncFind(
        users,
        async (user) =>
            user.userName === userName &&
            (await bcrypt.compare(password, user.password))
    );

    if (matchUser) {
        loggedInUser = matchUser;
        return true;
    }
    return false;
}

function checkIfLoggedIn() {
    return loggedInUser !== null;
}

function getBlockUsers() {
    return blocks.filter((blockUser) => blockUser.blockBy === loggedInUser.id);
}

function getFollowUsers() {
    return follows.filter((followUser) => followUser.from === loggedInUser.id);
}

// フォロー数の制限チェック
function canFollowMore(FOLLOW_LIMIT) {
    // モック
    return true;
}

// フォローする処理
function followUserUseCase(userId, targetUserId) {
    let isLoggedIn;
    let isFollowed;
    let isBlocked;
    let isFollowLimit;
    let loggedInUser;
    let targetUser;
    const FOLLOW_LIMIT = 5000;

    // すでにログインしているかの確認
    isLoggedIn = checkIfLoggedIn();
    if (!isLoggedIn) {
        return "ログインしていません";
    }

    // すでにフォローしているかの確認
    isFollowed = getFollowUsers().some(
        (followUser) =>
            (followUser.from === userId && followUser.to) === targetUserId
    );
    if (isFollowed) {
        return { error: "すでにフォローしています" };
    }

    // ブロックされていないかの確認
    isBlocked = getBlockUsers().some(
        (blockUser) =>
            blockUser.from === targetUserId && blockUser.to === userId
    );
    if (isBlocked) {
        return { error: "フォローできません" };
    }

    // フォロー数の制限チェック
    if (!canFollowMore(userId, FOLLOW_LIMIT)) {
        return { error: "フォロー上限に達しました" };
    }

    // フォロー処理の実行
    follows.push({
        id: uuid(),
        from: userId,
        to: targetUserId,
    });
    return { success: "フォローしました" };
}

// 使用例
console.log("ログイン画面です");
const loggedInResult = await login("Eric", "password1");
console.log(`ログイン結果: ${loggedInResult}`);

if (loggedInResult) {
    console.log("\n他画面でフォロー処理をします");
    const followResult = followUserUseCase(loggedInUser.id, "USER00000002");
    console.log(followResult);
}

console.log(follows);

プログラムのコードを読むのはある意味ブロックを読むことだと思っています。
適切に段落分けされた(空改行などで分けられたブロックの)コードを読んで、関数全体の流れを大まかに把握し、個々の段落の内容を詳細に読んでいくような感覚でいつも読んでいます。
ただ、単純に空改行で分けるより関数化などに変数や関数のスコープを規則として限定し、どうあっても使用を想定する意図の外では変数を使えない、関数を使えないようにすると、[1] 変数の責任は一つに限定 であげた他の開発者による意図しない再利用を避けることができます。
他の開発者を信じて、チーム内で共通の認識を作り、意識としてみんなで意図しない使用をしないこともできますが、現代のプログラミングではコードを規則づけたり制限する方法が備わっています。そもそもコードとして規則を作って、使用方法を制限する方が人的の心理的な負荷も下げられる方法だと思っています。

また、変数と関数の寿命を短くすることにより他の開発者がコードを読んだ時、それぞれの変数や関数を気にしなくてはいけない範囲が限定され、コードの理解がしやすくなるメリットがあります。

✅ 改善例

Follow.js
+ // フォロー処理
+ function addFollow(userId, targetUserId) {
+   follows.push({
+       id: uuid(),
+       from: userId,
+       to: targetUserId,
+   });
+ }
+ 
+ // フォローされているかの確認
+ function isFollowedByUser(userId, targetUserId) {
+   return follows.some(
+       (followUser) =>
+           (followUser.from === userId && followUser.to) === targetUserId
+   );
+ }
+
+ // ブロックされているかの確認
+ function isBlockedByUser(userId, targetUserId) {
+  return blocks.some(
+       (blockUser) =>
+           (blockUser.from === targetUserId && blockUser.to) === userId
+   );
+ }
+
// フォローする処理
function followUserUseCase(userId, targetUserId) {
-   let isLoggedIn;
-   let isFollowed;
-   let isBlocked;
-   let isFollowLimit;
-   let loggedInUser;
-   let targetUser;
    const FOLLOW_LIMIT = 5000;

-   // すでにログインしているかの確認
-   isLoggedIn = checkIfLoggedIn();
-   if (!isLoggedIn) {
-       return "ログインしていません";
-   }
-
-   // すでにフォローしているかの確認
-   isFollowed = getFollowUsers().some(
-       (followUser) =>
-           (followUser.from === userId && followUser.to) === targetUserId
-   );
-   if (isFollowed) {
-       return { error: "すでにフォローしています" };
-   }
-
-   // ブロックされていないかの確認
-   isBlocked = getBlockUsers().some(
-       (blockUser) =>
-           blockUser.from === targetUserId && blockUser.to === userId
-   );
-   if (isBlocked) {
-       return { error: "フォローできません" };
-   }
-
-   // フォロー数の制限チェック
-   if (!canFollowMore(userId, FOLLOW_LIMIT)) {
-       return { error: "フォロー上限に達しました" };
-   }
+   // フォロー前のチェック処理
+   const checkBeforeFollow = (userId, targetUserId, FOLLOW_LIMIT) => {
+       // すでにログインしているかの確認
+       if (!checkIfLoggedIn()) {
+           return { error: "ログインしていません" };
+       }
+
+      // すでにフォローしているかの確認
+       const isFollowed = isFollowedByUser(userId, targetUserId);
+       if (isFollowed) {
+           return { error: "すでにフォローしています" };
+       }
+
+       // ブロックされていないかの確認
+       const isBlocked = isBlockedByUser(targetUserId, userId);
+       if (isBlocked) {
+           return { error: "フォローできません" };
+       }
+
+       // フォロー数の制限チェック
+       if (!canFollowMore(userId, FOLLOW_LIMIT)) {
+           return { error: "フォロー上限に達しました" };
+       }
+        return { success: true };
+   };
+
+   // フォロー前のチェック処理
+   const checkResult = checkBeforeFollow(userId, targetUserId, FOLLOW_LIMIT);
+   if (checkResult.error) {
+       return checkResult;
+   }

    // フォロー処理の実行
-   follows.push({
-       id: uuid(),
-       from: userId,
-       to: targetUserId,
-   });
+   addFollow(userId, targetUserId);
    return { success: "フォローしました" };
}

[6] コードの複雑化を避ける

  • 例(SNSアプリ):ユーザーの検索

☑️ 元のコード

searchUsers.js
function searchUseCase(isAdult, searchHobbies) {
    // ログインしているかを確認
    if (!checkIfLoggedIn()) {
        return "ログインしていません";
    }

    const blockUser = getBlockUsers(); // ブロックしているユーザーを取得
    const users = getUsers(); // ユーザー一覧を取得

    // 検索条件に合致するユーザーだけを取得
    // ※ 本来なら、検索条件で絞り込むためこここで条件を作るのは現実的ではないが...例として無理やりなコードで書いてます
    const resultUsers = [];
    for (const user of users) {
        if (
            user.id !== loggedInUser.id && // 自分自身は表示しない
            !blockUser.some((block) => block.blockTo === user.id) && // ブロックしているユーザーは表示しない
            ((isAdult && user.age >= 20) || !isAdult) && // 20歳以上のユーザーのみ表示
            ((searchHobbies?.length > 0 && // 趣味が指定されている場合
                searchHobbies.some((hobby) => user.hobbies.includes(hobby))) ||
                !searchHobbies) && // 指定した趣味のユーザーのみ表示
            !user.isSecret // 検索対象が鍵アカウント設定でないかを判定
        ) {
            resultUsers.push(user);
        }
    }

    const message =
        resultUsers.length > 0
            ? `条件に合致するユーザーが ${resultUsers.length}件 見つかりました`
            : "条件に合致するユーザーはいません";

    return { resultUsers, message };
}
全文のコード
searchUsers.js
import bcrypt from "bcrypt";

const uuid = () => crypto.randomUUID();

const users = [
    {
        id: "USER00000001",
        userName: "Eric",
        password: await encryptPassword("password1"),
        age: 30,
        biography: "私は猫です🐈",
        hobbies: ["野球", "読書"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000002",
        userName: "Alice",
        password: await encryptPassword("password2"),
        age: 25,
        biography: "私は犬です🐕",
        hobbies: ["映画鑑賞", "旅行"],
        isSecret: false,
        isDelete: true,
    },
    {
        id: "USER00000003",
        userName: "Bob",
        password: await encryptPassword("password3"),
        age: 20,
        biography: "私は魚です🐟",
        hobbies: ["料理", "ゲーム"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000004",
        userName: "Tom",
        password: await encryptPassword("password4"),
        age: 35,
        biography: "私は鳥です🐦",
        hobbies: ["読書", "料理"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000005",
        userName: "Ken",
        password: await encryptPassword("password5"),
        age: 40,
        biography: "私は熊です🐻",
        hobbies: ["釣り", "映画鑑賞"],
        isSecret: true,
        isDelete: false,
    },
    {
        id: "USER00000006",
        userName: "John",
        password: await encryptPassword("password6"),
        age: 45,
        biography: "私は猿です🐒",
        hobbies: ["旅行", "読書"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000007",
        userName: "Mike",
        password: await encryptPassword("password7"),
        age: 15,
        biography: "私は虫です🐞",
        hobbies: ["ゲーム", "野球"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000008",
        userName: "Chris",
        password: await encryptPassword("password8"),
        age: 50,
        biography: "私は蛇です🐍",
        hobbies: ["映画鑑賞", "野球"],
        isSecret: false,
        isDelete: false,
    },
];

const blockUsers = [
    {
        id: uuid(),
        blockTo: "USER00000004",
        blockBy: "USER00000001", // EricがTomをブロック
    },
    {
        id: uuid(),
        blockTo: "USER00000003",
        blockBy: "USER00000001", // EricがBobをブロック
    },
];

let loggedInUser = null;

// パスワードの暗号化
async function encryptPassword(password) {
    return await bcrypt.hash(password, 10);
}

async function login(userName, password) {
    // array.find()の非同期版
    const asyncFind = async (array, predicate) => {
        for (const item of array) {
            if (await predicate(item)) {
                return item;
            }
        }
    };

    const matchUser = await asyncFind(
        users,
        async (user) =>
            user.userName === userName &&
            (await bcrypt.compare(password, user.password))
    );

    if (matchUser) {
        loggedInUser = matchUser;
        return true;
    }
    return false;
}

function checkIfLoggedIn() {
    return loggedInUser !== null;
}

function getBlockUsers() {
    return blockUsers.filter(
        (blockUser) => blockUser.blockBy === loggedInUser.id
    );
}

function getUsers() {
    return users
        .filter((user) => !user.isDelete) // 論理削除されていないユーザーを取得
        .map((user) => {
            return {
                id: user.id,
                userName: user.userName,
                age: user.age,
                biography: user.biography,
                hobbies: user.hobbies,
                isSecret: user.isSecret,
            };
        });
}

function searchUseCase(isAdult, searchHobbies) {
    // ログインしているかを確認
    if (!checkIfLoggedIn()) {
        return "ログインしていません";
    }

    const blockUser = getBlockUsers(); // ブロックしているユーザーを取得
    const users = getUsers(); // ユーザー一覧を取得

    // 検索条件に合致するユーザーだけを取得
    // ※ 本来なら、検索条件で絞り込むためこここで条件を作るのは現実的ではないが...例として無理やりなコードで書いてます
    const resultUsers = [];
    for (const user of users) {
        if (
            user.id !== loggedInUser.id && // 自分自身は表示しない
            !blockUser.some((block) => block.blockTo === user.id) && // ブロックしているユーザーは表示しない
            ((isAdult && user.age >= 20) || !isAdult) && // 20歳以上のユーザーのみ表示
            ((searchHobbies?.length > 0 && // 趣味が指定されている場合
                searchHobbies.some((hobby) => user.hobbies.includes(hobby))) ||
                !searchHobbies) && // 指定した趣味のユーザーのみ表示
            !user.isSecret // 検索対象が鍵アカウント設定でないかを判定
        ) {
            resultUsers.push(user);
        }
    }

    const message =
        resultUsers.length > 0
            ? `条件に合致するユーザーが ${resultUsers.length}件 見つかりました`
            : "条件に合致するユーザーはいません";

    return { resultUsers, message };
}

// 使用例
console.log("ログイン画面です");
const loggedInResult = await login("Eric", "password1");

console.log("\n他画面でユーザーの検索一覧を確認します");
const searchResult = searchUseCase(true, ["野球", "読書"]);
console.log(searchResult);

これも [4] 関心の分離とカプセル化 に通じています。複雑なコード、複雑な処理、複雑な条件 というのは基本的にコードの新規作成時や変更時に、ケアレスミスや処理の理解ミスでバグがでやすい箇所になります。他の開発者が読むにも特に苦労をする箇所です。
条件を関数化し、複雑なロジックを整理することで、読みやすくなり変更時のミスも防げる可能性が上がります。

ただ、「複雑な条件」はともかく、「複雑なコード」、「複雑な処理」とは何でしょう?
こちらは仕様や設計的にややこしいところ、また、ループ文が続いたり、入れ子になっている箇所のことです。基本的に自分で実装している時や、実装した後で見た時に「ん?えーと、何してるんだったかな?」と思ったところは、複雑化していると認識して、作業の中でリファクタリングをするようにしています。
また慣れてきたら、初めから複雑なロジックをカプセル化した状態でコードの設計し実装すると、作業中に思考がこんがらがることもなく、悩む時間が省かれ、実装する速度も心理的負荷も減ります。

✅ 改善例

searchUsers.js

function searchUseCase(isAdult, searchHobbies) {
    // ログインしているかを確認
    if (!checkIfLoggedIn()) {
        return "ログインしていません";
    }

    const blockUser = getBlockUsers(); // ブロックしているユーザーを取得
    const users = getUsers(); // ユーザー一覧を取得

+   // ※ 今回は簡易的に関数内に実装しました。アプリ全体として汎用的に使う条件は、関数外に定義することが望ましいです

+  // 自分自身でないかを判定
+  const isNotOwn = (targetUserId, ownUserId) => targetUserId !== ownUserId;

+   // 自分が相手ユーザーをブロックしていないかを判定
+   const isHaveNotBlocked = (blockUser, targetUserId) =>
+       !blockUser.some((block) => block.blockTo === targetUserId);

+   // 検索対象が成人だけの場合の判定(成人以外も検索する場合は、一律trueを返す)
+   const isSearchTargetAge = (age) => (isAdult ? age >= 20 : true);

+   // 検索対象の趣味が含まれているかを判定
+   const hasSearchHobbies = (targetUserHobbies) =>
+       targetUserHobbies?.length > 0
+           ? searchHobbies.some((hobby) => targetUserHobbies.includes(hobby))
+           : true;

    // 検索条件に合致するユーザーだけを取得
    // ※ 本来なら、検索条件で絞り込むためこここで条件を作るのは現実的ではないが...例として無理やりなコードで書いてます
    const resultUsers = [];
    for (const user of users) {
        if (
-           user.id !== loggedInUser.id && // 自分自身は表示しない
-           !blockUser.some((block) => block.blockTo === user.id) && // ブロックしているユーザーは表示しない
-           ((isAdult && user.age >= 20) || !isAdult) && // 20歳以上のユーザーのみ表示
-           ((searchHobbies?.length > 0 && // 趣味が指定されている場合
-               searchHobbies.some((hobby) => user.hobbies.includes(hobby))) ||
-               !searchHobbies) && // 指定した趣味のユーザーのみ表示
+           isNotOwn(user.id, loggedInUser.id) &&
+           isHaveNotBlocked(blockUser, user.id) &&
+           isSearchTargetAge(user.age) &&
+           hasSearchHobbies(user.hobbies) &&
            !user.isSecret // 検索対象が鍵アカウント設定でないかを判定
        ) {
            resultUsers.push(user);
        }
    }

    const message =
        resultUsers.length > 0
            ? `条件に合致するユーザーが ${resultUsers.length}件 見つかりました`
            : "条件に合致するユーザーはいません";

    return { resultUsers, message };
}

[7] 汎用的な部分と固有の部分を分ける

ここは何かに書いてあったのではなく、私のノウハウになってしまうのですが。
実装するコードの中で汎用的な処理と固有の処理と性質を分けて考えて実装しています。汎用的な処理は長い目で見て変更が入りにくいです。逆に固有の処理が何か仕様が変わったり変更があるときに改修が入りやすい箇所です。
処理をこの二つにわけ、固有の処理だけ変更を入れ、それ以外は触らなくてもそれまで通りに動くように作っておくと改修の範囲が限定され、テストの数も減り、改修によりバグが発生する可能性を抑えられます。

ただ、ここもあまりに細かく分けすぎると、かえって読みにくくなってしまうことがあるので、バランスが大切です。

  • 例(SNSアプリ):アクティビティポイントの計算

関数内で分ける例

☑️ 元のコード

activityPoint.js
function calcActivityPoints(userId, action) {
    let points = 0;

    switch (action) {
        case userActions.POST:
            points = 10;
            break;

        case userActions.COMMENT:
            points = 5;
            break;

        case userActions.SHARE:
            points = 8;
            break;

        case userActions.LIKE:
            points = 1;
            break;

        default:
            return 0;
    }

    let bonusPoints = 0;

    // ユーザーレベルによるボーナス計算
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // 日付に基づくボーナス
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // 週末ボーナス
    }

    return Math.floor(points + bonusPoints);
}

✅ 改善例

activityPoint.js
function calcActivityPoints(userId, action) {
-   let points = 0;
-
-   switch (action) {
-       case userActions.POST:
-           points = 10;
-           break;
-
-       case userActions.COMMENT:
-           points = 5;
-           break;
-
-       case userActions.SHARE:
-           points = 8;
-           break;
-
-       case userActions.LIKE:
-           points = 1;
-           break;
-
-       default:
-           return 0;
-   }
+   const pointsMapping = {
+       [userActions.POST]: 10,
+       [userActions.COMMENT]: 5,
+       [userActions.SHARE]: 8,
+       [userActions.LIKE]: 1,
+       // ※ アクションが増えたらここを追加するだけで拡張できる
+   };
+
+   // アクションに応じたポイントを取得
+   const points = pointsMapping[action];

    let bonusPoints = 0;

    // ユーザーレベルによるボーナス計算
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // 日付に基づくボーナス
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // 週末ボーナス
    }

    return Math.floor(points + bonusPoints);
}
全文のコード
activityPoint.js
const userActions = Object.freeze({
    POST: "post",
    COMMENT: "comment",
    SHARE: "share",
    LIKE: "like",
});

const userLevels = Object.freeze({
    BASIC: "basic",
    PREMIUM: "premium",
    VIP: "vip",
});

// データベースからユーザーレベルを取得する関数
function getUserLevel(userId) {
    // データベースからユーザーレベルを取得するロジック
    return userLevels.PREMIUM; // 仮のデータ
}

// アクティビティポイント計算
function calcActivityPoints(userId, action) {
    const pointsMapping = {
        [userActions.POST]: 10,
        [userActions.COMMENT]: 5,
        [userActions.SHARE]: 8,
        [userActions.LIKE]: 1,
    };

    // アクションに応じたポイントを取得
    const points = pointsMapping[action];

    let bonusPoints = 0;

    // ユーザーレベルによるボーナス計算
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // 日付に基づくボーナス
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // 週末ボーナス
    }

    return Math.floor(points + bonusPoints);
}

// 使用例
const userId = "user123";
const action = userActions.POST;
const data = { hasImage: true, hasVideo: false, text: "こんにちは!" };

const points = calcActivityPoints(userId, action, data);
console.log(`ユーザー ${userId} のアクティビティポイント: ${points}`);

関数で分ける例

☑️ 元のコード

activityPoint_func.js
function calcActivityPoints(userId, action, data) {
    let points = 0;

    switch (action) {
        case userActions.POST:
            points = 10;
            if (data.hasImage) points += 5;
            if (data.hasVideo) points += 10;
            if (data.text.length > 100) points += 3;
            break;

        case userActions.COMMENT:
            points = 5;
            if (data.text.length > 50) points += 2;
            // 特定のキャンペーン固有のロジック
            if (data.campaignId === "summer2024") points *= 2;
            break;

        case userActions.SHARE:
            points = 8;
            // プラットフォーム固有のロジック
            if (data.platform === "twitter") points += 3;
            if (data.platform === "facebook") points += 2;
            break;

        case userActions.LIKE:
            points = 1;
            // 特定のコンテンツタイプ固有のロジック
            if (data.contentType === "premium") points = 2;
            break;

        default:
            return 0;
    }

    let bonusPoints = 0;

    // ユーザーレベルによるボーナス計算
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // 日付に基づくボーナス
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // 週末ボーナス
    }

    return Math.floor(points + bonusPoints);
}

✅ 改善例

activityPoint_func.js
+ function calcPostPoints(data) {
+   return (data) => {
+       let points = 10;
+       if (data.hasImage) points += 5;
+       if (data.hasVideo) points += 10;
+       if (data.text.length > 100) points += 3;
+       return points;
+   };
+ }
+
+ function calcCommentPoints(data) {
+   return (data) => {
+       let points = 5;
+       if (data.text.length > 50) points += 2;
+       if (data.campaignId === "summer2024") points *= 2;
+       return points;
+   };
+ }
+
+ function calcSharePoints(data) {
+   return (data) => {
+       let points = 8;
+       if (data.platform === "twitter") points += 3;
+       if (data.platform === "facebook") points += 2;
+       return points;
+   };
+ }
+
+ function calcLikePoints(data) {
+   return (data) => {
+       let points = 1;
+       if (data.contentType === "premium") points = 2;
+       return points;
+   };
+ }
+
// アクティビティポイント計算
function calcActivityPoints(userId, action, data) {
-  let points = 0;
-
-   switch (action) {
-       case userActions.POST:
-           points = 10;
-           if (data.hasImage) points += 5;
-           if (data.hasVideo) points += 10;
-           if (data.text.length > 100) points += 3;
-           break;
-
-       case userActions.COMMENT:
-           points = 5;
-           if (data.text.length > 50) points += 2;
-           // 特定のキャンペーン固有のロジック
-           if (data.campaignId === "summer2024") points *= 2;
-           break;
-
-       case userActions.SHARE:
-           points = 8;
-           // プラットフォーム固有のロジック
-           if (data.platform === "twitter") points += 3;
-           if (data.platform === "facebook") points += 2;
-           break;
-
-       case userActions.LIKE:
-           points = 1;
-           // 特定のコンテンツタイプ固有のロジック
-           if (data.contentType === "premium") points = 2;
-           break;
-
-       default:
-           return 0;
-   }
+   // アクションに応じたポイント計算処理を定義
+   const actionFnc = {
+       [userActions.POST]: calcPostPoints(data),
+       [userActions.COMMENT]: calcCommentPoints(data),
+       [userActions.SHARE]: calcSharePoints(data),
+       [userActions.LIKE]: calcLikePoints(data),
+       // ※ アクションが増えたら新しい計算関数を作ってここを追加するだけで拡張できる
+       // ※ 既存の関数を編集する箇所を最低限にすることでテストのコストやバグの発生率を下げる
+   };
+
+   // アクションに応じたポイント計算処理を実行
+   const points = actionFnc[action](data);

    let bonusPoints = 0;

    // ユーザーレベルによるボーナス計算
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // 日付に基づくボーナス
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // 週末ボーナス
    }

    return Math.floor(points + bonusPoints);
}
全文のコード
activityPoint_func.js
const userActions = Object.freeze({
    POST: "post",
    COMMENT: "comment",
    SHARE: "share",
    LIKE: "like",
});

const userLevels = Object.freeze({
    BASIC: "basic",
    PREMIUM: "premium",
    VIP: "vip",
});

// データベースからユーザーレベルを取得する関数
function getUserLevel(userId) {
    // データベースからユーザーレベルを取得するロジック
    return userLevels.PREMIUM; // 仮のデータ
}

function calcPostPoints(data) {
    return (data) => {
        let points = 10;
        if (data.hasImage) points += 5;
        if (data.hasVideo) points += 10;
        if (data.text.length > 100) points += 3;
        return points;
    };
}

function calcCommentPoints(data) {
    return (data) => {
        let points = 5;
        if (data.text.length > 50) points += 2;
        if (data.campaignId === "summer2024") points *= 2;
        return points;
    };
}

function calcSharePoints(data) {
    return (data) => {
        let points = 8;
        if (data.platform === "twitter") points += 3;
        if (data.platform === "facebook") points += 2;
        return points;
    };
}

function calcLikePoints(data) {
    return (data) => {
        let points = 1;
        if (data.contentType === "premium") points = 2;
        return points;
    };
}

// アクティビティポイント計算
function calcActivityPoints(userId, action, data) {
    // アクションに応じたポイント計算処理を定義
    const actionFnc = {
        [userActions.POST]: calcPostPoints(data),
        [userActions.COMMENT]: calcCommentPoints(data),
        [userActions.SHARE]: calcSharePoints(data),
        [userActions.LIKE]: calcLikePoints(data),
    };

    // アクションに応じたポイント計算処理を実行
    const points = actionFnc[action](data);

    let bonusPoints = 0;

    // ユーザーレベルによるボーナス計算
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // 日付に基づくボーナス
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // 週末ボーナス
    }

    return Math.floor(points + bonusPoints);
}

// 使用例
const userId = "user123";
const action = userActions.POST;
const data = { hasImage: true, hasVideo: false, text: "こんにちは!" };

const points = calcActivityPoints(userId, action, data);
console.log(`ユーザー ${userId} のアクティビティポイント: ${points}`);

まとめ

以上になります。
出てきたサンプルのコードが、言語(JavaScript)を知らなくても、何となく何をしているのか読めるコードになっていましたら幸いです。
(サンプルの処理例はAIに生成してもらったのを元にしているので、実際に業務で使われるコードとは違うかもしれません)
今後もいろいろ経験したり、本を読み、より良いコードを目指し精進します٩( ᐛ )و


ここまで、読んでいただき誠にありがとうございます。
TechCommit AdventCalendar2024 の25日の記事は、締めのinoueさん(主催者)です。お楽しみに!

https://blog.tech-commit.jp/1857/

https://adventar.org/calendars/10584

参考

https://zenn.dev/tetsuyaohira/articles/53bcc378581d2f

https://stackoverflow.com/questions/55601062/using-an-async-function-in-array-find

素材は下記を使用させていただきました。

https://www.irasutoya.com/

https://fukidesign.com/

Discussion