iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🪴

In Pursuit of Clean Code: My Current Mental Model for Writing Code

に公開

https://adventar.org/calendars/10584

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

Introduction

This article is an output where I verbalize my current mental model of why naming variables and functions is important and what "clean code" feels like.

Also, since I'm having AI proofread this as I write, I thought it would be a good opportunity to reflect on what "good code" really is by comparing my own knowledge with the AI's suggestions ꉂꉂ( ᐛ )

Since this includes some personal know-how from my own experience, many people might have different opinions. I would appreciate it if you could point things out or write your own articles to spark active discussion ٩( ᐛ )و

Self-introduction

I am currently seeking a position as a Web Application Engineer (Frontend).

  • Career history: SE (Java for nearly 7 years), Web (PHP for 1.5 years). About 8 years of engineering experience in total.
  • Struggles: I am slow at absorbing books and knowledge, and I feel immature for the number of years I've worked.
  • Current situation: I have read books like "The Readable Code" and "Clean Code", and I occasionally watch videos by overseas creators about clean code on YouTube. Although I have read several books on "Object-Oriented Programming", "Design Patterns", and "Domain-Driven Design", I haven't quite grasped their true essence or core yet, and I'm continuing to study through books.

As long as I continue to be an engineer in the future, I will keep pursuing cleaner code in my work and hobbies. So, what I’m sharing today isn't my final answer; I expect it to evolve or change as I realize I was wrong. I hope you can read this thinking, "There are people who think like this," or "This is one way of looking at it" ٩( ᐛ )و

Sample code is disorganized, but you can also find it here:
https://github.com/mae616/cleanCode/tree/main/2024

Ultimately, I think it boils down to this one thing

★ Make code intuitively understandable and changeable based on English

  • Example (EC site): Adding items to a cart
cart.js
const cart = {
    pendingItems: [{ id: "SYOUHIN001", count: 1, price: 500 }],
    totalCount: 1,
    totalPrice: 500,
};

// Calculate the total number of items
const calcTotalCount = () => {
    cart.totalCount = cart.pendingItems.reduce(
        (prev, current) => prev + current.count,
        0
    );
};

// Calculate the total price
const calcTotalPrice = () => {
    cart.totalPrice = cart.pendingItems.reduce(
        (prev, current) => prev + current.price * current.count,
        0
    );
};

// Function to add an item to the cart
function addItemToCart(itemId, itemCount, itemPrice) {
    const existingItem = cart.pendingItems.find((item) => item.id === itemId);

    if (existingItem) {
        // Increase quantity if the item already exists
        existingItem.count += itemCount;
    } else {
        // Add a new item
        cart.pendingItems.push({
            id: itemId,
            count: itemCount,
            price: itemPrice,
        });
    }

    // Recalculate totals
    calcTotalCount();
    calcTotalPrice();
}

I tried to write code where the variable and function names are clear, making it obvious what the process is doing.

In the past, when I looked at OSS code or Alfred Workflows created by people overseas, there were many languages I didn't know, like Ruby. When I wanted to customize something in a language I didn't know, the variable and function names were often descriptive enough that I could understand where to make changes just by reading the code.
For me, this kind of "code where you can understand what's happening by using English knowledge or translation even if you don't know the programming language" has become one of the models (or goals) for "clean code."

I've read in books like "Clean Code" that "it's better to use this English word instead of that one," but I understand this ultimately means "use English words that accurately describe the meaning of the process."

Also, if ubiquitous language is available, I believe it's best to name variables and functions consistently using that language from design to implementation. I think it's best to use terms that are commonly understood among designers, the team, and everyone involved. (Naming length is also important, but I'll skip that in this article; my opinion is simply that "not too long" is best.)

By the way, although I write comments in Japanese, I believe Japanese comments are essential because Japanese people are not native English speakers, no matter how much I aim for "code that can be intuitively understood and changed based on English." I use comments to express what a part of the code is doing from a domain perspective, rather than just explaining what it's doing programmatically.

The specific details of the mental model during creation

  • Example (Matching app): User preference data
javascript
// User's ideal type
const userIdealType = {
    ageRange: { min: 25, max: 35 },
    hobbies: ["hiking", "reading"],
    locations: ["Tokyo"],
};

Variables sometimes represent information, state, or reusable settings.
When a variable represents information, I try to write code that allows others to appropriately imagine "what information does it represent?" through its naming.

userIdealType represents the "user's ideal type information."
If you encountered a feature using this userIdealType, what would you think?
You might imagine, "A feature that uses 'user's ideal type information'... probably used for 'matching' or 'searching'." Sometimes, you might even wonder, "Does this feature really need to use 'user's ideal type information' in the first place?"
By making variable names easy to understand, the data content becomes clear, the intent of the code becomes explicit, and the burden of rereading the code later is reduced.

  • Example (Task management app): Displaying tasks, adding tasks, etc.
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 },
];

// Get tasks that are not completed
function getTasks() {
    const notCompleteTasks = allTasks.filter(
        (task) => task.status !== taskStatus.COMPLETED
    );
    return notCompleteTasks;
}

// Get all tasks
function getAllTasks() {
    return allTasks;
}

// Add a task
function addTask(newTaskTitle) {
    const newTask = {
        id: uuid(),
        title: newTaskTitle,
        status: taskStatus.PENDING,
    };
    allTasks.push(newTask);
    return newTask;
}

// Update task status
function updateTaskStatus(targetTaskId, newStatus) {
    const targetTask = allTasks.find((task) => task.id === targetTaskId);
    if (!targetTask) {
        return null;
    }
    targetTask.status = newStatus;
    return targetTask;
}

// Usage example
console.log(getTasks());

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

Ideally, function names should reflect the actions or features that the function performs. When the role of a function is clearly communicated through its name, readers can immediately understand its purpose. However, if naming is too specific, it may hinder the ability to respond flexibly to changes in specifications.

Let me tell a short story.
In a company, there are people in charge of "Accounting," "Administration," and "General Affairs." A company functions because people in various roles work together.

However, do people in other departments know exactly what "Accounting," "Administration," or "General Affairs" are doing specifically? Is it something they need to know entirely?
The company runs as long as we know to give "Accounting" our invoices for travel expenses, contact "Administration" with our attendance records, and inform "General Affairs" about used supplies. It's enough to know "which role" to give "which information" to get "what result." There's no need to know everything; it's fine as long as the person in charge knows the procedures and details.

I believe that in code, too, details should only be read when necessary. That's why I aim for code where the overall picture can be understood roughly just by the names. I try to ensure that people can understand the intent and behavior not just through function names alone, but through the combination of function and variable names.

Finding the right balance of abstraction in function names is difficult; if they are too abstract, the meaning becomes hard to convey. I think it's best to use names that clearly indicate the domain responsibility of the function, but if it's hard to communicate, I find it useful to add supplements using comments as needed.

Ideally, function and variable names should be based on a common understanding so that everyone—within the team, on the business side, and all stakeholders—can imagine what is being done just by looking at the names.

(3) Mapping to real-world concepts through Object-Oriented Programming (Modeling)

  • Example (Matching app): Classes for users and match criteria
matching.js
// Ideal type (matching criteria)
class IdealType {
    #minAge;
    #maxAge;
    #hobbies;
    #locations;
    constructor(minAge, maxAge, hobbies, locations) {
        this.#minAge = minAge;
        this.#maxAge = maxAge;
        this.#hobbies = hobbies;
        this.#locations = locations;
    }

    // Determine if it's a match
    match(candidate) {
        return (
            candidate.age >= this.#minAge &&
            candidate.age <= this.#maxAge &&
            this.#locations.includes(candidate.location) &&
            this.#hobbies.some((hobby) => candidate.hobbies.includes(hobby))
        );
    }
}

// LovedOne (User; used here in the sense of "someone who tries to love someone")
class LovedOne {
    constructor(name, age, hobbies, location) {
        this.name = name;
        this.age = age;
        this.hobbies = hobbies;
        this.location = location;
        this.idealType = null; // Ideal type is not initially set
    }

    // Imagined an ideal type! (Setting the ideal type)
    imagineIdealType(idealType) {
        this.idealType = idealType;
    }
}

// Cupid performs matching (matching feature)
class CupidMatching {
    #criteria;
    constructor(lovedOne) {
        if (!lovedOne || !lovedOne.idealType) {
            throw new Error("Ideal type is not set");
        }
        this.#criteria = lovedOne.idealType; // Based on the loved one's ideal type
    }

    // Matching judgment
    match(candidate) {
        if (!candidate) {
            throw new Error("User information is invalid");
        }

        if (this.#criteria.match(candidate)) {
            return `Match found with ${candidate.name}!`;
        } else {
            return `Match not found with ${candidate.name}.`;
        }
    }
}

// Usage example
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 asked the Cupid for matching!

const charlie = new LovedOne("Charlie", 32, ["dance", "art"], "Kyoto");
console.log(cupid.match(charlie)); // Match not found with Charlie

const bob = new LovedOne("Bob", 29, ["dance", "travel", "cat"], "Tokyo");
console.log(cupid.match(bob)); // Match found with Bob!

Object-Oriented Programming allows us to represent people, things, and events in the real world as classes or objects in code.
The example above is quite a playful one, but it shows how OOP can represent people, things, and events in this way. It's like a naming convention that metaphors the real world with fantasy.

To use a slightly romantic expression, "naming is expressing." (I think this was also mentioned in "Refactoradio").

LovedOne could also be represented as User.
It seems to be about what words to use to best describe intentions and roles so that anyone can easily understand, read, and change the code. However, this is not something to be done individually; it requires creating a common understanding among stakeholders.

Things I'm careful about

[1] Limit a variable's responsibility to one

  • Example (SNS app): Loading own posts and checking the number of likes

❌ Anti-pattern

fetchMyPosts.js
function fetchMyPostsUseCase() {
    // Check if logged in
    let flag = checkIfLoggedIn(); // Check if logged in
    if (!flag) {
        return "Not logged in";
    }

    flag = false; // Reusing the flag to determine if there are liked posts !!The meaning of the flag has changed!!
    const myPosts = getMyPosts();

    // Check if there are liked posts
    let totalLikeCount = 0;
    for (const post of myPosts) {
        const likeCount = post.likeCount;
        if (likeCount > 0) {
            flag = true;
            totalLikeCount += likeCount;
        }
    }

    // Creating a message
    let message = "";
    if (flag) {
        message = `There are ${totalLikeCount} liked posts`;
    }

    return { myPosts, message };
}
Full code
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: "Hello. Today is a wonderful day 🎄",
    },
    {
        id: "POST20241224002",
        date: "2024-12-24",
        author: "Alice",
        body: "Did Santa come to your place too? 🎅",
    },
];

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

let loggedInUser = null;

// Encrypt password
async function encryptPassword(password) {
    return await bcrypt.hash(password, 10);
}

async function login(userName, password) {
    // Asynchronous version of 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() {
    // Check if logged in
    let flag = checkIfLoggedIn(); // Check if logged in
    if (!flag) {
        return "Not logged in";
    }

    flag = false; // Reusing the flag to determine if there are liked posts !!The meaning of the flag has changed!!
    const myPosts = getMyPosts();

    // Check if there are liked posts
    let totalLikeCount = 0;
    for (const post of myPosts) {
        const likeCount = post.likeCount;
        if (likeCount > 0) {
            flag = true;
            totalLikeCount += likeCount;
        }
    }

    // Creating a message
    let message = "";
    if (flag) {
        message = `There are ${totalLikeCount} liked posts`;
    }

    return { myPosts, message };
}

// Usage example
console.log("Login screen");
const loggedInResult = await login("Eric", "password1"); // Login
console.log(`Login result: ${loggedInResult}`);

console.log("\nChecking the user's MyPosts on another screen");

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

The above is a somewhat forced example, but I make an effort to limit a variable's responsibility to one.
Reusing variables after their initial purpose or using them with ambiguous intent makes the code's objective unclear, which often leads to bugs when you or someone else modifies it months later.
By maintaining a single responsibility, it becomes easier to apply appropriate naming, the intent of the code becomes explicit, and even during extension, the purpose is conveyed, helping to avoid new defects or unnecessary refactoring.

✅ Improvement example

fetchPosts.js
function fetchPostsUseCase() {
    // Check if logged in
-   let flag = checkIfLoggedIn(); // Check if logged in
-   if (!flag) {
+   let isLoggedIn = checkIfLoggedIn(); // Check if logged in
+   if (!isLoggedIn) {
        return "Not logged in";
    }

-   flag = false; // Reusing the flag to determine if there are liked posts !!The meaning of the flag has changed!!
    const myPosts = getMyPosts();
+   let hasLikedPosts = false; // Flag for whether there are liked posts

    // Check if there are liked posts
    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 = `There are ${totalLikeCount} liked posts`;
    }

    return { myPosts, message };
}

[2] Limit a function's responsibility to one

  • Example (SNS app): Adult check, discount application check

❌ Anti-pattern

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

// Check if adult
function checkAdult(user) {
    return !isUnder20(user.age);
}

// Discount processing
function getDiscountedPrice(totalPrice) {
    if (!isUnder20(totalPrice)) {
        // Discount processing
        return totalPrice * 0.8;
    }
    return totalPrice;
}
Full code
payment.js
function isUnder20(num) {
    return num < 20;
}

// Check if adult
function checkAdult(user) {
    return !isUnder20(user.age);
}

// Discount processing
function getDiscountedPrice(totalPrice) {
    if (!isUnder20(totalPrice)) {
        // Discount processing
        return totalPrice * 0.8;
    }
    return totalPrice;
}

// Usage example
const user = {
    age: 10,
};
const isAdult = checkAdult(user);
console.log(`User is ${isAdult ? "an adult" : "a minor"}`);

const totalPrice = 1000;
const discountedPrice = getDiscountedPrice(totalPrice);
console.log(`Regular price: ${totalPrice} yen, Discounted price: ${discountedPrice} yen`);

Functions should also have a single responsibility. This makes the intent easier to convey, for the same reason as with variables.
I've provided an example of a function with an ambiguous role as an anti-pattern. Reusability is important for functions, but sometimes being too focused on DRY (Don't Repeat Yourself) results in functions with vague responsibilities that are reused from various places for different purposes. This can become a bottleneck when specifications change. For example, if a general-purpose function is called for multiple uses, its scope of impact becomes broad when changes are needed, potentially causing unexpected bugs.
I try to use naming and content that clearly reflect the intent for general-purpose utility functions or the specific responsibility (role) for functions with a distinct purpose.

✅ Improvement example

payment.js

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

// Check if adult
function checkAdult(user) {
-    return !isUnder20(user.age);
+    return isAdult(user.age);
}

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

// Discount processing
function getDiscountedPrice(totalPrice) {
-    if (!isUnder20(totalPrice)) {
+    if (isDiscount(totalPrice)) {
        // Discount processing
        return totalPrice * 0.8;
    }
    return totalPrice;
}

[3] Limit a class or component's responsibility to one

  • Example (SNS app): Post class

☑️ Original code

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();
    }

    // Processing related to posts
    addComment(comment, userId) {
        this.comments.push({ comment, userId });
    }

    addLike() {
        this.likes++;
    }

    // User-related processing (should originally be in the User class)
    getUserName() {
        return users.find((user) => user.id === this.userId).userName;
    }

    // Notification-related processing (should originally be in the Notification class)
    sendAddPostNotification() {
        const notification = {
            userId: this.userId,
            userName: this.getUserName(),
            message: "A new post has been created",
            createdAt: new Date(),
        };
        return notification;
    }

    // Analytics-related processing (should originally be in the Analytics class)
    trackPostMetrics() {
        const analytics = {
            event: "post_created",
            userId: this.userId,
            timestamp: this.createdAt,
        };
        return analytics;
    }
}
Full code
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();
    }

    // Processing related to posts
    addComment(comment, userId) {
        this.comments.push({ comment, userId });
    }

    addLike() {
        this.likes++;
    }

    // User-related processing (should originally be in the User class)
    getUserName() {
        return users.find((user) => user.id === this.userId).userName;
    }

    // Notification-related processing (should originally be in the Notification class)
    sendAddPostNotification() {
        const notification = {
            userId: this.userId,
            userName: this.getUserName(),
            message: "A new post has been created",
            createdAt: new Date(),
        };
        return notification;
    }

    // Analytics-related processing (should originally be in the Analytics class)
    trackPostMetrics() {
        const analytics = {
            event: "post_created",
            userId: this.userId,
            timestamp: this.createdAt,
        };
        return analytics;
    }
}

// Usage example
const userPost = new UserPost("user1", "title1", "content1");
const notification = userPost.sendAddPostNotification();
console.log(notification);

Classes and components should also have a single responsibility (role). This is for the same reason.
In object-oriented programming, "keeping responsibility to one"—meaning "which behavior should be assigned to which class"—is a very difficult part. If the scope of concepts or roles that a class should represent is not clarified, unnecessary dependencies (complex relationships) are born, leading to unexpected impacts during changes.
Basically, if a concept is single, I implement behavior in that class, and if multiple concepts appear, I implement them in independent classes. I want to continue gaining experience and improve in this area in the future.

✅ Improvement example

userPost.js
- class UserPost {
-   constructor(userId, title, content) {
-       this.userId = userId;
+ // Processing related to posts
+ 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();
    }


-   // Processing related to posts
-   addComment(comment, userId) {
-       this.comments.push({ comment, userId });
-   }
+   addComment(comment, commentedUser) {
+       this.comments.push({
+           comment,
+           userId: commentedUser.id,
+           userName: commentedUser.userName,
+       });
+   }

    addLike() {
        this.likes++;
    }
}
Full code
userPost.js
// Repository class responsible for user data persistence
class UserRepository {
    constructor() {
        // Set initial data
        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;
    }
}

// User-related processing
class User {
    constructor(userRepository, userId) {
        this.userRepository = userRepository;
        this.id = userId;
        this.userName = userRepository.getUserName(userId);
    }
}

// Processing related to posts
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++;
    }
}

// Notification-related processing
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: "A new post has been created",
            createdAt: this.post.createdAt,
        };
        return notification;
    }
}

// Analytics-related processing
class AnalyticsService {
    trackPostMetrics(post) {
        const analytics = {
            event: "post_created",
            userId: post.userId,
            timestamp: post.createdAt,
        };
        return analytics;
    }
}
// Usage example
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] Separation of Concerns and Encapsulation

  • Example (SNS app): Checking credit card numbers for membership fee payment

☑️ Original code

checkCreditCard.js
// Checking credit card numbers using the Luhn algorithm
function checkCreditCardNumber(cardNumber) {
    let isEven = cardNumber.length % 2 === 0; // Even

    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;
}
Full code
checkCreditCard.js
// Checking credit card numbers using the Luhn algorithm
function checkCreditCardNumber(cardNumber) {
    let isEven = cardNumber.length % 2 === 0; // Even

    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;
}

// Usage example
console.log(checkCreditCardNumber("79927398713")); // true
console.log(checkCreditCardNumber("79927398715")); // false

I try to functionalize (isolate as a single function) processes where responsibilities (roles, obligations) can be separated, keeping them independent. Also, by giving such functions appropriate names, what the process does becomes clear at a glance, making the source code easier to read and maintain. In short, through functionalization and proper naming, the responsibility of the process becomes clear, and you can communicate to other developers that they don't need to touch parts where changes aren't necessary.
I believe that the essence of "clean code" is to convey the developer's intent through the source code alone, without having to speak directly or create supplementary documentation.

Since over-fragmenting functions can sometimes make the code harder to read, I think finding the right balance is important and difficult. I try to strike that balance by "shortening the lifespan (shortening the scope of functions)," which I'll mention in the next paragraph, to limit the range required to understand the code, making it readable and easy to grasp for many people.

✅ Improvement example

checkCreditCard.js
// Checking credit card numbers using the Luhn algorithm
function checkCreditCardNumber(cardNumber) {
+   // Calculation for even-numbered digits
+   const even = (digit) => {
+       let result = digit * 2;
+       if (result > 9) {
+           const [ten, one] = String(result).split("");
+           result = Number(ten) + Number(one);
+       }
+       return result;
+   };

    // Main process
    let isEven = cardNumber.length % 2 === 0; // Even
    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] Shortening the lifespan of variables and functions

  • Example (SNS app): Process of following a user

☑️ Original code

Follow.js
// Process of following a user
function followUserUseCase(userId, targetUserId) {
    let isLoggedIn;
    let isFollowed;
    let isBlocked;
    let isFollowLimit;
    let loggedInUser;
    let targetUser;
    const FOLLOW_LIMIT = 5000;

    // Checking if already logged in
    isLoggedIn = checkIfLoggedIn();
    if (!isLoggedIn) {
        return "Not logged in";
    }

    // Checking if already following
    isFollowed = getFollowUsers().some(
        (followUser) =>
            (followUser.from === userId && followUser.to) === targetUserId
    );
    if (isFollowed) {
        return { error: "Already following" };
    }

    // Checking if not blocked
    isBlocked = getBlockUsers().some(
        (blockUser) =>
            blockUser.from === targetUserId && blockUser.to === userId
    );
    if (isBlocked) {
        return { error: "Cannot follow" };
    }

    // Limit check for the number of follows
    if (!canFollowMore(userId, FOLLOW_LIMIT)) {
        return { error: "Follow limit reached" };
    }

    // Executing the follow process
    follows.push({
        id: uuid(),
        from: userId,
        to: targetUserId,
    });
    return { success: "Followed" };
}
Full code
Follow.js
import bcrypt from "bcrypt";

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

const users = [
    {
        id: "USER00000001",
        userName: "Eric",
        password: await encryptPassword("password1"),
        age: 30,
        biography: "I am a cat 🐈",
        hobbies: ["Baseball", "Reading"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000002",
        userName: "Alice",
        password: await encryptPassword("password2"),
        age: 25,
        biography: "I am a dog 🐕",
        hobbies: ["Watching movies", "Travel"],
        isSecret: false,
        isDelete: true,
    },
    {
        id: "USER00000003",
        userName: "Bob",
        password: await encryptPassword("password3"),
        age: 20,
        biography: "I am a fish 🐟",
        hobbies: ["Cooking", "Gaming"],
        isSecret: false,
        isDelete: false,
    },
];

const blocks = [
    {
        id: uuid(),
        from: "USER00000001", // Eric blocks Bob
        to: "USER00000003",
    },
];

const follows = [
    {
        id: uuid(),
        from: "USER00000003", // Bob follows Alice
        to: "USER00000002",
    },
];

let loggedInUser = null;

// Encrypt password
async function encryptPassword(password) {
    return await bcrypt.hash(password, 10);
}

async function login(userName, password) {
    // Asynchronous version of 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);
}

// Limit check for the number of follows
function canFollowMore(FOLLOW_LIMIT) {
    // Mock
    return true;
}

// Process of following a user
function followUserUseCase(userId, targetUserId) {
    let isLoggedIn;
    let isFollowed;
    let isBlocked;
    let isFollowLimit;
    let loggedInUser;
    let targetUser;
    const FOLLOW_LIMIT = 5000;

    // Checking if already logged in
    isLoggedIn = checkIfLoggedIn();
    if (!isLoggedIn) {
        return "Not logged in";
    }

    // Checking if already following
    isFollowed = getFollowUsers().some(
        (followUser) =>
            (followUser.from === userId && followUser.to) === targetUserId
    );
    if (isFollowed) {
        return { error: "Already following" };
    }

    // Checking if not blocked
    isBlocked = getBlockUsers().some(
        (blockUser) =>
            blockUser.from === targetUserId && blockUser.to === userId
    );
    if (isBlocked) {
        return { error: "Cannot follow" };
    }

    // Limit check for the number of follows
    if (!canFollowMore(userId, FOLLOW_LIMIT)) {
        return { error: "Follow limit reached" };
    }

    // Executing the follow process
    follows.push({
        id: uuid(),
        from: userId,
        to: targetUserId,
    });
    return { success: "Followed" };
}

// Usage example
console.log("Login screen");
const loggedInResult = await login("Eric", "password1");
console.log(`Login result: ${loggedInResult}`);

if (loggedInResult) {
    console.log("\nPerforming follow process on another screen");
    const followResult = followUserUseCase(loggedInUser.id, "USER00000002");
    console.log(followResult);
}

console.log(follows);

I believe that reading program code is, in a sense, like reading blocks. I always read with the sense of scanning the overall flow of a function by looking at well-paragraphed code (blocks separated by empty lines) and then diving into the details of each paragraph.

However, rather than just separating by empty lines, if you limit the scope of variables and functions through rules like functionalization—ensuring they cannot be used outside their intended purpose—you can avoid unintended reuse by other developers, as mentioned in "[1] Limit a variable's responsibility to one."

While you could trust other developers and create a common understanding within the team to consciously avoid unintended use, modern programming provides ways to formalize rules and restrictions in the code itself. I believe that establishing rules within the code to limit usage is a better way to reduce the psychological burden on people in the first place.

Furthermore, by shortening the lifespan of variables and functions, the range that other developers need to care about for each variable or function is limited, which has the benefit of making the code easier to understand.

✅ Improvement example

Follow.js
+ // Follow process
+ function addFollow(userId, targetUserId) {
+   follows.push({
+       id: uuid(),
+       from: userId,
+       to: targetUserId,
+   });
+ }
+ 
+ // Checking if followed
+ function isFollowedByUser(userId, targetUserId) {
+   return follows.some(
+       (followUser) =>
+           (followUser.from === userId && followUser.to) === targetUserId
+   );
+ }
+
+ // Checking if blocked
+ function isBlockedByUser(userId, targetUserId) {
+  return blocks.some(
+       (blockUser) =>
+           (blockUser.from === targetUserId && blockUser.to) === userId
+   );
+ }
+
// Process of following a user
function followUserUseCase(userId, targetUserId) {
-   let isLoggedIn;
-   let isFollowed;
-   let isBlocked;
-   let isFollowLimit;
-   let loggedInUser;
-   let targetUser;
    const FOLLOW_LIMIT = 5000;

-   // Checking if already logged in
-   isLoggedIn = checkIfLoggedIn();
-   if (!isLoggedIn) {
-       return "Not logged in";
-   }
-
-   // Checking if already following
-   isFollowed = getFollowUsers().some(
-       (followUser) =>
-           (followUser.from === userId && followUser.to) === targetUserId
-   );
-   if (isFollowed) {
-       return { error: "Already following" };
-   }
-
-   // Checking if not blocked
-   isBlocked = getBlockUsers().some(
-       (blockUser) =>
-           blockUser.from === targetUserId && blockUser.to === userId
-   );
-   if (isBlocked) {
-       return { error: "Cannot follow" };
-   }
-
-   // Limit check for the number of follows
-   if (!canFollowMore(userId, FOLLOW_LIMIT)) {
-       return { error: "Follow limit reached" };
-   }
+   // Check process before following
+   const checkBeforeFollow = (userId, targetUserId, FOLLOW_LIMIT) => {
+       // Checking if already logged in
+       if (!checkIfLoggedIn()) {
+           return { error: "Not logged in" };
+       }
+
+      // Checking if already following
+       const isFollowed = isFollowedByUser(userId, targetUserId);
+       if (isFollowed) {
+           return { error: "Already following" };
+       }
+
+       // Checking if not blocked
+       const isBlocked = isBlockedByUser(targetUserId, userId);
+       if (isBlocked) {
+           return { error: "Cannot follow" };
+       }
+
+       // Limit check for the number of follows
+       if (!canFollowMore(userId, FOLLOW_LIMIT)) {
+           return { error: "Follow limit reached" };
+       }
+        return { success: true };
+   };
+
+   // Check process before following
+   const checkResult = checkBeforeFollow(userId, targetUserId, FOLLOW_LIMIT);
+   if (checkResult.error) {
+       return checkResult;
+   }

    // Executing the follow process
-   follows.push({
-       id: uuid(),
-       from: userId,
-       to: targetUserId,
-   });
+   addFollow(userId, targetUserId);
    return { success: "Followed" };
}

[6] Avoiding code complexity

  • Example (SNS app): Searching for users

☑‸ Original code

searchUsers.js
function searchUseCase(isAdult, searchHobbies) {
    // Check if logged in
    if (!checkIfLoggedIn()) {
        return "Not logged in";
    }

    const blockUser = getBlockUsers(); // Get blocked users
    const users = getUsers(); // Get user list

    // Get only users who match search criteria
    // * Normally, it is not realistic to create conditions here as they would be filtered by search criteria... written as a forced example for illustration.
    const resultUsers = [];
    for (const user of users) {
        if (
            user.id !== loggedInUser.id && // Do not display yourself
            !blockUser.some((block) =2 block.blockTo === user.id) && // Do not display blocked users
            ((isAdult && user.age >= 20) || !isAdult) && // Display only users aged 20 or older
            ((searchHobbies?.length > 0 && // If hobbies are specified
                searchHobbies.some((hobby) =2 user.hobbies.includes(hobby))) ||
                !searchHobbies) && // Display only users with specified hobbies
            !user.isSecret // Determine if the search target is not a private account
        ) {
            resultUsers.push(user);
        }
    }

    const message =
        resultUsers.length > 0
            ? `${resultUsers.length} matching users found`
            : "No users match the criteria";

    return { resultUsers, message };
}
Full code
searchUsers.js
import bcrypt from "bcrypt";

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

const users = [
    {
        id: "USER00000001",
        userName: "Eric",
        password: await encryptPassword("password1"),
        age: 30,
        biography: "I am a cat 🐈",
        hobbies: ["Baseball", "Reading"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000002",
        userName: "Alice",
        password: await encryptPassword("password2"),
        age: 25,
        biography: "I am a dog 🐕",
        hobbies: ["Watching movies", "Travel"],
        isSecret: false,
        isDelete: true,
    },
    {
        id: "USER00000003",
        userName: "Bob",
        password: await encryptPassword("password3"),
        age: 20,
        biography: "I am a fish 🐟",
        hobbies: ["Cooking", "Gaming"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000004",
        userName: "Tom",
        password: await encryptPassword("password4"),
        age: 35,
        biography: "I am a bird 🐦",
        hobbies: ["Reading", "Cooking"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000005",
        userName: "Ken",
        password: await encryptPassword("password5"),
        age: 40,
        biography: "I am a bear 🐻",
        hobbies: ["Fishing", "Watching movies"],
        isSecret: true,
        isDelete: false,
    },
    {
        id: "USER00000006",
        userName: "John",
        password: await encryptPassword("password6"),
        age: 45,
        biography: "I am a monkey 🐒",
        hobbies: ["Travel", "Reading"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000007",
        userName: "Mike",
        password: await encryptPassword("password7"),
        age: 15,
        biography: "I am a bug 🐞",
        hobbies: ["Gaming", "Baseball"],
        isSecret: false,
        isDelete: false,
    },
    {
        id: "USER00000008",
        userName: "Chris",
        password: await encryptPassword("password8"),
        age: 50,
        biography: "I am a snake 🐍",
        hobbies: ["Watching movies", "Baseball"],
        isSecret: false,
        isDelete: false,
    },
];

const blockUsers = [
    {
        id: uuid(),
        blockTo: "USER00000004",
        blockBy: "USER00000001", // Eric blocks Tom
    },
    {
        id: uuid(),
        blockTo: "USER00000003",
        blockBy: "USER00000001", // Eric blocks Bob
    },
];

let loggedInUser = null;

// Encrypt password
async function encryptPassword(password) {
    return await bcrypt.hash(password, 10);
}

async function login(userName, password) {
    // Asynchronous version of array.find()
    const asyncFind = async (array, predicate) =2 {
        for (const item of array) {
            if (await predicate(item)) {
                return item;
            }
        }
    };

    const matchUser = await asyncFind(
        users,
        async (user) =2
            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) =2 blockUser.blockBy === loggedInUser.id
    );
}

function getUsers() {
    return users
        .filter((user) =2 !user.isDelete) // Get users who are not logically deleted
        .map((user) =2 {
            return {
                id: user.id,
                userName: user.userName,
                age: user.age,
                biography: user.biography,
                hobbies: user.hobbies,
                isSecret: user.isSecret,
            };
        });
}

function searchUseCase(isAdult, searchHobbies) {
    // Check if logged in
    if (!checkIfLoggedIn()) {
        return "Not logged in";
    }

    const blockUser = getBlockUsers(); // Get blocked users
    const users = getUsers(); // Get user list

    // Get only users who match search criteria
    // * Normally, it is not realistic to create conditions here as they would be filtered by search criteria... written as a forced example for illustration.
    const resultUsers = [];
    for (const user of users) {
        if (
            user.id !== loggedInUser.id && // Do not display yourself
            !blockUser.some((block) =2 block.blockTo === user.id) && // Do not display blocked users
            ((isAdult && user.age >= 20) || !isAdult) && // Display only users aged 20 or older
            ((searchHobbies?.length > 0 && // If hobbies are specified
                searchHobbies.some((hobby) =2 user.hobbies.includes(hobby))) ||
                !searchHobbies) && // Display only users with specified hobbies
            !user.isSecret // Determine if the search target is not a private account
        ) {
            resultUsers.push(user);
        }
    }

    const message =
        resultUsers.length > 0
            ? `${resultUsers.length} matching users found`
            : "No users match the criteria";

    return { resultUsers, message };
}

// Usage example
console.log("Login screen");
const loggedInResult = await login("Eric", "password1");

console.log("\nChecking the user search list on another screen");
const searchResult = searchUseCase(true, ["Baseball", "Reading"]);
console.log(searchResult);

This also relates to [4] Separation of Concerns and Encapsulation. Complex code, processes, and conditions are generally where bugs are most likely to occur due to careless mistakes or misunderstandings when creating or modifying them. These are also parts that other developers find particularly difficult to read.
By functionalizing conditions and organizing complex logic, you can improve readability and increase the chances of preventing mistakes during modification.

But what exactly are "complex conditions," "complex processes," or "complex code"?
These refers to parts that are complicated in terms of specifications or design, as well as locations where loop statements continue or are nested. Basically, when I'm implementing something myself or looking at it later and think, "Wait, what was I doing again?", I recognize it as having become complex and perform refactoring as part of my work.
Also, once you get used to it, designing and implementing with complex logic encapsulated from the beginning prevents your thoughts from getting tangled during work, saves time spent worrying, and reduces implementation speed and psychological burden.

✅ Improvement example

searchUsers.js

function searchUseCase(isAdult, searchHobbies) {
    // Check if logged in
    if (!checkIfLoggedIn()) {
        return "Not logged in";
    }

    const blockUser = getBlockUsers(); // Get blocked users
    const users = getUsers(); // Get user list

+   // * Implemented simply within the function this time. Conditions used globally throughout the app should preferably be defined outside the function.

+  // Determine if not self
+  const isNotOwn = (targetUserId, ownUserId) =2 targetUserId !== ownUserId;

+   // Determine if the target user is not blocked by me
+   const isHaveNotBlocked = (blockUser, targetUserId) =2
+       !blockUser.some((block) =2 block.blockTo === targetUserId);

+   // Determination when searching for adults only (returns true if searching for non-adults as well)
+   const isSearchTargetAge = (age) =2 (isAdult ? age >= 20 : true);

+   // Determine if search target's hobbies are included
+   const hasSearchHobbies = (targetUserHobbies) =2
+       targetUserHobbies?.length > 0
+           ? searchHobbies.some((hobby) =2 targetUserHobbies.includes(hobby))
+           : true;

    // Get only users who match search criteria
    // * Normally, it is not realistic to create conditions here as they would be filtered by search criteria... written as a forced example for illustration.
    const resultUsers = [];
    for (const user of users) {
        if (
-           user.id !== loggedInUser.id && // Do not display yourself
-           !blockUser.some((block) =2 block.blockTo === user.id) && // Do not display blocked users
-           ((isAdult && user.age >= 20) || !isAdult) && // Display only users aged 20 or older
-           ((searchHobbies?.length > 0 && // If hobbies are specified
-               searchHobbies.some((hobby) =2 user.hobbies.includes(hobby))) ||
-               !searchHobbies) && // Display only users with specified hobbies
+           isNotOwn(user.id, loggedInUser.id) &&
+           isHaveNotBlocked(blockUser, user.id) &&
+           isSearchTargetAge(user.age) &&
+           hasSearchHobbies(user.hobbies) &&
            !user.isSecret // Determine if the search target is not a private account
        ) {
            resultUsers.push(user);
        }
    }

    const message =
        resultUsers.length > 0
            ? `${resultUsers.length} matching users found`
            : "No users match the criteria";

    return { resultUsers, message };
}

[7] Separating General-Purpose and Specific Parts

This is not something I read in a book; it's my own personal know-how.
In the code I implement, I distinguish between "general-purpose processes" and "specific processes." General-purpose processes are unlikely to change in the long run. Conversely, specific processes are the parts where modifications are most likely to occur when specifications change or updates are needed.
By dividing processes into these two categories and designing them so that you only need to modify the specific parts while the rest functions as before, you can limit the scope of modifications, reduce the number of tests, and minimize the risk of introducing bugs through modifications.

However, splitting things too finely can sometimes make the code harder to read, so finding the right balance is crucial.

  • Example (SNS app): Calculating activity points

Example of separating within a function

☑️ Original code

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;

    // Bonus calculation based on user level
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // Bonus based on date
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // Weekend bonus
    }

    return Math.floor(points + bonusPoints);
}

✅ Improvement example

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,
+       // * Can be extended just by adding here if actions increase
+   };
+
+   // Get points corresponding to the action
+   const points = pointsMapping[action];

    let bonusPoints = 0;

    // Bonus calculation based on user level
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // Bonus based on date
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // Weekend bonus
    }

    return Math.floor(points + bonusPoints);
}
Full code
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 to get user level from the database
function getUserLevel(userId) {
    // Logic to get user level from the database
    return userLevels.PREMIUM; // Dummy data
}

// Activity point calculation
function calcActivityPoints(userId, action) {
    const pointsMapping = {
        [userActions.POST]: 10,
        [userActions.COMMENT]: 5,
        [userActions.SHARE]: 8,
        [userActions.LIKE]: 1,
    };

    // Get points corresponding to the action
    const points = pointsMapping[action];

    let bonusPoints = 0;

    // Bonus calculation based on user level
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // Bonus based on date
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // Weekend bonus
    }

    return Math.floor(points + bonusPoints);
}

// Usage example
const userId = "user123";
const action = userActions.POST;
const data = { hasImage: true, hasVideo: false, text: "Hello!" };

const points = calcActivityPoints(userId, action, data);
console.log(`User ${userId} activity points: ${points}`);

Example of separating by function

☑️ Original code

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;
            // Logic specific to a particular campaign
            if (data.campaignId === "summer2024") points *= 2;
            break;

        case userActions.SHARE:
            points = 8;
            // Logic specific to a platform
            if (data.platform === "twitter") points += 3;
            if (data.platform === "facebook") points += 2;
            break;

        case userActions.LIKE:
            points = 1;
            // Logic specific to a content type
            if (data.contentType === "premium") points = 2;
            break;

        default:
            return 0;
    }

    let bonusPoints = 0;

    // Bonus calculation based on user level
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // Bonus based on date
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // Weekend bonus
    }

    return Math.floor(points + bonusPoints);
}

✅ Improvement example

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;
+   };
+ }
+
// Activity point calculation
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;
-           // Logic specific to a particular campaign
-           if (data.campaignId === "summer2024") points *= 2;
-           break;
-
-       case userActions.SHARE:
-           points = 8;
-           // Logic specific to a platform
-           if (data.platform === "twitter") points += 3;
-           if (data.platform === "facebook") points += 2;
-           break;
-
-       case userActions.LIKE:
-           points = 1;
-           // Logic specific to a content type
-           if (data.contentType === "premium") points = 2;
-           break;
-
-       default:
-           return 0;
-   }
+   // Define point calculation logic corresponding to the action
+   const actionFnc = {
+       [userActions.POST]: calcPostPoints(data),
+       [userActions.COMMENT]: calcCommentPoints(data),
+       [userActions.SHARE]: calcSharePoints(data),
+       [userActions.LIKE]: calcLikePoints(data),
+       // * Can be extended just by creating a new calculation function and adding it here if actions increase
+       // * Reduces test costs and bug rates by minimizing changes to existing functions
+   };
+
+   // Execute point calculation logic corresponding to the action
+   const points = actionFnc[action](data);

    let bonusPoints = 0;

    // Bonus calculation based on user level
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // Bonus based on date
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // Weekend bonus
    }

    return Math.floor(points + bonusPoints);
}
Full code
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 to get user level from the database
function getUserLevel(userId) {
    // Logic to get user level from the database
    return userLevels.PREMIUM; // Dummy data
}

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;
    };
}

// Activity point calculation
function calcActivityPoints(userId, action, data) {
    // Define point calculation logic corresponding to the action
    const actionFnc = {
        [userActions.POST]: calcPostPoints(data),
        [userActions.COMMENT]: calcCommentPoints(data),
        [userActions.SHARE]: calcSharePoints(data),
        [userActions.LIKE]: calcLikePoints(data),
    };

    // Execute point calculation logic corresponding to the action
    const points = actionFnc[action](data);

    let bonusPoints = 0;

    // Bonus calculation based on user level
    const userLevel = getUserLevel(userId);
    if (userLevel === userLevels.PREMIUM) {
        bonusPoints = points * 0.2;
    } else if (userLevel === userLevels.VIP) {
        bonusPoints = points * 0.5;
    }

    // Bonus based on date
    const now = new Date();
    if (now.getDay() === 0 || now.getDay() === 6) {
        bonusPoints += points * 0.1; // Weekend bonus
    }

    return Math.floor(points + bonusPoints);
}

// Usage example
const userId = "user123";
const action = userActions.POST;
const data = { hasImage: true, hasVideo: false, text: "Hello!" };

const points = calcActivityPoints(userId, action, data);
console.log(`User ${userId} activity points: ${points}`);

Conclusion

That’s all for now. I hope the sample code provided was readable and gave you an idea of what's happening, even if you're not familiar with the language (JavaScript).
(Please note that the sample logic was based on AI-generated examples, so it may differ from code used in actual business scenarios.)
I will continue to gain experience, read books, and strive to write better code ٩( ᐛ )و


Thank you very much for reading this far.
The article for Day 25 of the TechCommit Advent Calendar 2024 is the finale by Mr. Inoue (the organizer). Stay tuned!

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

https://adventar.org/calendars/10584

References

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

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

I used materials from the following sources:

https://www.irasutoya.com/

https://fukidesign.com/

Discussion