Vertical Separation
code ที่เกี่ยวข้องกันถูกวางห่างกันมากใน file
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Vertical Separation คือกลิ่นที่เกิดเมื่อ “สิ่งที่เกี่ยวข้องกัน” สองสิ่งใน file เดียวกันถูกวางห่างกันเกินจำเป็น — ตัวแปรถูกประกาศไว้บนสุดของ method แต่ถูกใช้งานจริงอีกหลายสิบบรรทัดถัดมา, method private ที่ถูกเรียกใช้อยู่บรรทัดที่ 20 แต่ตัวมันเองถูกนิยามไว้บรรทัดที่ 200, หรือ field กับ constructor ที่ใช้มันถูกคั่นด้วย method อื่นอีกนับสิบตัว
รูปแบบที่พบบ่อยที่สุดคือนิสัยการ “ประกาศตัวแปรทั้งหมดไว้ด้านบนก่อน” ตามธรรมเนียมภาษาเก่า ๆ (เช่น C แบบดั้งเดิมที่บังคับประกาศตัวแปรต้น block) ทั้งที่ภาษาสมัยใหม่อย่าง C# ไม่มีข้อจำกัดนั้นแล้ว การทำเช่นนี้สร้างช่องว่างเทียม (artificial separation) ระหว่างจุดที่ตัวแปรถูกสร้างกับจุดที่มันถูกใช้จริง ทั้งที่ทั้งสองส่วนควรอยู่ใกล้กันเพื่อให้อ่านเป็นเรื่องเดียวกันได้ในคราวเดียว
กลิ่นนี้เป็นเรื่องของ ระยะห่างในแนวตั้ง (บรรทัดต่อบรรทัดใน file) ไม่ใช่เรื่องโครงสร้าง class หรือ module จึงมักถูกมองข้ามใน code รีวิว เพราะแต่ละบรรทัดที่แยกกันอาจดู “ถูกต้อง” ในตัวเอง ปัญหาจะเห็นชัดก็ต่อเมื่อพยายามอ่านทั้ง method เป็นเรื่องราวเดียว
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- ตัวแปรถูกประกาศที่ต้น method แต่ถูกใช้งานครั้งแรกหลังจากนั้นอีกหลายสิบบรรทัด
- ต้องเลื่อนขึ้นเลื่อนลง (scroll) ไปมาเพื่อเชื่อมโยงว่าตัวแปรตัวนี้มาจากไหน หรือ method ที่ถูกเรียกอยู่ที่ไหน
- method private/helper ถูกวางไว้ไกลจากจุดที่ถูกเรียกใช้ครั้งแรก แทนที่จะอยู่ใต้ผู้เรียกทันที
- มีการใช้
#regionเพื่อ “ซ่อน” กลุ่ม method helper ไว้ท้าย file ซึ่งมักเป็นการปิดบังปัญหานี้มากกว่าแก้ไข (ดู Regions) - จัดกลุ่ม method ตาม access modifier (public ทั้งหมดก่อน แล้วค่อย private ทั้งหมด) แทนที่จะจัดตามความสัมพันธ์เชิงหน้าที่ — ผู้เรียกกับผู้ถูกเรียกที่เกี่ยวข้องกันจึงกระจัดกระจาย
- method ยาวมาก (มักมากับ Long Method) ที่ทำหลายขั้นตอน โดยตัวแปรของแต่ละขั้นตอนถูกปนกันไว้ตอนต้น function
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”การอ่าน code คือการสร้างแบบจำลองในหัวว่า “อะไรเกิดขึ้น แล้วมันเชื่อมกับอะไร” เมื่อสิ่งที่เกี่ยวข้องกันถูกแยกออกจากกันด้วย code อื่นจำนวนมาก ผู้อ่านต้อง ถือบริบทไว้ในหัว (เช่น “อ้อ ตัวแปร taxRate ที่เห็นตอนต้น เดี๋ยวจะโดนใช้ตอนท้าย”) ตลอดช่วงที่เลื่อนผ่าน code ที่ไม่เกี่ยวข้อง นี่คือภาระทางปัญญา (cognitive load) ที่ไม่จำเป็น และยิ่ง method ยาวเท่าไร ภาระนี้ก็ยิ่งสะสมมากขึ้นเท่านั้น
ผลกระทบที่ตามมา:
- code รีวิวพลาดง่ายขึ้น — ผู้รีวิวเห็นเฉพาะบรรทัดที่เปลี่ยน (diff) แต่บริบทของตัวแปรหรือ method ที่เกี่ยวข้องอยู่ไกลออกไปนอกจอ ทำให้พลาดผลกระทบข้างเคียงได้ง่าย
- แก้ bug ยากขึ้น — เมื่อจะแก้ไขวิธีคำนวณค่าใดค่าหนึ่ง ต้องไล่หาทุกจุดที่เกี่ยวข้องซึ่งกระจายอยู่ทั่ว file แทนที่จะเห็นเป็นกลุ่มก้อนเดียว
- เพิ่มความเสี่ยงต่อ bug เวลาแก้ไข — ถ้าย้ายหรือแก้ตัวแปรที่ประกาศไว้ไกลจากจุดใช้ ผู้แก้ไขอาจมองไม่เห็นผลกระทบทั้งหมด เพราะการใช้งานจริงถูกซ่อนอยู่ลึกใน method
- เป็นสัญญาณของปัญหาที่ลึกกว่า — บ่อยครั้ง Vertical Separation ไม่ใช่แค่เรื่องจัดบรรทัด แต่บอกว่า method นั้นทำหลายหน้าที่เกินไป (ขัดกับ Single Responsibility) จนต้องมีตัวแปรหลายชุดที่ใช้ในช่วงต่าง ๆ ของ method เดียวกัน การจัดกลุ่ม code ใหม่ให้ใกล้กันจึงมักเป็นก้าวแรกที่นำไปสู่การเห็นขอบเขตของ method ย่อยที่ซ่อนอยู่
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างนี้ประกาศตัวแปรทั้งหมดไว้บนสุด แล้วใช้งานกระจายอยู่ปลาย method พร้อมทั้งซ่อน method helper ไว้ใน #region ท้าย class:
// smelly: ตัวแปรถูกประกาศไว้บนสุด แต่ถูกใช้จริงอีกหลายบรรทัดถัดมา// และ method helper ถูกซ่อนไว้ท้าย class ด้วย #regionpublic class OrderProcessor{ public void ProcessOrder(Order order) { var pricingPlan = _pricingService.GetPlan(order.CustomerId); var taxRate = _taxService.GetRate(order.ShippingAddress); var discountCode = order.DiscountCode; decimal total = 0;
ValidateOrder(order); LogOrderReceived(order);
// ...สมมติมี code อื่นคั่นอยู่อีกหลายสิบบรรทัดตรงนี้...
total = CalculateSubtotal(order); total = ApplyDiscount(total, discountCode); // เพิ่งใช้ discountCode ตอนนี้ total += total * taxRate; // เพิ่งใช้ taxRate ตอนนี้ total = pricingPlan.AdjustForPlan(total); // เพิ่งใช้ pricingPlan ตอนนี้
Charge(order.CustomerId, total); }
#region Helper methods private void ValidateOrder(Order order) { /* ... */ } private void LogOrderReceived(Order order) { /* ... */ } private decimal CalculateSubtotal(Order order) { /* ... */ } private decimal ApplyDiscount(decimal total, string code) { /* ... */ } private void Charge(string customerId, decimal amount) { /* ... */ } #endregion}หลัง refactor ด้วย Slide Statements (เลื่อนการประกาศตัวแปรให้ไปอยู่ติดกับจุดที่ใช้จริง — refactoring ใน catalog ของ Martin Fowler) ร่วมกับ Extract Method (แยกแต่ละขั้นตอนคำนวณออกเป็น method ที่มีชื่อสื่อความหมาย) แล้วเลิกใช้ #region เป็นตัวซ่อนปัญหา:
// clean: ตัวแปรแต่ละตัวถูกประกาศและใช้งานอยู่ในขอบเขตเดียวกัน// ผู้เรียกกับ helper อยู่ใกล้กัน อ่านจากบนลงล่างได้เป็นเรื่องเดียวpublic class OrderProcessor{ public void ProcessOrder(Order order) { ValidateOrder(order); LogOrderReceived(order);
var subtotal = CalculateSubtotal(order); var discounted = ApplyDiscount(subtotal, order.DiscountCode); var taxed = ApplyTax(discounted, order.ShippingAddress); var total = ApplyPricingPlan(taxed, order.CustomerId);
Charge(order.CustomerId, total); }
private void ValidateOrder(Order order) { /* ... */ }
private void LogOrderReceived(Order order) { /* ... */ }
private decimal CalculateSubtotal(Order order) { /* ... */ }
private decimal ApplyDiscount(decimal total, string discountCode) { // discountCode ถูกประกาศและใช้ในจุดเดียวกันแล้ว ไม่ต้องแบกไว้ตั้งแต่ต้น method // ... return total; }
private decimal ApplyTax(decimal total, Address shippingAddress) { var taxRate = _taxService.GetRate(shippingAddress); return total + total * taxRate; }
private decimal ApplyPricingPlan(decimal total, string customerId) { var pricingPlan = _pricingService.GetPlan(customerId); return pricingPlan.AdjustForPlan(total); }
private void Charge(string customerId, decimal amount) { /* ... */ }}สังเกตว่าหลัง refactor ตัวแปรอย่าง pricingPlan และ taxRate ไม่ได้ “หาย” ไปไหน แต่ถูกย้ายให้ประกาศและใช้งานอยู่ ใน method เดียวกัน ในบรรทัดติดกัน ผลลัพธ์คือ ProcessOrder เองอ่านได้เหมือนสารบัญของขั้นตอนธุรกิจ (validate → log → subtotal → discount → tax → pricing plan → charge) โดยไม่ต้องแบกรายละเอียดการคำนวณแต่ละอย่างไว้ในหัวระหว่างอ่าน และไม่ต้องใช้ #region มาซ่อนความยาวของ class อีกต่อไป
ข้อควรระวัง: Slide Statements ทำได้เฉพาะเมื่อการย้ายไม่เปลี่ยนพฤติกรรม — ห้ามเลื่อนการใช้ตัวแปรไปอยู่ก่อนจุดประกาศ และถ้ามีบรรทัดอื่นที่แก้ไขค่าตัวแปรระหว่างทาง ต้องไม่เลื่อนข้ามบรรทัดนั้น
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Code Readability — แนวปฏิบัติหลักที่ Vertical Separation ละเมิด
- Long Method — method ยาวมักเป็นสาเหตุที่ทำให้เกิดช่องว่างแนวตั้งขนาดใหญ่
- Regions — การใช้
#regionมักเป็นการซ่อน Vertical Separation แทนที่จะแก้ไขมัน - Comments — คอมเมนต์อธิบายบริบทมักถูกใช้ชดเชยเมื่อ code ที่เกี่ยวข้องกันอยู่ห่างกันเกินไป
- Bump Road — อีกกลิ่นเรื่องโครงสร้าง method ที่ทำให้อ่านลำบาก