โหมดมืด
บทที่ 4 — Clean Code
"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, data1.10 Boolean = is/has/should
java
✅ isActive, hasPermission, shouldRetry, canEdit
❌ active, permission, retry, edit1.11 Constant = SCREAMING_SNAKE
java
✅ MAX_RETRIES = 3
✅ DEFAULT_TIMEOUT_MS = 5000
❌ max_retries
❌ MaxRetries1.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?
- เรียงผิดง่าย — เปลี่ยน
(name, email)เป็น(email, name)compiler ไม่จับ (type เหมือนกัน) — bug หลุดถึง production- อ่านยาก —
createUser("John", "j@x.com", 25, "0812345678", ...)ดูแล้วไม่รู้ว่าตัวไหนคืออะไร- เพิ่ม-ลด field ลำบาก — ทุก caller ต้องแก้
- บอกเป็นนัยว่า 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 บังคับให้ callertry/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
Legal / License
java
// Copyright (c) 2026 MyCompany. Apache 2.0Explanation 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 userOutdated
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(...) { ... }
}4.3 Group Related Things
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 เงียบ ๆ) → earlythrowเป็นการ เปลี่ยน 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 ที่มี methodclose()) — ใส่ไว้ในวงเล็บหลัง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 แต่เป็น "เป้าหมายระหว่างฝึก"):
- Class ห้ามเกิน 100 บรรทัด
- Method ห้ามเกิน 5 บรรทัด
- Method รับ argument ห้ามเกิน 4 ตัว (รวม keyword args ใน Ruby)
- 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, helper | meaningful domain names |
| Function 500 lines | < 20 — split |
| Boolean flag arg | split into 2 methods |
| Return null | Optional or throw |
| Catch generic Exception | specific |
// TODO without context | TODO with JIRA + deadline |
| Comment what code does | comment WHY |
| Mix high + low abstraction | same level per function |
| Hardcoded magic | constant / enum |
| Test 1 = test 10 things | 1 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