Skip to content

บทที่ 4 — Clean Code

← บทที่ 3 | สารบัญ | บทที่ 5 →

"Clean code reads like well-written prose." — Robert C. Martin (โร-เบิร์ต ซี. มาร์-ติน — เจ้าของฉายา "อันเคิล บ็อบ" / Uncle Bob — ผู้แต่ง Clean Code ปี 2008) (โค้ดที่สะอาด อ่านลื่นเหมือนงานเขียนร้อยแก้วที่เรียบเรียงมาดี)

⚖️ เสียงค้านยุคใหม่ (ต้องรู้ก่อนเชื่อทุกคำ): หนังสือ Clean Code (2008) มีอิทธิพลมาก แต่มีคนค้านตั้งแต่ปี 2018 เป็นต้นมา:

  • John Ousterhout (จอห์น เอาส-เตอร์-เฮาท์) — A Philosophy of Software Design (2018, 2nd ed 2021) เสนอ "deep modules" — function ยาวที่ทำหน้าที่ชัด มักอ่านง่ายกว่า function เล็กเยอะ ๆ
  • Kent Beck (เคนต์ เบ็ค) — Tidy First? (2023) เสนอแนวคิด empirical (อิม-พี-ริ-คอล = อิงจากข้อมูลจริง) แทน dogma
  • DHH (David Heinemeier Hansson — เด-วิด ไฮ-เน-ไม-เออร์ ฮัน-สัน — ผู้สร้าง Ruby on Rails) ปี 2024 — "Clean Code is bad code" (ถ้าทำเป็น dogma)

สรุป: กฎใน Clean Code = guideline ไม่ใช่ commandment — context สำคัญกว่ากฎ

หลังจบบท คุณจะ:

  • ตั้งชื่อ variable, function, class ที่ดี
  • เขียน function ที่อ่านง่าย + ทดสอบง่าย
  • รู้ว่าเมื่อไหร่ comment ดี / แย่
  • จัดการ error อย่าง consistent
  • เขียน code ที่ "self-documenting"

1. Naming — กฎที่สำคัญที่สุด

การตั้งชื่อคือทักษะ clean code ที่สำคัญที่สุด เพราะเราอ่านโค้ดมากกว่าเขียน — ชื่อที่ดี "บอกเจตนา" จนไม่ต้องมี comment, ไม่ทำให้เข้าใจผิด และค้นหาได้ ส่วนนี้รวมกฎการตั้งชื่อที่ทำให้โค้ดอ่านเหมือนอ่านภาษาคน:

"There are only two hard things in Computer Science: cache invalidation (แคช อิน-แวล-ลิ-เด-ชั่น = การ "ล้าง" cache ให้ถูกจังหวะ) and naming things." — Phil Karlton (ฟิล คาร์ล-ตัน — วิศวกร Netscape ยุค 90) (ในวิทยาการคอมพิวเตอร์มีเรื่องยากอยู่แค่สองอย่าง: การล้าง cache ให้ถูกจังหวะ กับการตั้งชื่อ)

1.1 Reveal Intent

java
// ❌ มี comment ก็เพราะชื่อไม่ดี
int d;   // elapsed time in days

// ✅ ชื่อบอกหมด
int elapsedTimeInDays;
int daysSinceCreation;
int fileAgeInDays;

1.2 Avoid Disinformation

java
// ❌ ไม่ใช่ list — เป็น ArrayList
List<User> userList = new ArrayList<>();

// ✅ ใช้ "users" — อย่ามีคำว่า "list" เว้นแต่จริง ๆ
List<User> users = new ArrayList<>();

// ❌ ตัวอักษรใกล้กันที่อ่านยาก
int l = 1;   // l vs 1
int O = 0;   // O vs 0

// ✅
int line = 1;
int origin = 0;

1.3 Use Pronounceable Names

java
// ❌
Date genymdhms;
Date modymdhms;

// ✅
Date generationTimestamp;
Date modificationTimestamp;

1.4 Use Searchable Names

java
// ❌ Search 5 → return หลายร้อยที่
for (int j = 0; j < 5; j++) { ... }

// ✅
final int WORK_DAYS_PER_WEEK = 5;
for (int j = 0; j < WORK_DAYS_PER_WEEK; j++) { ... }

1.5 Class Names = Noun

java
✅ User, Customer, Account, Address, Invoice
❌ Manager, Processor, Data, Info   ← เลี่ยง (ถ้าทำได้)

1.6 Method Names = Verb

java
✅ getUser, saveOrder, deleteAccount, sendEmail
❌ user, order, account   (ไม่บอก action)

1.7 Don't Be Cute

java
whackEverything()       // เก๋แต่งง
holyHandGrenade()
deleteAll()
removeExpiredEntries()

1.8 One Word per Concept

ทั่ว codebase:

java
get(...)  ใช้ทุกที่ที่ดึงข้อมูล
❌ มี get / fetch / retrieve / load — ใช้ปนกัน

1.9 Use Domain Names

java
// E-commerce context
✅ cart, order, lineItem, sku
❌ list, items, things, data

1.10 Boolean = is/has/should

java
✅ isActive, hasPermission, shouldRetry, canEdit
❌ active, permission, retry, edit

1.11 Constant = SCREAMING_SNAKE

java
✅ MAX_RETRIES = 3
✅ DEFAULT_TIMEOUT_MS = 5000
❌ max_retries
❌ MaxRetries

1.12 Don't Encode Type

java
❌ String nameStr;
int countInt;
❌ List<User> userListList;

✅ String name;
int count;
✅ List<User> users;

1.13 ความยาว — Context-aware

  • Loop variable → 1 letter OK (i, j)
  • Function scope → short (name, count)
  • Class field → descriptive (userPreferences)
  • Public API → very clear (maximumNumberOfRetries)

2. Functions

2.1 Small — Smaller is Better

java
// ❌ 300-line function
public void processOrder() {
    // ...
}

// ✅ Compose จาก function เล็ก
public void processOrder(Order order) {
    validateOrder(order);
    calculateTotal(order);
    chargePayment(order);
    saveOrder(order);
    sendConfirmation(order);
}

กฎ: function ≤ 20 บรรทัด (กฎหยาบ ๆ) — ถ้าเกิน คิดว่ามี responsibility มากเกินไหม

⚖️ เสียงค้าน (ต้องรู้): John Ousterhout ในหนังสือ A Philosophy of Software Design (2018) ค้านกฎนี้ — บอกว่าการ split function สั้น ๆ มากเกินไปทำให้ logic กระจัดกระจาย ต้อง "โดด" ไป-มาเพื่อตามอ่าน
แนวคิด "deep modules" ของเขา: function ที่ยาวขึ้นแต่ มี purpose ชัดเจน + interface แคบ มักอ่านง่ายกว่า
สรุป: 20 บรรทัด = guideline ไม่ใช่ commandment — context สำคัญ ถ้า function ยาว 40 บรรทัดแต่ทำเรื่องเดียวลื่น ๆ ก็ดีกว่าแตกเป็น 5 function ที่ต้องตามอ่าน

✏️ คำพูดเสริม: "Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian Kernighan (ไบรอัน เคอ-นิ-แกน — ผู้ร่วมสร้างภาษา C)
(การ debug ยากกว่าการเขียนโค้ดสองเท่า ฉะนั้นถ้าเขียนโค้ด "ฉลาด" ที่สุดเท่าที่ทำได้ — แปลว่าคุณไม่ฉลาดพอที่จะ debug มัน → เขียนให้เรียบง่ายไว้ก่อน)

2.2 Do One Thing

java
// ❌ ทำหลายอย่าง
public void emailUsersAndUpdateBalance(List<User> users) {
    for (User u : users) {
        emailService.send(u.getEmail(), ...);   // 1
        u.setBalance(u.getBalance() - 10);       // 2
        repo.save(u);                            // 3
    }
}

// ✅ Split
public void chargeAndNotifyUsers(List<User> users) {
    for (User u : users) {
        chargeUser(u);
        notifyUser(u);
    }
}

private void chargeUser(User u) {
    u.setBalance(u.getBalance() - 10);
    repo.save(u);
}

private void notifyUser(User u) {
    emailService.send(u.getEmail(), ...);
}

2.3 One Level of Abstraction

java
// ❌ Mix high + low level
public void processOrder(Order order) {
    Connection conn = DriverManager.getConnection(url);    // low
    if (order.getTotal() > 1000) applyDiscount(order);     // high
    PreparedStatement stmt = conn.prepareStatement("...");   // low
    sendEmail(order.getCustomer());                          // high
}

// ✅ Same level
public void processOrder(Order order) {
    applyBusinessRules(order);
    saveOrder(order);
    notifyCustomer(order);
}

2.4 Function Arguments — น้อย ๆ

0 arguments = best
1 argument  = good
2 arguments = ok
3 arguments = consider refactor
4+ arguments = ❌ probably wrong

⚖️ เสียงค้าน: "0 arguments = best" เป็นกฎที่สุดโต่ง — ถ้า function ไม่รับ argument เลย แปลว่ามันต้องไปดึง state จากที่อื่น (global, field) → กลายเป็น hidden dependency (Ousterhout, Sandi Metz เตือนเรื่องนี้)
กฎที่ปลอดภัยกว่า: 1-3 args ที่ "explicit + จำเป็นจริง" ดีกว่า 0 args ที่ซ่อน state

💡 ทำไม long parameter list = bug magnet?

  1. เรียงผิดง่าย — เปลี่ยน (name, email) เป็น (email, name) compiler ไม่จับ (type เหมือนกัน) — bug หลุดถึง production
  2. อ่านยากcreateUser("John", "j@x.com", 25, "0812345678", ...) ดูแล้วไม่รู้ว่าตัวไหนคืออะไร
  3. เพิ่ม-ลด field ลำบาก — ทุก caller ต้องแก้
  4. บอกเป็นนัยว่า function ทำหลายเรื่อง — รับ 8 ค่าแปลว่ามันต้องสนใจหลาย concern → ละเมิด SRP
java
// ❌
void createUser(String name, String email, int age, String phone, 
                String address, String city, String role, boolean isActive) { ... }

// ✅ Object parameter
void createUser(CreateUserRequest req) { ... }

record CreateUserRequest(
    String name, String email, int age,
    String phone, String address, String city,
    String role, boolean isActive
) {}

2.5 No Boolean Flag

java
// ❌ Flag = doing 2 things
sendEmail(user, true);   // urgent? high priority? html?

// ✅ Split methods
sendEmail(user);
sendUrgentEmail(user);

2.6 No Side Effects

java
// ❌ Side effect ไม่บอกในชื่อ
public boolean checkPassword(String password) {
    if (matches(password)) {
        Session.initialize();          // ⚠️ side effect!
        return true;
    }
    return false;
}

// ✅ ชื่อบอกหมด
public boolean isPasswordValid(String password) {
    return matches(password);
}

public void login(User user) {
    Session.initialize(user);
}

2.7 Command-Query Separation

java
// ❌ ทั้ง command + query
public boolean set(String key, String value) {
    map.put(key, value);
    return map.containsKey(key);
}

// ✅ Separate
public void set(String key, String value) { map.put(key, value); }
public boolean has(String key) { return map.containsKey(key); }

กฎ: function = command (do something) หรือ query (return value) — ไม่ใช่ทั้งคู่

2.8 Prefer Exception over Error Code

java
// ❌ Error code — caller ต้องเช็ค ลืม → bug
public int deleteFile(String path) {
    if (!exists(path)) return -1;
    if (!hasPermission(path)) return -2;
    // ...
    return 0;
}

// Caller
int result = deleteFile(...);
if (result == -1) { ... }
else if (result == -2) { ... }

// ✅ Exception
public void deleteFile(String path) {
    if (!exists(path)) throw new FileNotFoundException(path);
    if (!hasPermission(path)) throw new AccessDeniedException(path);
    // ...
}

🆕 Java exception 101 (สำหรับมือใหม่): Java แบ่ง exception เป็น 2 ชนิดหลัก:

  • Checked exception (extends Exception) — compiler บังคับให้ caller try/catch หรือ throws ต่อ — เช่น IOException, SQLException
  • Unchecked exception (extends RuntimeException) — ไม่บังคับ caller จัดการ — เช่น IllegalArgumentException, NullPointerException

ตัวอย่างข้างบนใช้ FileNotFoundException (checked, จาก java.io) และ AccessDeniedException (checked, จาก java.nio.file) — caller ที่เรียก deleteFile() ต้อง try/catch หรือ throws ต่อให้ compiler ผ่าน

2.8.1 ใช้ BigDecimal ไม่ใช่ double สำหรับเงิน

java
// ❌ double — มี floating-point error
double price = 0.1 + 0.2;   // = 0.30000000000000004 (!)

// ❌ ตัวอย่างที่เห็นในบทอื่นที่ใช้ double กับ amount/balance
class Account {
    public void withdraw(double amount) { ... }   // ❌ เสี่ยงเงินผิดเซ็นต์
}

// ✅ BigDecimal — แม่นยำ
import java.math.BigDecimal;

class Account {
    private BigDecimal balance;

    public void withdraw(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        balance = balance.subtract(amount);
    }
}

📌 กฎเหล็ก: เงิน/ราคา/ยอดเงิน → ใช้ BigDecimal เสมอ (หรือเก็บเป็น long หน่วย "satang") — double/float เก็บได้ใกล้เคียงแต่ไม่ตรง → ทำให้ยอดผิดเซ็นต์ทีละน้อยจนสะสมเป็นล้าน
ตัวอย่าง LSP ที่ใช้ double amount ใน Account class ก็ควรเปลี่ยนเป็น BigDecimal ในงานจริง

2.9 Don't Return Null

java
// ❌
public User findUser(Long id) {
    User u = repo.findById(id);
    return u;   // อาจ null
}

// Caller
User u = findUser(1);
if (u != null) u.doSomething();   // ลืม → NPE

// ✅ Optional
public Optional<User> findUser(Long id) {
    return repo.findById(id);
}

// ✅ Throw
public User getUser(Long id) {
    // orElseThrow รับ Supplier<X extends Throwable> — lambda () -> ... คือ Supplier ที่ "สร้าง exception เมื่อจำเป็น"
    // ดูเรื่อง Optional, lambda, method reference เพิ่มในบทที่ 5 (Functional Programming)
    return repo.findById(id).orElseThrow(() -> new UserNotFoundException(id));
}

🆕 ทางเลือกยุคใหม่ (2024+) นอก Optional:

  • JSpecify annotations: @Nullable/@NonNull — มาตรฐานใหม่ของ Java ecosystem แทน @Nullable ของ JetBrains/Eclipse — IDE และ tool (NullAway, Error Prone) ใช้ตรวจ null บน field/parameter ตอน compile
  • JEP 8303099 null-restricted types (preview ตั้งแต่ JDK 23+) — Java จะมี String! (non-null) และ String? (nullable) แบบ Kotlin → compiler บังคับเอง
    ในงาน 2026: Optional ยังใช้ดีกับ return type, แต่บน field/parameter ใช้ @Nullable/@NonNull ของ JSpecify ดีกว่า (Optional ไม่ควรใส่ใน field — สิ้นเปลือง memory + serialize ยาก)

2.10 Don't Pass Null

java
// ❌
notification.send(null, "Hello");

// ✅ Use empty / default
notification.send(SystemUser.INSTANCE, "Hello");

3. Comments

comment ที่ดีอธิบาย "ทำไม" ไม่ใช่ "ทำอะไร" (โค้ดบอกอยู่แล้วว่าทำอะไร) — comment ส่วนใหญ่ที่เห็นเป็น "กลิ่น" ของชื่อที่ไม่ดีหรือโค้ดที่ซับซ้อนเกิน ส่วนนี้แยก comment ที่ควรมี (เจตนา, business reason, warning) จากที่ควรลบ (อธิบายโค้ดที่ชื่อบอกได้):

"Don't comment bad code—rewrite it." — Brian Kernighan (ไบรอัน เคอ-นิ-แกน — ผู้ร่วมสร้างภาษา C, ผู้เขียน The Practice of Programming) (อย่าเขียน comment อธิบายโค้ดห่วย ๆ — เขียนโค้ดใหม่ให้ดีไปเลย)

3.1 Good Comments

java
// Copyright (c) 2026 MyCompany. Apache 2.0

Explanation of Intent (Why)

java
// We delay 5 seconds because the third-party API has rate limit
// of 12 req/min and bursts cause 429 errors. See JIRA-1234.
Thread.sleep(5000);

Warning / Constraints

java
// WARNING: This must run before initializeCache() — depends on
// the database connection being ready.

Performance Note

java
// This regex is hot — compiled once. Don't move into loop.
private static final Pattern EMAIL = Pattern.compile(...);

TODO with Context

java
// TODO(jira-456): Remove this when migration to v2 API is complete (deadline Q3 2026)

3.2 Bad Comments

Restating Code

java
// ❌ บอกสิ่งที่ code อ่านได้
i++;            // increment i
user.save();    // save user

Outdated

java
// ❌ comment + code ไม่ตรง
// Apply 10% VAT
double tax = price * 0.07;

Misleading

java
// ❌
// Returns user or null
public User findUser(Long id) {
    return ...;   // actually throws if not found
}

Commented-Out Code

java
// ❌ Don't keep
// public void oldMethod() {
//     // ... 100 lines
// }

→ ลบเลย — git history เก็บไว้

3.3 Self-documenting > Comment

🔤 self-documenting code (เซลฟ์-ด๊อก-คิว-เมน-ติ้ง) = โค้ดที่ "อธิบายตัวเองได้" ผ่านชื่อและโครงสร้างที่ดี จนแทบไม่ต้องเขียน comment กำกับ

java
// ❌
// Check if user is admin and active
if (user.role == 1 && user.status == 1) { ... }

// ✅ Code explains itself
if (user.isActiveAdmin()) { ... }

// User class
public boolean isActiveAdmin() {
    return role == Role.ADMIN && status == Status.ACTIVE;
}

4. Code Structure

นอกจากชื่อ การ "จัดวาง" โค้ดก็ส่งผลต่อความอ่านง่าย — โค้ดที่เกี่ยวข้องกันควรอยู่ใกล้กัน (vertical distance), function สั้นทำอย่างเดียว, จัดลำดับจากบนลงล่างเหมือนอ่านบทความ ส่วนนี้รวมหลักการจัดโครงสร้างที่ทำให้ไล่อ่านโค้ดได้ลื่น:

4.1 Vertical Distance

Related code อยู่ใกล้กัน:

java
// ❌ Helper function ไกล
class UserService {
    public void process(User u) {
        validate(u);
        save(u);
    }
    
    // ... 50 other methods ...
    
    private void validate(User u) { ... }
}

// ✅ Helper ใกล้ caller
class UserService {
    public void process(User u) {
        validate(u);
        save(u);
    }
    
    private void validate(User u) { ... }
    
    private void save(User u) { ... }
}

Stepdown Rule (สเตป-ดาวน์ = ลดระดับลง — "กฎลดระดับ"): จัดเรียงให้ method ระดับสูง (ภาพรวม) อยู่บน แล้วไล่ลงไปหา method ระดับรายละเอียดด้านล่าง — อ่านไล่จากบนลงล่างได้เหมือนอ่านบทความที่ค่อย ๆ ลงลึกทีละขั้น

4.2 Newspaper Article Order

🌐 อธิบายสำนวน: "Newspaper Article Order" = วิธีเรียงเนื้อหาแบบบทความข่าวฝรั่ง (เรียกว่า "inverted pyramid" / พีระมิดกลับหัว) — ขึ้นต้นด้วย headline (สำคัญสุด) → lead paragraph (สรุปใจความ) → body (รายละเอียดเชิงลึก) → footer (เกร็ดเสริม) — ผู้อ่านเลิกอ่านได้ทุกจุดและยังได้ใจความหลัก
(สื่อไทยมักเรียงต่างกัน — เริ่มเล่ายาว ๆ ค่อยถึงประเด็น ฝรั่งกลับด้าน)

class UserService {
    // 1. Important public methods first (the headline)
    public void register(User user) { ... }
    public void login(...) { ... }
    
    // 2. Helper methods after
    private void validatePassword(...) { ... }
    private void sendWelcomeEmail(...) { ... }
}
java
class Order {
    // === State ===
    private OrderStatus status;
    private LocalDateTime createdAt;
    
    // === Items ===
    private List<OrderItem> items;
    private BigDecimal subtotal;
    
    // === Payment ===
    private PaymentMethod paymentMethod;
    private BigDecimal paid;
    
    // === Methods ===
    // ...
}

4.4 Indentation = Logical Structure

java
// ❌ Deeply nested
if (user != null) {
    if (user.isActive()) {
        if (user.hasPermission()) {
            if (request.isValid()) {
                process();
            }
        }
    }
}

// ✅ Early return / guard clause
if (user == null) return;
if (!user.isActive()) return;
if (!user.hasPermission()) throw new ForbiddenException();   // ⚠️ throw = เปลี่ยน contract ตั้งใจ
if (!request.isValid()) throw new ValidationException();

process();

⚠️ หมายเหตุเรื่อง contract: การเปลี่ยนจาก nested if (return เงียบ ๆ) → early throw เป็นการ เปลี่ยน contract ตั้งใจ — caller เดิมที่คาดว่า function จะ "เงียบ" ตอน permission ไม่พอ จะเจอ exception แทน
ถ้ามี behavior เดิมที่อยากคง → ใช้ return ทั้งหมดแทน throw
ถ้าต้องการ fail fast เรื่องสิทธิ์ → throw ดีกว่า เพราะ "permission ไม่พอ" คือ programmer/security error ไม่ใช่ business flow ปกติ


5. Error Handling

error handling ที่ clean ทำให้โค้ดหลักอ่านง่ายและจัดการความผิดพลาดได้จริง — ใช้ exception (ไม่ใช่ return code), catch ให้เจาะจง, ห้าม "กลืน" exception เงียบ ๆ และอย่าใช้ null ส่งสัญญาณ error ส่วนนี้รวมหลักการที่กันบั๊กจาก error handling ที่หละหลวม:

5.1 Use Exceptions, Not Return Codes (recap)

5.2 Specific Exception > Generic

java
// ❌
catch (Exception e) { ... }

// ✅
catch (IOException e) { ... }
catch (SQLException e) { ... }

5.3 Don't Swallow Exceptions

java
// ❌ Silent
try {
    risky();
} catch (Exception e) {
    // ignore
}

// ✅ At least log
try {
    risky();
} catch (IOException e) {
    log.error("Failed to read file: {}", path, e);
    throw new BusinessException("Could not process file", e);
}

5.4 Try-with-Resources

🔤 คำอ่าน: Try-with-Resources (ทราย-วิธ-รี-ซอร์-ซิส) — syntax ของ Java 7+ ที่ปิด resource ให้อัตโนมัติ

🆕 ทำงานยังไง: ทุก resource ที่ implement AutoCloseable (interface ที่มี method close()) — ใส่ไว้ในวงเล็บหลัง try (...) Java จะเรียก close() ให้อัตโนมัติเมื่อจบ block (ทั้งกรณีปกติและกรณี exception)

java
// ❌
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader(path));
    return reader.readLine();
} finally {
    if (reader != null) reader.close();   // ต้องเช็ค null + handle IOException เอง
}

// ✅
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
    return reader.readLine();
    // Java auto-call reader.close() ที่นี่ (ปกติ หรือ exception ก็ปิด)
}

5.5 Custom Exceptions Tell Story

java
// ❌ Generic
throw new RuntimeException("Bad");

// ✅ Domain-specific
throw new InsufficientFundsException(account, amount);

class InsufficientFundsException extends DomainException {
    public InsufficientFundsException(Account account, BigDecimal amount) {
        super(String.format("Account %s has insufficient funds for %s", 
            account.id(), amount));
    }
}

5.6 Define Exception Hierarchy

java
public abstract class AppException extends RuntimeException { ... }

public abstract class DomainException extends AppException { ... }

public class UserNotFoundException extends DomainException { ... }
public class InsufficientFundsException extends DomainException { ... }

public abstract class TechnicalException extends AppException { ... }
public class DatabaseException extends TechnicalException { ... }
public class ExternalServiceException extends TechnicalException { ... }

→ Catch by category — catch (DomainException e) → return 4xx, catch (TechnicalException e) → 5xx

💬 Checked vs Unchecked — debate:

  • ตัวอย่างข้างบนใช้ extends RuntimeException (unchecked) — caller ไม่ถูกบังคับให้ catch → flexible แต่ลืม handle ได้
  • บางทีม (เช่นทีมที่เน้น "fail loud" ในระบบการเงิน) ใช้ checked exception สำหรับ domain error เพื่อบังคับ caller จัดการ
  • มาตรฐานยุค modern (Spring, Java EE) นิยม unchecked เพราะลด boilerplate ของ throws ... throws ... ใน signature
  • กฎจำง่าย: ใช้ unchecked เป็น default — ใช้ checked เฉพาะกรณีที่ "caller ต้องจัดการแน่ ๆ และมีทางแก้"

6. Boundaries

"boundary" คือจุดที่โค้ดเราเชื่อมกับโลกภายนอก (library, external API, HTTP) — ควรห่อ dependency ภายนอกด้วย interface ของเราเอง (สลับ vendor ได้โดยแก้ที่เดียว) และใช้ DTO ที่ชายแดน (ไม่ leak entity ภายในออก API) ส่วนนี้สอนการจัดการ boundary ให้โค้ดไม่ผูกกับของนอก:

6.1 Wrap External APIs

java
// ❌ Use library directly everywhere
class UserService {
    public void register(User u) {
        SendGridClient client = new SendGridClient(apiKey);
        Email email = new Email();
        email.from = "noreply@app.com";
        email.to = u.getEmail();
        // ... SendGrid-specific code
        client.send(email);
    }
}

// ✅ Wrap behind interface
interface EmailService {
    void send(EmailRequest req);
}

class SendGridEmailService implements EmailService {
    // SendGrid-specific
}

class UserService {
    private final EmailService emailService;
    
    public void register(User u) {
        emailService.send(EmailRequest.welcome(u));
    }
}

→ Switch SendGrid → Mailgun = 1 class change

6.2 Use DTO at Boundary

java
// ❌ Expose entity to API
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) {
    return userRepo.findById(id);   // ⚠️ might expose password, ssn
}

// ✅ DTO
@GetMapping("/users/{id}")
public UserResponse get(@PathVariable Long id) {
    return UserResponse.from(userRepo.findById(id));
}

record UserResponse(Long id, String name, String email) {
    static UserResponse from(User u) {
        return new UserResponse(u.getId(), u.getName(), u.getEmail());
    }
}

7. Tests = Clean Code Too

test code ก็ต้อง clean เหมือน production code เพราะมันคือเอกสารที่อธิบายว่าระบบทำงานยังไง — test ที่ดีตามหลัก FIRST (เร็ว, อิสระ, ทำซ้ำได้) และอ่านออกว่าทดสอบ "พฤติกรรม" อะไร test ที่รกหรือเปราะทำให้คนไม่กล้าแก้โค้ด:

7.1 F.I.R.S.T.

  • Fast — เร็ว รันได้ไว (จะได้รันบ่อย ๆ ไม่ขี้เกียจ)
  • Independent — อิสระ ไม่ผูกลำดับ test (test ตัวไหนรันก่อน-หลังก็ได้ผลเหมือนกัน)
  • Repeatable — ทำซ้ำได้ รันกี่ครั้งกี่เครื่องก็ผลเดิม
  • Self-validating — ตรวจผลเองได้ บอกผ่าน/ไม่ผ่านชัดเจน ไม่ต้องมานั่งดูเอง
  • Timely — เขียนทันเวลา เขียน test ไปพร้อมกับโค้ด (ไม่ใช่ทิ้งไว้ทีหลัง)

7.2 AAA Pattern

java
// @Test และ assertEquals มาจาก JUnit 5 — testing framework ของ Java (ดูบทที่ 17 Testing)
@Test
void shouldDepositMoney() {
    // Arrange
    Account account = new Account(100);
    
    // Act
    account.deposit(50);
    
    // Assert
    assertEquals(150, account.balance());
}

7.3 One Assert per Test (มัก)

java
// ❌ 5 things in 1 test
@Test
void testEverything() {
    User u = service.create("anna@x.com", "Anna");
    assertNotNull(u.getId());
    assertEquals("anna@x.com", u.getEmail());
    assertEquals("Anna", u.getName());
    assertTrue(u.isActive());
    assertFalse(u.isAdmin());
}

// ✅ Split or use SoftAssertions / AssertJ chain
// AssertJ = library assertion ภายนอก (ต้อง add dependency org.assertj:assertj-core:3.20+)
// .returns(expected, extractor) — extractor เป็น method reference (User::getEmail = (u) -> u.getEmail())
@Test
void shouldCreateUserWithCorrectAttributes() {
    User u = service.create("anna@x.com", "Anna");
    
    assertThat(u)
        .returns("anna@x.com", User::getEmail)
        .returns("Anna", User::getName)
        .returns(true, User::isActive)
        .returns(false, User::isAdmin);
    assertThat(u.getId()).isNotNull();
}

📚 AssertJ — รู้จักไว้: library assertion ที่ใช้ทั่ว Java ecosystem ปัจจุบัน (Spring Boot ใส่มาให้ด้วย) เขียน fluent อ่านเหมือนภาษาคน เช่น assertThat(list).hasSize(3).contains("a", "b") — ดูบทที่ 17 (Testing) สำหรับรายละเอียด

7.4 Descriptive Test Names

java
// ❌
@Test void test1() { ... }
@Test void testUser() { ... }

// ✅
@Test void shouldRejectRegistrationWhenEmailAlreadyExists() { ... }
@Test void shouldChargeUserWhenStockAvailable() { ... }

8. Concurrent Code

โค้ดที่ทำงานหลาย thread พร้อมกันเป็นจุดที่บั๊กซ่อนได้ลึกที่สุด (race condition, deadlock) — หลักคือแยกส่วนที่เกี่ยวกับ concurrency ไปไว้ที่เดียว (อย่าโรย synchronized กระจายทั่วโค้ด) และใช้ข้อมูลแบบ immutable ส่งข้าม thread (ไม่ต้อง lock เลย):

8.1 Keep Concurrency in One Place

ℹ️ หมายเหตุ: บทนี้แตะ concurrency แค่หลักการเขียนให้ clean — สำหรับ thread/lock/race condition ลึก ๆ ดู resource ภายนอก (เช่น Java Concurrency in Practice ของ Brian Goetz) เพราะ scope ของหนังสือนี้ไม่ครอบ concurrency primitives เต็ม

java
// ❌ Mix throughout
class UserService {
    public synchronized void update(...) { ... }
    public void process(...) { synchronized (this) { ... } }
}

// ✅ Encapsulate
// ConcurrentHashMap = thread-safe Map ของ Java (รับ read/write จากหลาย thread พร้อมกันได้)
class ThreadSafeUserCache {
    private final Map<Long, User> cache = new ConcurrentHashMap<>();
    public User get(Long id) { return cache.get(id); }
    public void put(User u) { cache.put(u.getId(), u); }
}

8.2 Immutable Data Across Threads

java
// ❌ Mutable
class Config {
    private String host;
    public void setHost(String h) { host = h; }
    public String getHost() { return host; }
}

// ✅ Immutable (record)
record Config(String host, int port, String username) {}

→ No synchronization needed


9. Real-world Examples

ตัวอย่างนี้รวมหลักทั้งหมดเข้าด้วยกัน — โค้ดเดิม (Before) ใช้ชื่อตัวแปรสั้นจับใจความไม่ได้ (p, t, ls), loop+cast แบบ manual, และ logic ฝังอยู่ในเงื่อนไขซ้อน ส่วนโค้ดใหม่ (After) ใช้ชื่อสื่อความหมาย + type ชัด + stream ทำให้อ่านปุ๊บเข้าใจปั๊บ:

Before

java
public void p(List<Object> ls) {
    int t = 0;
    for (int i = 0; i < ls.size(); i++) {
        Object o = ls.get(i);
        if (o instanceof Map) {
            Map m = (Map) o;
            if (m.get("type").equals("o")) {
                t = t + (Integer) m.get("a");
            }
        }
    }
    System.out.println(t);
}

After

java
public void printTotalRevenue(List<Transaction> transactions) {
    int total = transactions.stream()
        .filter(t -> t.type() == TransactionType.ORDER)
        .mapToInt(Transaction::amount)
        .sum();
    
    System.out.println("Total revenue: " + total);
}

record Transaction(TransactionType type, int amount) {}
enum TransactionType { ORDER, REFUND }

→ Same logic, easier to read + maintain


10. Code Review Checklist

checklist นี้ใช้เป็น "ไม้บรรทัด" ตอน review PR (ของตัวเองหรือเพื่อน) — ไล่ดูทีละข้อว่าโค้ดผ่านเกณฑ์ clean code หรือยัง ก่อนกด approve ครอบคลุมตั้งแต่ naming, ขนาดฟังก์ชัน, error handling, edge case, test, ไปจนถึง security และ performance:

□ Naming clear?
□ Function < 20 lines? Single responsibility?
□ Comments explain WHY?
□ No commented-out code?
□ Error handling explicit?
□ Edge cases handled (null, empty, max, min)?
□ Tests cover happy path + error?
□ No magic numbers/strings?
□ DRY — duplication?
□ Imports clean?
□ Logs at appropriate level?
□ Security: input validated, output encoded?
□ Performance: obvious N+1, O(n²) avoided?
□ Money uses BigDecimal (not double/float)?

— 🆕 รายการสำหรับยุค AI + TypeScript (2026):
□ AI-generated code มีคน review แล้วหรือยัง?
□ TypeScript — มี `any` หลุดมาไหม? (strict mode บังคับ?)
□ Type narrowing (เช่น `instanceof`, `typeof`) ใช้ถูกต้องไหม?
□ ไม่มี secret/API key hard-code (ตรวจ git history ด้วย)?

Sandi Metz Rules (แซน-ดี เม็ทซ์ — ผู้แต่ง Practical Object-Oriented Design in Ruby)

กฎ 4 ข้อสุดโต่งจาก Sandi Metz (สำหรับ pair programming / refactoring exercise — ไม่ใช่กฎตายตัวสำหรับ production แต่เป็น "เป้าหมายระหว่างฝึก"):

  1. Class ห้ามเกิน 100 บรรทัด
  2. Method ห้ามเกิน 5 บรรทัด
  3. Method รับ argument ห้ามเกิน 4 ตัว (รวม keyword args ใน Ruby)
  4. Controller ส่งให้ view ได้แค่ 1 instance variable

→ กฎเข้มงวดสุด ๆ — เธอบอกเองว่า "break the rule when you have a good reason" — ใช้เป็นเครื่องมือฝึกสายตา ไม่ใช่บังคับใช้ทุกกรณี

Boy Scout Rule (กฎลูกเสือ — บอย-สเกาท์-รูล)

"Always leave the campground cleaner than you found it." (ทุกครั้งที่ออกจากแคมป์ — ทำให้สะอาดกว่าตอนมาถึง)

ดัดแปลงโดย Robert Martin ให้เป็นกฎเขียนโค้ด:

"Always leave the code cleaner than you found it." (ทุกครั้งที่ commit — ทำให้โค้ดสะอาดกว่าตอนเริ่ม)

วิธีทำ: เห็นชื่อแย่ → rename, เห็น function ยาว → split เล็กน้อย, เห็น comment ที่ outdate → update — ไม่ต้องรอ "PR refactor ใหญ่" ทำทีละนิดทุก commit

→ codebase ดีขึ้นเรื่อย ๆ โดยไม่ต้อง "Big Bang refactor"


12.5 Glossary บทนี้ (คำอ่าน)

ศัพท์คำอ่านความหมายสั้น
Clean Codeคลีน-โค้ดโค้ดที่อ่านง่าย แก้ง่าย เหมือนงานเขียน
self-documentingเซลฟ์-ด๊อก-คิว-เมน-ติ้งโค้ดที่อธิบายตัวเองได้
proseโพรสร้อยแก้ว (งานเขียนที่ไม่ใช่บทกวี)
cache invalidationแคช อิน-แวล-ลิ-เด-ชั่นล้าง cache ให้ถูกจังหวะ
command-query separationคอม-มานด์-เคว-รี เซ็พ-เพอ-เร-ชั่นแยก function ทำ vs function ถาม
Stepdown Ruleสเตป-ดาวน์ รูลกฎเรียง method จากบน (สูง) ลงล่าง (ละเอียด)
Newspaper Article Orderนิวส์-เปเปอร์ อาร์-ติ-เคิล ออร์-เดอร์เรียงแบบบทความข่าว (สำคัญสุดขึ้นต้น)
Try-with-Resourcesทราย-วิธ-รี-ซอร์-ซิสsyntax ปิด resource อัตโนมัติ (Java 7+)
AutoCloseableออ-โต-โคล-สะ-เบิ้ลinterface ที่บอกว่า resource ปิดได้
checked/unchecked exceptionเช็ค-ด/อัน-เช็ค-ด เอ็กซ์-เซ็พ-ชั่นexception ที่ compiler บังคับ/ไม่บังคับ catch
AssertJแอส-เซิร์ท-เจlibrary assertion fluent ของ Java
JUnitเจ-ยู-นิตtesting framework ของ Java
AAA patternเอเอ-เอ-แพท-เทิร์นArrange / Act / Assert
FIRSTเฟิร์สต์กฎ test: Fast/Independent/Repeatable/Self-validating/Timely
DTOดี-ที-โอData Transfer Object — object สำหรับขนข้อมูลข้าม boundary
boundaryบาวด์-ดะ-รีชายแดนระหว่างโค้ดเรากับของนอก
Boy Scout Ruleบอย-สเกาท์-รูลทุก commit ทำโค้ดสะอาดกว่าเดิม
Sandi Metzแซน-ดี เม็ทซ์นักเขียน Ruby OOP (กฎ 4 ข้อ)
Brian Kernighanไบรอัน เคอ-นิ-แกนผู้ร่วมสร้างภาษา C
Phil Karltonฟิล คาร์ล-ตันวิศวกร Netscape (quote naming)
Robert C. Martinโร-เบิร์ต ซี. มาร์-ติน"Uncle Bob" ผู้แต่ง Clean Code
John Ousterhoutจอห์น เอาส-เตอร์-เฮาท์ผู้แต่ง A Philosophy of Software Design
Kent Beckเคนต์ เบ็คผู้ร่วมสร้าง XP/TDD ผู้แต่ง Tidy First?
DHHดี-เอช-เอชDavid Heinemeier Hansson ผู้สร้าง Rails
Donald Knuthโดนัลด์ คะ-นูธquote "premature optimization is the root of all evil"

11. ⚠️ Common Pitfalls

ตารางนี้สรุปกับดักที่มือใหม่ติดบ่อยที่สุดคู่กับวิธีที่ถูกต้อง — ฝั่งซ้ายคือสิ่งที่ควรเลิกทำ ฝั่งขวาคือสิ่งที่ควรทำแทน ใช้เป็นลิสต์ "อย่าทำ" แบบเร็ว ๆ เวลาเขียนหรือ review โค้ด:

data, info, manager, helpermeaningful domain names
Function 500 lines< 20 — split
Boolean flag argsplit into 2 methods
Return nullOptional or throw
Catch generic Exceptionspecific
// TODO without contextTODO with JIRA + deadline
Comment what code doescomment WHY
Mix high + low abstractionsame level per function
Hardcoded magicconstant / enum
Test 1 = test 10 things1 behavior per test

12. Tools

เครื่องมือช่วยบังคับ clean code อัตโนมัติ แบ่งเป็น 3 กลุ่ม — formatter (จัดรูปแบบโค้ดให้เหมือนกันทั้งทีม), linter (จับ pattern ที่ไม่ดี), และ quality platform (วัด code smell / coverage / duplication ในภาพรวม) เสียบไว้ใน CI จะกันโค้ดสกปรกไม่ให้ merge:

bash
# Format
./mvnw spotless:apply              # Java
prettier --write .                  # JS/TS

# Lint
./mvnw checkstyle:check
eslint .

# Quality
SonarQube
CodeClimate
DeepSource

# Refactor
IntelliJ IDEA (built-in)

13. Checkpoint

🛠️ Checkpoint 4.1 — Naming Audit
จาก project ของคุณ — list 10 ชื่อที่ไม่ดี + refactor

🛠️ Checkpoint 4.2 — Function Smell
หา function ในโค้ดที่:

  • 50 lines → split

  • มี boolean flag → split
  • 4 parameter → object

🛠️ Checkpoint 4.3 — Refactor

java
public String f(String s, int t) {
    String r = "";
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (t == 1) r += Character.toUpperCase(c);
        else if (t == 2) r += Character.toLowerCase(c);
        else if (t == 3) {
            if (i % 2 == 0) r += Character.toUpperCase(c);
            else r += Character.toLowerCase(c);
        }
    }
    return r;
}

Refactor — ทุกอย่าง: naming, function, no magic

🛠️ Checkpoint 4.4 — Error Handling Review
หา code ที่:

  • catch + swallow
  • return null
  • generic exception แก้

14. สรุปบท

Naming = สิ่งสำคัญที่สุด — reveal intent, no disinformation, searchable
Functions: small, do one thing, one level of abstraction, few args (0-2)
✅ No boolean flags, no side effects, command-query separation
✅ Return Optional or throw — never null
Comments = explain WHY, not WHAT — code that needs comment usually has bad name
Error handling: exception > error code, specific catch, don't swallow
✅ Wrap external APIs (boundary)
DTO at API/UI boundary, not entity directly
Tests = clean code — F.I.R.S.T., AAA, descriptive name
✅ Code review checklist saves future pain


← บทที่ 3 | บทที่ 5 → Functional Programming