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 เก่าทิ้งไป ไม่ได้อ่านของเดิมซ้ำ ทำให้กลิ่นเดิมสะสมต่อไปเรื่อย ๆ โดยไม่มีใครทักท้วง
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”- ซ่อนปัญหาแทนที่จะแก้ — class หรือ method ที่โตเกินไปคือสัญญาณว่ามีมากกว่าหนึ่งความรับผิดชอบซ่อนอยู่ การพับมันให้ดูสั้นลงบนหน้าจอไม่ได้ลดความซับซ้อนที่แท้จริงเลย เมื่อใดที่ต้องแก้ bug จริง นักพัฒนาก็ยังต้องกางทุก region ออกมาอ่านอยู่ดี
- บั่นทอนแรงกดดันให้ refactor — ถ้าไม่มี region class ยาว ๆ จะทำให้ทุกคนรู้สึกอึดอัดทันทีที่เปิด file ซึ่งเป็นแรงผลักดันธรรมชาติให้แตก class ออก แต่ region ลบสัญญาณเตือนภัยนั้นทิ้งไป ทีมจึงปล่อยให้ class บวมต่อไปเรื่อย ๆ อย่างสบายใจ
- บิดเบือนการนำทางและการค้นหา — เครื่องมือค้นหา code, blame, และ diff ยังคงทำงานกับ code ทั้งหมดเหมือนเดิม แต่มนุษย์ที่ต้อง scroll ผ่าน region ที่พับ/กางสลับกันจะสูญเสีย mental model ของโครงสร้าง file ยิ่ง file มี region ซ้อนกันหลายชั้น ยิ่งงงว่าตอนนี้กำลังอยู่ใน scope ไหน
- ผูกความเข้าใจไว้กับเครื่องมือเดียว — region เป็น feature ของ IDE ไม่ใช่ของภาษา เมื่อ code ถูกอ่านผ่าน diff บน GitHub, terminal, หรือ editor อื่นที่ไม่รองรับการพับ region จะกลายเป็นเพียงคอมเมนต์รกไร้ประโยชน์ที่ทำให้ file ยาวขึ้นโดยไม่ให้คุณค่าอะไรเพิ่ม
- เพิ่มต้นทุนการดูแล — ทุกครั้งที่ย้าย method ข้าม region ต้องคอยจำจะปิด/เปิด tag ให้ครบ ยิ่ง merge conflict ก็ยิ่งเจอ noise จาก
#region/#endregionที่ขยับตำแหน่งกัน ทั้งที่ไม่ได้เกี่ยวกับ logic จริง
โดยสรุป กลิ่นนี้ไม่ได้อันตรายเพราะ syntax ของมันเอง แต่อันตรายเพราะมันเป็น สัญญาณเตือนภัยที่ถูกปิดเสียง — class ควรจะรู้สึก “อึดอัด” เมื่อโตเกินไป แต่ region ทำให้มันดูสงบเรียบร้อยตลอดเวลา
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ก่อน: ใช้ region ซ่อน class ที่ทำหลายหน้าที่เกินไป
หัวข้อที่มีชื่อว่า “ก่อน: ใช้ region ซ่อน class ที่ทำหลายหน้าที่เกินไป”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 ตาม SRP แทนการพับด้วย region
หัวข้อที่มีชื่อว่า “หลัง: Extract Class ตาม SRP แทนการพับด้วย region”ใช้เทคนิค 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 ออกไปเลยแทนที่จะพับมันไว้