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

Regions

ใช้ #region ซ่อน code แทนที่​จะ​แก้​ปัญหา​ที่ code โต​เกิน​ไป

Regions เกิด​เมื่อ​ใช้ directive พับ code (เช่น #region#endregion ใน C#, หรือ feature code folding แบบ​เดียวกัน​ใน​ภาษา​อื่น) เพื่อ​จัด​ระเบียบ​หรือ ซ่อน code ภายใน file แทน​การ​แก้​ปัญหา​ที่แท้​จริง​คือ type หรือ method ที่​โต​เกิน​ไป มัน​แค่​กลบ​ปัญหา​ด้วย​กลไก​พับ​เชิง​เครื่องสำอาง ไม่​ได้​ทำให้ code เล็ก​ลง​หรือ​เข้าใจ​ง่าย​ขึ้น​แม้แต่​บรรทัด​เดียว

ลอง​นึก​ภาพ class หนึ่ง​ยาว 1,200 บรรทัด ถูก​หั่น​ด้วย #region Properties, #region Constructors, #region Public Methods, #region Private Helpers, #region Event Handlers — เมื่อ​พับ region ทั้งหมด หน้า​จอ​จะ​ดู​สะอาด​เรียบร้อย เหมือน class มี​ระเบียบ​ดี แต่​นั่น​คือ​ภาพลวงตา class ยัง​คง​มี 1,200 บรรทัด ยัง​คง​ละเมิด Single Responsibility Principle เหมือน​เดิม​ทุก​ประการ region เพียง​แค่​เปลี่ยน “ความ​ยุ่งเหยิง​ที่​มอง​เห็น” ให้​กลาย​เป็น “ความ​ยุ่งเหยิง​ที่​ซ่อน​อยู่​หลัง​ลูกศร​พับ”

Erik Dietrich เปรียบเทียบ region ว่า​เป็น​เหมือน “น้ำหอม​ดับ​กลิ่น code” (code deodorant) — มัน​บอก​ทีม​เป็น​นัยว่า “ใส่ region ของ​คุณ​ไว้​ก่อน ฉัน​จะ​ได้​ไม่​ต้อง​เห็น method ที่​บวม​เป่ง การ​จัด​วาง​ที่มั่วซั่ว และ class ยักษ์​ที่​คุณ​เขียน​ออก​มา” กล่าว​คือ region เป็น​เครื่องมือ​ที่ ให้​รางวัล​กับ​นิสัย​การ​ออกแบบ​ที่​แย่ แทนที่​จะ​ป้องกัน​มัน

  • file class มี #region มากกว่า​สอง​สาม​อัน​เพื่อ​แบ่ง​กลุ่ม field, property, constructor, method อย่าง​จงใจ
  • เมื่อ​พับ region ทั้งหมด​แล้ว จำนวน​บรรทัด​ของ class ยัง​ยาว​เกิน 300–500 บรรทัด (สัญญาณ​ของ Large Class)
  • มี region ซ้อน​อยู่ ภายใน method เดียว เช่น #region Validation, #region Save, #region Notify — นี่​คือ Long Method ที่​พราง​ตัว​อยู่
  • ชื่อ region เป็น​คำ​กว้าง ๆ อย่าง “Helpers”, “Misc”, “Old Code” ซึ่ง​ซ้ำ​กับ​ปัญหา Poor Names และ Dead Code ที่​แฝง​อยู่​ข้าง​ใน
  • code รีวิว (code review) ผ่าน​ไป​อย่าง​รวดเร็ว​เพราะ​ผู้​รีวิว​พับ region เก่า​ทิ้ง​ไป ไม่​ได้​อ่าน​ของ​เดิม​ซ้ำ ทำให้​กลิ่น​เดิม​สะสม​ต่อ​ไป​เรื่อย ๆ โดย​ไม่มี​ใคร​ทักท้วง
  1. ซ่อน​ปัญหา​แทนที่​จะ​แก้ — class หรือ method ที่​โต​เกิน​ไป​คือ​สัญญาณ​ว่า​มี​มากกว่า​หนึ่ง​ความ​รับผิดชอบ​ซ่อน​อยู่ การ​พับ​มัน​ให้​ดู​สั้น​ลง​บน​หน้า​จอ​ไม่​ได้​ลด​ความ​ซับซ้อน​ที่แท้​จริง​เลย เมื่อใด​ที่​ต้อง​แก้ bug จริง นัก​พัฒนา​ก็​ยัง​ต้อง​กาง​ทุก region ออก​มา​อ่าน​อยู่ดี
  2. บั่นทอน​แรง​กดดัน​ให้ refactor — ถ้า​ไม่มี region class ยาว ๆ จะ​ทำให้​ทุก​คน​รู้สึก​อึดอัด​ทันที​ที่​เปิด file ซึ่ง​เป็น​แรง​ผลักดัน​ธรรมชาติ​ให้​แตก class ออก แต่ region ลบ​สัญญาณ​เตือน​ภัย​นั้น​ทิ้ง​ไป ทีม​จึง​ปล่อย​ให้ class บวม​ต่อ​ไป​เรื่อย ๆ อย่าง​สบายใจ
  3. บิดเบือน​การนำทาง​และ​การ​ค้นหา — เครื่องมือ​ค้นหา code, blame, และ diff ยัง​คง​ทำงาน​กับ code ทั้งหมด​เหมือน​เดิม แต่​มนุษย์​ที่​ต้อง scroll ผ่าน region ที่​พับ/กาง​สลับ​กัน​จะ​สูญเสีย mental model ของ​โครงสร้าง file ยิ่ง file มี region ซ้อน​กัน​หลาย​ชั้น ยิ่ง​งง​ว่า​ตอน​นี้​กำลัง​อยู่​ใน scope ไหน
  4. ผูก​ความ​เข้าใจ​ไว้​กับ​เครื่องมือ​เดียว — region เป็น feature ของ IDE ไม่ใช่​ของ​ภาษา เมื่อ code ถูก​อ่าน​ผ่าน diff บน GitHub, terminal, หรือ editor อื่น​ที่​ไม่​รองรับ​การ​พับ region จะ​กลาย​เป็น​เพียง​คอมเมนต์​รก​ไร้​ประโยชน์​ที่​ทำให้ file ยาว​ขึ้น​โดย​ไม่​ให้​คุณค่า​อะไร​เพิ่ม
  5. เพิ่ม​ต้นทุน​การ​ดูแล — ทุก​ครั้ง​ที่​ย้าย method ข้าม region ต้อง​คอย​จำ​จะ​ปิด/เปิด tag ให้​ครบ ยิ่ง merge conflict ก็​ยิ่ง​เจอ noise จาก #region/#endregion ที่​ขยับ​ตำแหน่ง​กัน ทั้ง​ที่​ไม่​ได้​เกี่ยว​กับ logic จริง

โดย​สรุป กลิ่น​นี้​ไม่​ได้​อันตราย​เพราะ syntax ของ​มัน​เอง แต่​อันตราย​เพราะ​มัน​เป็น สัญญาณ​เตือน​ภัย​ที่​ถูก​ปิด​เสียง — class ควร​จะ​รู้สึก “อึดอัด” เมื่อ​โต​เกิน​ไป แต่ region ทำให้​มัน​ดู​สงบ​เรียบร้อย​ตลอด​เวลา

public class OrderProcessor
{
#region Fields
private readonly IOrderRepository _repository;
private readonly IEmailSender _emailSender;
private readonly IInventoryService _inventory;
#endregion
#region Constructor
public OrderProcessor(IOrderRepository repository, IEmailSender emailSender, IInventoryService inventory)
{
_repository = repository;
_emailSender = emailSender;
_inventory = inventory;
}
#endregion
#region Validation
private bool ValidateOrder(Order order)
{
if (order.Items.Count == 0) return false;
if (order.CustomerEmail == null) return false;
// ตรวจสอบเงื่อนไขอีกยาวเหยียด...
return true;
}
#endregion
#region Pricing
private decimal CalculateTotal(Order order)
{
decimal total = 0;
foreach (var item in order.Items)
{
total += item.Price * item.Quantity;
}
// คำนวณส่วนลด, ภาษี, ค่าส่งอีกหลายสิบบรรทัด...
return total;
}
#endregion
#region Persistence and Notification
public void Process(Order order)
{
if (!ValidateOrder(order)) throw new InvalidOperationException("คำสั่งซื้อไม่ถูกต้อง");
order.Total = CalculateTotal(order);
_inventory.Reserve(order.Items);
_repository.Save(order);
_emailSender.SendConfirmation(order.CustomerEmail, order);
}
#endregion
}

ปัญหา​คือ OrderProcessor แบก​ความ​รับผิดชอบ​สี่​อย่าง​พร้อม​กัน — validation, pricing, การ​จอง​สต็อก, และ​การ​แจ้ง​เตือน — region เพียง​จัด​กลุ่ม​ความ​ยุ่งเหยิง​ให้​ดู​เป็น​ระเบียบ แต่ class ยัง​คง​ต้อง​เปลี่ยน​ทุก​ครั้ง​ที่​กฎ​การ​คิด​ราคา​เปลี่ยน หรือ​ทุก​ครั้ง​ที่​รูปแบบ​อีเมล​เปลี่ยน (Divergent Change)

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

public class OrderValidator
{
public bool Validate(Order order)
{
if (order.Items.Count == 0) return false;
if (order.CustomerEmail == null) return false;
return true;
}
}
public class OrderPricingCalculator
{
public decimal CalculateTotal(Order order)
{
decimal total = 0;
foreach (var item in order.Items)
{
total += item.Price * item.Quantity;
}
return total;
}
}
public class OrderProcessor
{
private readonly IOrderRepository _repository;
private readonly IEmailSender _emailSender;
private readonly IInventoryService _inventory;
private readonly OrderValidator _validator;
private readonly OrderPricingCalculator _pricingCalculator;
public OrderProcessor(
IOrderRepository repository,
IEmailSender emailSender,
IInventoryService inventory,
OrderValidator validator,
OrderPricingCalculator pricingCalculator)
{
_repository = repository;
_emailSender = emailSender;
_inventory = inventory;
_validator = validator;
_pricingCalculator = pricingCalculator;
}
public void Process(Order order)
{
if (!_validator.Validate(order))
throw new InvalidOperationException("คำสั่งซื้อไม่ถูกต้อง");
order.Total = _pricingCalculator.CalculateTotal(order);
_inventory.Reserve(order.Items);
_repository.Save(order);
_emailSender.SendConfirmation(order.CustomerEmail, order);
}
}

ตอน​นี้​แต่ละ class สั้น อ่าน​จบ​ใน​หน้า​จอ​เดียว ไม่​ต้อง​มี region เลย​เพราะ​ไม่มี​อะไร​ให้​ซ่อน OrderProcessor เหลือ​หน้าที่​เดียว​คือ “ประสาน” (orchestrate) ขั้นตอน​ของ order ส่วน​กฎ validation และ​การ​คิด​ราคา​ย้าย​ไป​อยู่​ใน​ที่​ของ​มัน​เอง ทดสอบ​แยก​หน่วย​ได้​ง่าย​ขึ้น และ​เมื่อ​กฎ​ภาษี​เปลี่ยน ก็​แก้​แค่ OrderPricingCalculator โดย​ไม่​กระทบ​ส่วน​อื่น

flowchart LR
    A[OrderProcessor เดิม พร้อม region หลายอัน] --> B{Extract Class}
    B --> C[OrderValidator]
    B --> D[OrderPricingCalculator]
    B --> E[OrderProcessor ที่เหลือ orchestrate อย่างเดียว]

กฎ​ง่าย ๆ ที่​ใช้​ตัดสิน​ใจ: ถ้า​คุณ​อยาก​ใช้ #region เพื่อ “ซ่อน” กลุ่ม code ใน class เดียว ให้​ถาม​ตัวเอง​ว่า​กลุ่ม​นั้น​ควร​เป็น class แยก​ต่างหาก หรือ​ไม่ ถ้า​ใช่ ให้ refactor ออก​ไป​เลย​แทนที่​จะ​พับ​มัน​ไว้