ข้าม​ไป​ยัง​เนื้อหา

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 — ทำทุกอย่างเกี่ยวกับ Order
public 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 จริง​หรือ​แตะ​ฐาน​ข้อมูล​จริง

เทคนิค​หลัก​คือ Extract Class: แยก​ความ​รับผิดชอบ​แต่ละ​กลุ่ม​ออก​เป็น class ของ​ตัวเอง ให้​แต่ละ class มี​เหตุผล​ให้​เปลี่ยนแปลง​เพียง​เหตุผล​เดียว แล้ว​ให้ class ระดับ​บน​ทำ​หน้าที่​แค่ orchestration (ประสาน​งาน) ไม่ใช่​ทำ​เอง​ทั้งหมด ขั้นตอน​ที่​ใช้ได้​จริง​ใน code ที่​มี​อยู่​แล้ว:

  1. เขียน test ครอบ behavior ปัจจุบัน​ของ Blob ไว้​ก่อน (characterization test) เพื่อ​ให้​มี safety net ระหว่าง refactor
  2. ระบุ​กลุ่ม method/field ที่​ใช้​ข้อมูล​ร่วม​กัน​และ​เปลี่ยนแปลง​ด้วย​เหตุผล​เดียวกัน — นั่น​คือ​ผู้​สมัคร Extract Class แต่ละ​กลุ่ม
  3. ย้าย​กลุ่ม​เหล่า​นั้น​ออก​เป็น class ใหม่​ที​ละ​กลุ่ม รัน test หลัง​ย้าย​ทุก​ครั้ง
  4. ให้ class เดิม​เก็บ​ไว้​แค่ dependency ของ class ใหม่ (ผ่าน Dependency Inversion) แล้ว​เปลี่ยน​บทบาท​ตัวเอง​เป็น​ตัว​ประสาน​งาน​เท่านั้น
  5. ถ้า​มี​หลาย 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