The Blob
class ที่ดูดกลืนทุกอย่างจนโตไร้ขอบเขต (God Object)
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”The Blob ได้ชื่อมาจากสัตว์ประหลาดในหนังไซไฟปี 1958 เรื่อง The Blob — สิ่งมีชีวิตที่ดูดกลืนทุกอย่างที่สัมผัสแล้วโตขึ้นเรื่อย ๆ ไม่มีขอบเขต ในซอฟต์แวร์ Blob คือ class 1 class (มักตั้งชื่อเชิงจัดการอย่าง Manager, Processor, System, Engine) ที่ค่อย ๆ ดูดซับความรับผิดชอบของทั้งระบบเข้ามาไว้กับตัวเอง จนกลายเป็น controller ตัวเดียวที่ซับซ้อนมาก ล้อมรอบด้วย class ข้อมูลเปล่า ๆ ที่แทบไม่มีพฤติกรรมอะไรเลย
รูปแบบทั่วไปของ Blob คือ diagram class ที่มี class หนึ่งเชื่อมโยง (dependency) ไปหา class อื่นแทบทุก class ในระบบ ในขณะที่ class รอบข้างเป็นเพียง data holder หรือ struct-like class ที่มีแต่ property ไม่มี logic method และแอตทริบิวต์ของ Blob ไม่มีความสัมพันธ์กันเชิงความหมาย (lack of cohesion) — มันไม่ได้ทำ “หนึ่งเรื่อง” แต่ทำทุกเรื่องที่นักพัฒนานึกออกในตอนนั้น
ชื่ออื่นที่ใช้เรียกปัญหาเดียวกันนี้ ได้แก่ God Object, God Class, Large Class (ชื่อ code smell ใน refactoring.guru) และบางครั้งเรียกว่า Blob Class
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”Blob ไม่ได้เกิดจากการออกแบบผิดตั้งแต่ต้น — มันคือผลสะสมของการตัดสินใจที่ “สมเหตุสมผลในระยะสั้น” ซ้ำ ๆ กัน:
- ง่ายในทันที เมื่อมี feature ใหม่ การเพิ่ม method เข้าไปใน class ที่มีอยู่แล้วใช้ความคิดน้อยกว่าการนั่งออกแบบ class ใหม่ ตั้งชื่อ กำหนดขอบเขตความรับผิดชอบ แล้วเดินสาย dependency ให้ถูกต้อง
- ทุกอย่างอยู่ที่เดียว นักพัฒนาที่เพิ่งเข้า project รู้สึกว่า debug ง่ายในช่วงแรก เพราะไม่ต้องกระโดดข้ามหลาย file เพื่อเข้าใจ flow — ทั้งหมดอยู่ใน method เดียวหรือ class เดียว
- ไม่มีแรงต้าน (friction) ให้หยุดคิด เมื่อ orchestration logic ทั้งหมดกระจุกอยู่ที่จุดเดียว มันดึงดูดให้ code ใหม่ ๆ วิ่งเข้ามารวมที่จุดนั้นต่อไปเรื่อย ๆ (คล้ายแรงโน้มถ่วง) เพราะทุกอย่างที่จำเป็นก็อยู่ตรงนั้นแล้ว
- ประหยัดเวลาระยะสั้นจริง โดยเฉพาะในทีมเล็กหรือ prototype ที่ยังไม่แน่ใจขอบเขต domain การรวมทุกอย่างไว้ที่เดียวอาจดูเหมือนเลี่ยง over-engineering ได้
ปัญหาคือ Blob แทบไม่เคย “หยุดโต” ด้วยตัวเอง เพราะไม่มีแรงผลักดันตามธรรมชาติให้แยกมันออก — ต้องอาศัยวินัยของทีมเท่านั้น
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”Blob ละเมิดหลักการออกแบบพื้นฐานหลายข้อพร้อมกัน:
- ละเมิด Single Responsibility Principle — class มี “เหตุผลให้เปลี่ยนแปลง” มากกว่าหนึ่งเหตุผลเสมอ เปลี่ยน business rule ก็ต้องแก้ที่นี่ เปลี่ยนวิธี persist ข้อมูลก็ต้องแก้ที่นี่ เปลี่ยน format การแจ้งเตือนก็ต้องแก้ที่นี่
- ละเมิด Open-Closed Principle — เพราะทุก feature ใหม่ต้องแก้ไข class เดิม (modify) แทนที่จะ extend มันจากภายนอก ความเสี่ยงที่จะทำของเดิมพังจึงสูงขึ้นทุกครั้งที่แตะ code
- cohesion ต่ำ, coupling สูง — method ใน class ไม่เกี่ยวข้องกันเชิงความหมาย แต่ class อื่นทั้งระบบกลับพึ่งพา (depend on) Blob โดยตรง ผลคือการแก้ไขจุดเล็ก ๆ จุดหนึ่งอาจสร้าง ripple effect ไปกระทบ function ที่ดูไม่เกี่ยวข้องกันเลย
- ทดสอบยาก unit test ต้อง mock dependency จำนวนมากเพื่อทดสอบแค่ behavior เดียว และ test ที่ครอบคลุม Blob ทั้งก้อนมักจะเปราะ (fragile) เพราะแตะ concern หลายอย่างพร้อมกัน
- เป็นจุดคอขวดของทีม เมื่อทุก feature ต้องแก้ file เดียวกัน merge conflict เกิดถี่ และ ownership ของ code ไม่ชัดเจนว่าใครดูแลส่วนไหน
- นำไปสู่ Big Ball of Mud — เมื่อ Blob เกิดขึ้นหลายตัวและพันกันเองในระบบ โครงสร้างทั้งหมดจะเสื่อมสภาพจนไม่มี architecture ที่จำแนกได้อีกต่อไป
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างนี้แสดง OrderManager ที่ค่อย ๆ ดูดซับทุกความรับผิดชอบของ flow การสั่งซื้อ ทั้ง validation, การคิดราคา, persistence, การเรียก payment gateway, การส่งอีเมล และ logging — ทั้งหมดอยู่ใน method เดียว
// ANTIPATTERN: OrderManager คือ Blob — ทำทุกอย่างเกี่ยวกับ Orderpublic class OrderManager{ private readonly SqlConnection _connection; private readonly SmtpClient _smtpClient;
public OrderManager(string connectionString) { _connection = new SqlConnection(connectionString); _smtpClient = new SmtpClient("smtp.example.com"); }
public void ProcessOrder(int customerId, List<OrderLine> lines, string cardNumber) { // 1. ตรวจสอบความถูกต้อง if (lines == null || lines.Count == 0) throw new InvalidOperationException("ไม่มีรายการสินค้า");
// 2. คำนวณราคารวมและภาษี decimal total = 0; foreach (var line in lines) { total += line.UnitPrice * line.Quantity; } decimal tax = total * 0.07m; total += tax;
// 3. เรียก payment gateway โดยตรง var paymentClient = new HttpClient(); var response = paymentClient.PostAsync( "https://payments.example.com/charge", new StringContent($"card={cardNumber}&amount={total}") ).Result;
if (!response.IsSuccessStatusCode) throw new Exception("ชำระเงินไม่สำเร็จ");
// 4. เขียนลงฐานข้อมูลด้วย SQL ตรง ๆ _connection.Open(); var cmd = new SqlCommand( $"INSERT INTO Orders (CustomerId, Total) VALUES ({customerId}, {total})", _connection); cmd.ExecuteNonQuery(); _connection.Close();
// 5. ส่งอีเมลยืนยัน _smtpClient.Send("orders@example.com", "customer@example.com", "ยืนยันคำสั่งซื้อ", $"ยอดรวมของคุณคือ {total:C}");
// 6. เขียน log ลง file File.AppendAllText("orders.log", $"{DateTime.Now}: Order for customer {customerId} = {total}\n"); }}OrderManager รู้จักทั้ง SQL, HTTP client ของ payment gateway, SMTP, และ file system — เปลี่ยน payment provider ก็ต้องแก้ file นี้ เปลี่ยนวิธี log ก็ต้องแก้ file นี้ และแทบเป็นไปไม่ได้ที่จะ unit test การคำนวณราคาโดยไม่ยิง HTTP request จริงหรือแตะฐานข้อมูลจริง
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”เทคนิคหลักคือ Extract Class: แยกความรับผิดชอบแต่ละกลุ่มออกเป็น class ของตัวเอง ให้แต่ละ class มีเหตุผลให้เปลี่ยนแปลงเพียงเหตุผลเดียว แล้วให้ class ระดับบนทำหน้าที่แค่ orchestration (ประสานงาน) ไม่ใช่ทำเองทั้งหมด ขั้นตอนที่ใช้ได้จริงใน code ที่มีอยู่แล้ว:
- เขียน test ครอบ behavior ปัจจุบันของ Blob ไว้ก่อน (characterization test) เพื่อให้มี safety net ระหว่าง refactor
- ระบุกลุ่ม method/field ที่ใช้ข้อมูลร่วมกันและเปลี่ยนแปลงด้วยเหตุผลเดียวกัน — นั่นคือผู้สมัคร Extract Class แต่ละกลุ่ม
- ย้ายกลุ่มเหล่านั้นออกเป็น class ใหม่ทีละกลุ่ม รัน test หลังย้ายทุกครั้ง
- ให้ class เดิมเก็บไว้แค่ dependency ของ class ใหม่ (ผ่าน Dependency Inversion) แล้วเปลี่ยนบทบาทตัวเองเป็นตัวประสานงานเท่านั้น
- ถ้ามีหลาย consumer ที่ต้องการ subset ของความสามารถต่างกัน ให้พิจารณาแยก interface ด้วย (Interface Segregation)
// หลัง refactor: แยกความรับผิดชอบออกเป็น class เฉพาะทาง
public interface IOrderRepository{ void Save(Order order);}
public interface IPaymentGateway{ bool Charge(string cardNumber, decimal amount);}
public interface IOrderNotifier{ void SendConfirmation(Order order);}
public class OrderCalculator{ private const decimal TaxRate = 0.07m;
public decimal CalculateTotal(List<OrderLine> lines) { if (lines == null || lines.Count == 0) throw new InvalidOperationException("ไม่มีรายการสินค้า");
decimal subtotal = lines.Sum(line => line.UnitPrice * line.Quantity); return subtotal + subtotal * TaxRate; }}
// OrderService เหลือหน้าที่แค่ประสานงาน (orchestration) เท่านั้นpublic class OrderService{ private readonly OrderCalculator _calculator; private readonly IPaymentGateway _paymentGateway; private readonly IOrderRepository _repository; private readonly IOrderNotifier _notifier;
public OrderService( OrderCalculator calculator, IPaymentGateway paymentGateway, IOrderRepository repository, IOrderNotifier notifier) { _calculator = calculator; _paymentGateway = paymentGateway; _repository = repository; _notifier = notifier; }
public void ProcessOrder(int customerId, List<OrderLine> lines, string cardNumber) { decimal total = _calculator.CalculateTotal(lines);
if (!_paymentGateway.Charge(cardNumber, total)) throw new PaymentFailedException();
var order = new Order(customerId, total); _repository.Save(order); _notifier.SendConfirmation(order); }}ตอนนี้แต่ละ class ทดสอบแยกกันได้ง่าย (OrderCalculator ไม่ต้อง mock อะไรเลย), เปลี่ยน payment provider แค่ implement IPaymentGateway ใหม่โดยไม่แตะ OrderService, และ log/notification เป็นเรื่องของ IOrderNotifier ที่แยกอิสระออกไปเลย
แผนภาพด้านล่างแสดงรูปทรงของปัญหา — OrderManager เดิมชี้ (depend) ไปยังทุก class ในระบบโดยตรง:
classDiagram class OrderManager class Order class Customer class Database class PaymentGateway class EmailService class Logger OrderManager --> Order OrderManager --> Customer OrderManager --> Database OrderManager --> PaymentGateway OrderManager --> EmailService OrderManager --> Logger
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Single Responsibility Principle — หลักการที่ Blob ละเมิดโดยตรง
- Open-Closed Principle — Blob บังคับให้ต้องแก้ไข class เดิมทุกครั้งที่มี feature ใหม่
- Interface Segregation Principle — เครื่องมือช่วยแยก interface ของ class ที่แตกออกมา
- Big Ball of Mud — สิ่งที่เกิดขึ้นเมื่อ Blob กระจายไปทั่วทั้งระบบ
- Feature Envy — code smell ที่มักพบคู่กับ Blob เมื่อ class อื่นต้องเอื้อมมาดึงข้อมูลจาก Blob ตลอดเวลา
- Refactoring — แนวปฏิบัติที่ใช้แก้ Blob ด้วยเทคนิค Extract Class