Class Doesn’t Do Much
class ที่ทำน้อยจนไม่คุ้มการมีอยู่ (Lazy Class)
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Class Doesn’t Do Much (หรือชื่อดั้งเดิมในหนังสือของ Fowler ว่า Lazy Class) อธิบาย class ที่ทำหน้าที่น้อยเกินกว่าจะคุ้มกับต้นทุนที่มันสร้างขึ้น — ต้นทุนของการมี class 1 class ไม่ได้มีแค่บรรทัด code แต่รวมถึง file ที่ต้องเปิด ชื่อที่ต้องจำ test ที่ต้องเขียน และ indirection ที่คนอ่าน code ต้องกระโดดตามเพื่อทำความเข้าใจ ระบบทีมงานของ DevIQ สรุปสั้น ๆ ว่า ถ้า class หนึ่งไม่ได้ทำอะไรมากพอที่จะ “จ่ายค่าเช่า” ความซับซ้อนที่มันเพิ่มเข้ามา ก็ควรถูกยุบทิ้ง
กลิ่นนี้มักเกิดจากสองเส้นทาง:
- หดตัวจากการ refactor — ครั้ง1 class นี้เคยมีความรับผิดชอบเต็ม แต่เมื่อเวลาผ่านไป logic ถูกดึงออกไปอยู่ที่อื่นทีละนิด (Extract Method/Class ไปเรื่อย ๆ) จนเหลือแต่เปลือกที่ห่อ field เดียวหรือ method เดียว
- Speculative Generality — สร้าง class ขึ้นมาล่วงหน้าเพื่อรองรับอนาคตที่ยังไม่มาถึง เช่น “เผื่อวันหนึ่งจะมีหลาย strategy” แต่จนถึงวันนี้มี implementation เดียว
สิ่งสำคัญคือต้องแยกให้ออกจาก value object, DTO, Null Object หรือ marker type ที่ตั้งใจให้เล็ก — class เหล่านั้นเล็กแต่ “ทำงาน” อยู่จริง เช่น บังคับ invariant ป้องกันค่าที่ผิดกฎ หรือให้ type safety ที่ primitive ธรรมดาให้ไม่ได้ Lazy Class ต่างออกไปตรงที่มันไม่ได้ทำอะไรเลยนอกจากส่งต่อ (forward) ค่าหรือการเรียก method
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- class มี field เดียวหรือสองอัน และมี method เดียวที่แค่คืนค่าหรือคำนวณตรง ๆ จาก field นั้น ไม่มี logic อื่น
- class ถูก instantiate และเรียกใช้จากจุดเดียวในทั้งระบบ ไม่มีที่อื่นอ้างอิงถึง
- method ส่วนใหญ่เป็น getter/setter หรือ pass-through ไปยัง object อื่นโดยไม่เพิ่มพฤติกรรมอะไร
- subclass ที่ override method parent เพียงเล็กน้อยหรือไม่ override อะไรเลย — สัญญาณว่า hierarchy นั้นควรถูกยุบ
- เวลาถามทีมว่า “ทำไม class นี้ต้องแยกจาก class ที่เรียกมัน” คำตอบมักเป็น “เผื่ออนาคต” มากกว่าเหตุผลด้าน domain หรือ invariant
- class ถูกสร้างขึ้นเพื่อรองรับ interface ที่วางแผนไว้ (เช่น Strategy ที่มี implementation เดียว) แต่ยังไม่มีเหตุผลจริงให้สลับ implementation
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”Lazy Class ไม่ได้ผิดเพราะทำให้โปรแกรมพัง แต่ผิดเพราะมันเพิ่ม ต้นทุนความเข้าใจ (cognitive overhead) โดยไม่ได้แลกกับคุณค่าอะไรกลับมา:
- การนำทาง code ยากขึ้น — ผู้อ่านต้องกระโดดข้าม file เพื่อตามรอย logic ที่จริง ๆ ควรอยู่รวมกันในที่เดียว
- ความรับผิดชอบไม่ชัดเจน — เพราะ class ไม่มีความรับผิดชอบที่จับต้องได้ จึงขัดกับเจตนารมณ์ของ Single Responsibility Principle ในทางกลับกัน (มี class แต่ไม่มี responsibility ให้รับผิดชอบ)
- ต้นทุนบำรุงรักษาสะสม — test ต้องครอบคลุม class เพิ่ม, การ refactor ต้องแก้หลาย file, การ onboard คนใหม่ต้องอธิบาย class ที่ไม่มีเหตุผลอยู่
- บั่นทอนความไว้วางใจต่อ abstraction — เมื่อทีมเจอ Lazy Class บ่อย ๆ จะเริ่มมองข้าม abstraction ใหม่ ๆ ว่าอาจไม่มีประโยชน์จริง ทำให้การออกแบบในอนาคตถูกตั้งคำถามโดยไม่จำเป็น
จุดสมดุลคือ: การมี class เล็กไม่ใช่ปัญหาในตัวมันเอง ปัญหาคือ class เล็กที่ ไม่ทำอะไรเพื่อแลกกับต้นทุนที่มันสร้าง
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”กรณีที่ 1 — class ห่อ field เดียว แก้ด้วย Inline Class
หัวข้อที่มีชื่อว่า “กรณีที่ 1 — class ห่อ field เดียว แก้ด้วย Inline Class”code ก่อนแก้: DiscountCalculator ถูกสร้างขึ้นมาเพื่อ “แยก concern การคิดส่วนลด” แต่ถูกใช้จากที่เดียวเท่านั้น และไม่มี logic ใดนอกจากการคูณ
public class DiscountCalculator{ private readonly decimal _rate;
public DiscountCalculator(decimal rate) { _rate = rate; }
public decimal Calculate(decimal amount) { return amount * _rate; }}
public class Order{ private readonly DiscountCalculator _discountCalculator; public decimal Total { get; }
public Order(decimal total, decimal discountRate) { Total = total; _discountCalculator = new DiscountCalculator(discountRate); }
public decimal GetDiscountedTotal() { return Total - _discountCalculator.Calculate(Total); }}DiscountCalculator มี field เดียว method เดียว และไม่ถูกอ้างอิงจากที่อื่นเลย — เข้าเกณฑ์ Lazy Class ชัดเจน ใช้ Inline Class ยุบมันกลับเข้า Order:
public class Order{ private readonly decimal _discountRate; public decimal Total { get; }
public Order(decimal total, decimal discountRate) { Total = total; _discountRate = discountRate; }
public decimal GetDiscountedTotal() { return Total - (Total * _discountRate); }}code สั้นลง อ่านง่ายขึ้น และไม่มี indirection ที่ต้องตาม — ถ้าในอนาคตการคิดส่วนลดซับซ้อนขึ้นจริง (เช่นมีหลายกฎ หลาย tier) ค่อย Extract Class กลับออกมาใหม่ตอนนั้นก็ยังทัน
กรณีที่ 2 — subclass ที่ไม่ทำอะไรเพิ่ม แก้ด้วย Collapse Hierarchy
หัวข้อที่มีชื่อว่า “กรณีที่ 2 — subclass ที่ไม่ทำอะไรเพิ่ม แก้ด้วย Collapse Hierarchy”public abstract class Shipment{ public abstract decimal CalculateFee(decimal weightKg);}
public class StandardShipment : Shipment{ public override decimal CalculateFee(decimal weightKg) { return weightKg * 10m; }}ถ้า StandardShipment เป็น subclass เดียวที่มีอยู่ และไม่มีแผนจะเพิ่มชนิดการจัดส่งอื่นในเร็ว ๆ นี้ hierarchy ทั้งชุดก็เป็นภาระโดยไม่จำเป็น ใช้ Collapse Hierarchy รวม2 class เป็น class เดียว:
public class Shipment{ public decimal CalculateFee(decimal weightKg) { return weightKg * 10m; }}เมื่อวันหนึ่งมีชนิดการจัดส่งที่สองจริง ๆ (เช่น ExpressShipment ที่คิดค่าธรรมเนียมต่างออกไป) ค่อยแยก abstraction กลับมาใหม่ตามความจำเป็นจริง ไม่ใช่เผื่อไว้ล่วงหน้า
ข้อควรระวัง — อย่าสับสนกับ class เล็กที่มีเหตุผล
หัวข้อที่มีชื่อว่า “ข้อควรระวัง — อย่าสับสนกับ class เล็กที่มีเหตุผล”// ไม่ใช่ Lazy Class แม้จะมี field เดียว — เพราะบังคับ invariant จริงpublic sealed class Money{ public decimal Amount { get; } public string Currency { get; }
public Money(decimal amount, string currency) { if (amount < 0) throw new ArgumentException("amount ต้องไม่ติดลบ"); Amount = amount; Currency = currency; }}Money เล็กพอ ๆ กับ DiscountCalculator ในตัวอย่างแรก แต่มันทำงานจริง (บังคับกฎ ป้องกันค่าไม่ถูกต้อง ให้ type safety) จึงไม่ใช่ Lazy Class — การตัดสินใจต้องดูที่ “ทำอะไรอยู่” ไม่ใช่แค่ “ขนาดใหญ่แค่ไหน”
flowchart TB
subgraph Before
OrderA[Order] --> Calc[DiscountCalculator]
Calc --> RateA[rate field]
end
subgraph After
OrderB[Order with rate field inline]
end
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Middle Man — class ที่ทำงานได้บ้างแต่ส่วนใหญ่เป็นการส่งต่อ (delegate) มากเกินไป มักปนกับ Lazy Class
- Speculative Generality — สาเหตุอันดับต้น ๆ ที่ทำให้เกิด Lazy Class คือการออกแบบเผื่ออนาคตที่ไม่เคยมาถึง
- Data Class — อีกกลิ่นในกลุ่ม Dispensable ที่ class มีแต่ data ไม่มี behavior
- YAGNI — หลักการที่อธิบายว่าทำไมไม่ควรสร้าง abstraction เผื่อไว้ล่วงหน้า
- KISS — หลักการความเรียบง่ายที่ Lazy Class มักละเมิดโดยไม่ตั้งใจ