Complete Domain Primitive Suite
ภารกิจประจำวัน: วันนี้เราจะสกัด Domain Primitives อีก 4 ตัวที่เหลือ ได้แก่ Money, PostalAddress, ItemCode, และ DiscountCode เพื่อกำจัด Primitive Obsession ให้หมดสิ้น จากนั้นนำแนวทาง New API Approach (Day 12) มาปรับปรุง OrderProcessingService โดยเปิดเมธอดใหม่คู่ขนานไปกับ Legacy Bridge Method ที่ติด @Deprecated เพื่อให้มั่นใจว่าการปรับปรุงโค้ดจะไม่ส่งผลกระทบต่อระบบเดิมที่กำลังรันอยู่!
Money ที่รวมทั้ง Amount และ Currency ป้องกันการบวกเงินข้ามสกุล
Day 15 ได้ทำ Code Audit วิเคราะห์ 4 รอยแตกร้าว และสกัด EmailAddress กับ Quantity เรียบร้อยแล้ว
Day 16 จาก 35 วัน — สกัด Domain Primitives ส่วนที่เหลือให้ครบชุด และเชื่อม Bridge สู่ OrderProcessingService
สร้าง Money, PostalAddress, ItemCode, DiscountCode และล้างแค้น Legacy Code ด้วย New API
การสกัด Domain Primitives ครบชุด (Completing the Suite)
import java.util.Currency; import java.util.Objects; public final class Money { private final double amount; private final Currency currency; public Money(double amount, Currency currency) { // Step 1 & 2: Origin & Bound Validation (ห้ามติดลบ และห้ามไร้สกุลเงิน) if (amount < 0) { throw new IllegalArgumentException("Amount cannot be negative. Provided: " + amount); } if (currency == null) { throw new IllegalArgumentException("Currency cannot be null"); } this.amount = amount; this.currency = currency; } // Domain Operation: คูณจำนวนสินค้าอย่างปลอดภัย โดยรักษา Currency เดิมเสมอ public Money multiply(int quantity) { if (quantity <= 0) { throw new IllegalArgumentException("Quantity for multiplication must be positive"); } return new Money(this.amount * quantity, this.currency); } public double amount() { return amount; } public Currency currency() { return currency; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Money money = (Money) o; return Double.compare(money.amount, amount) == 0 && Objects.equals(currency, money.currency); } @Override public int hashCode() { return Objects.hash(amount, currency); } @Override public String toString() { return String.format("%.2f %s", amount, currency.getCurrencyCode()); } }
public final class ItemCode { private static final String ITEM_CODE_REGEX = "^ITEM-[A-Z0-9]{5,10}$"; private final String value; public ItemCode(String rawCode) { if (rawCode == null || rawCode.isBlank()) { throw new IllegalArgumentException("Item code cannot be null or blank"); } String trimmed = rawCode.trim().toUpperCase(); if (!trimmed.matches(ITEM_CODE_REGEX)) { throw new IllegalArgumentException("Invalid Item Code format: " + rawCode); } this.value = trimmed; } public String value() { return value; } @Override public String toString() { return value; } }
public final class DiscountCode { private static final String DISCOUNT_REGEX = "^[A-Z0-9]{4,8}$"; private final String code; public DiscountCode(String rawCode) { // Safe Default: อนุญาตให้เป็น null/blank ได้โดยจะแทนค่าด้วย "NONE" if (rawCode == null || rawCode.isBlank()) { this.code = "NONE"; return; } String trimmed = rawCode.trim().toUpperCase(); if (!trimmed.matches(DISCOUNT_REGEX)) { throw new IllegalArgumentException("Invalid Discount Code syntax"); } this.code = trimmed; } public String code() { return code; } @Override public String toString() { return code; } }
public final class PostalAddress { private final String addressLine; public PostalAddress(String rawAddress) { if (rawAddress == null || rawAddress.isBlank()) { throw new IllegalArgumentException("Shipping address cannot be null or blank"); } String trimmed = rawAddress.trim(); if (trimmed.length() < 5 || trimmed.length() > 250) { throw new IllegalArgumentException("Address length must be between 5 and 250 characters"); } this.addressLine = trimmed; } public String value() { return addressLine; } @Override public String toString() { return addressLine; } }
import java.util.Currency; public class OrderProcessingService { // ✅ New API Approach: ใช้ Domain Primitives ครบทั้งชุด การันตี Compile-Time Safety 100%! // ไม่สามารถส่ง String สลับตำแหน่ง หรือส่ง Quantity/Money ติดลบเข้ามาได้อีกต่อไป public void processOrder(EmailAddress customerEmail, PostalAddress shippingAddress, ItemCode itemCode, Quantity quantity, Money unitPrice, DiscountCode discountCode) { // คำนวณราคารวมผ่าน Domain Primitives ปราศจากการใช้ Primitive Ops ติดลบ Money totalAmount = unitPrice.multiply(quantity.value()); System.out.println("Processing secure order for: " + customerEmail.value() + " Item: " + itemCode.value() + " Total: " + totalAmount); } // 🔄 Legacy Bridge Method (Deprecated): ใช้สำหรับรองรับผู้เรียกใช้เก่า แล้วแปลงเข้า Domain Primitives ทันที // หากผู้เรียกเก่าส่ง quantity = -5 หรือ email ผิดรูปแบบ Constructor ของ Domain Primitive จะ Fail-Fast ทันที! @Deprecated public void processOrder(String customerEmail, String shippingAddress, String itemCode, int quantity, double unitPrice, String discountCode) { processOrder( new EmailAddress(customerEmail), new PostalAddress(shippingAddress), new ItemCode(itemCode), new Quantity(quantity), new Money(unitPrice, Currency.getInstance("USD")), new DiscountCode(discountCode) ); } }
จุดเปลี่ยนสำคัญทางสถาปัตยกรรม (Architectural Trophies)
ไม่มีพารามิเตอร์ประเภท String ติดกันอีกต่อไป คอมไพเลอร์บังคับประเภทข้อมูลอย่างเข้มงวด หากสลับที่อยู่กับอีเมลจะ Compile Error ทันที
ทั้ง Quantity และ Money ล็อก Invariants ห้ามค่าติดลบและศูนย์ ทำให้ผลการคำนวณราคารวมไม่มีวันติดลบได้อีก
เมธอดชั้นในตัด if (email == null) ทิ้งได้ 100% เพราะ Domain Primitives รับประกันความสมบูรณ์ตั้งแต่วินาทีที่สร้าง
ใช้ New API Approach ควบคู่กับ @Deprecated ทำให้ระบบเดิมทำงานต่อเนื่องได้โดยไร้ Downtime พร้อมค่อยๆ ย้ายโค้ดอย่างปลอดภัย
ชุดควิซประเมินความรู้สะสมประจำวัน (Day 16 Cumulative Quiz - 10 ข้อ)
30% ทบทวนความรู้ Phase 1 (Days 2, 6, 9, 12) + 70% เจาะลึกการ Refactoring Day 16 ตอบปุ๊บ รู้ผลเฉลยทันทีพร้อมคำอธิบาย
ทดสอบความรู้ Day 16 สะสมครบ 10 ข้อสำเร็จแล้ว!
สรุปปิดท้าย Day 16 — Kata 1: Primitive Obsession Breakdown (Part 2)
ในวันนี้เราได้สกัด Domain Primitives ครบทั้งชุด (Money, PostalAddress, ItemCode, DiscountCode) และนำแนวทาง New API Approach มาปรับปรุง OrderProcessingService โดยเชื่อม Legacy Bridge Method ไว้ ทำให้ระบบเก่าทำงานได้อย่างปลอดภัย ไร้ Parameter Swapping และป้องกันค่าติดลบได้อย่างถอนรากถอนโคน! ควิซทั้ง 10 ข้อได้เชื่อมโยงทฤษฎีจาก Phase 1 (Day 2 Anti-Hamlet, Day 6 Partial Primitives, Day 9 Fail-Secure, Day 12 New API) เข้ากับโค้ดจริงได้อย่างสมบูรณ์แบบ