Long Parameter List
method ที่รับ parameter มากเกินไป
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Long Parameter List เป็นหนึ่งใน Bloaters — กลิ่น code ที่ค่อย ๆ บวมขึ้นจนหนักอึ้งเกินจะดูแล เกิดขึ้นเมื่อ method หนึ่งตัวรับ parameter เข้ามามากเกินไป ไม่มีตัวเลขตายตัวว่า “มากเกินไป” คือเท่าไหร่ แต่แนวทางที่ยอมรับกันทั่วไป (รวมถึงใน refactoring.guru และ SourceMaking) คือ ตั้งแต่ 3-4 ตัวขึ้นไปควรเริ่มระวัง และยิ่งมากกว่านั้นยิ่งน่าสงสัยว่า method กำลังทำมากเกินหน้าที่ของมัน
กลิ่นนี้มักโผล่มาจากสาเหตุที่ฟังดูมีเหตุผลในตอนแรก:
- รวมหลาย algorithm เข้าด้วยกัน — method เดียวถูกทำให้ทำได้หลายแบบ แล้วใช้ parameter (มักเป็น boolean flag) ควบคุมว่าจะรันแบบไหน ผลคือ parameter เพิ่มขึ้นทุกครั้งที่เพิ่มพฤติกรรมใหม่ อาการนี้ใกล้เคียงกับ Flags Over Objects
- พยายามลด coupling ระหว่าง class — code ที่เคยสร้าง object เองภายใน method ถูกย้ายออกไปให้ผู้เรียกสร้างแล้วส่งเข้ามาแทน ยิ่งสร้าง object กี่ตัว ก็ยิ่งต้องมี parameter เพิ่มเท่านั้น
- ข้อมูลที่เกี่ยวข้องกันแต่ไม่เคยถูกจัดกลุ่ม — เช่น street, city, postalCode, country ที่จริงคือ “ที่อยู่” เดียวกัน แต่ถูกส่งเป็น parameter แยกทีละตัว ซึ่งมักมาคู่กับ Data Clumps และ Primitive Obsession
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- ต้องเปิดดู signature หรือ IntelliSense ทุกครั้งก่อนเรียกใช้ เพราะจำลำดับ parameter ไม่ได้
- parameter หลายตัวมี type เดียวกัน (เช่น
string, string, string) ทำให้สลับตำแหน่งกันได้ง่ายโดย compiler ไม่ฟ้อง - ทุกครั้งที่เพิ่มความสามารถใหม่เพียงนิดเดียว ต้องเพิ่ม parameter ใหม่ใน method นี้ และต้องตามไปแก้ทุกจุดที่เรียกใช้ — อาการคล้าย Shotgun Surgery
- มี overload หลายตัวของ method เดียวกันเพื่อรองรับชุด parameter ที่ต่างกันเล็กน้อย
- เขียน test แล้วต้องไล่ประกอบชุด parameter นับสิบชุดเพื่อให้ครอบคลุม — สัญญาณของ Combinatorial Explosion
- parameter บางตัวเป็นค่าที่คำนวณได้จาก object อื่นที่ส่งมาอยู่แล้ว (เช่นส่งทั้ง
orderและorder.Totalแยกกัน)
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”อ่านยาก เรียกยาก — รายการ parameter ยาวทำให้ผู้เรียกต้องจำทั้งจำนวน ลำดับ และความหมายของแต่ละตัว ยิ่งยาวยิ่งเป็นภาระทางสมอง (cognitive load) ที่ไม่จำเป็น
ผิดพลาดง่ายแบบไม่มี compiler ช่วยจับ — เมื่อ parameter สองสามตัวมี type เดียวกัน การสลับตำแหน่งกันคือ bug เงียบที่ compile ผ่านฉลุยแต่พังตอนรันจริง เช่นสลับ city กับ country ที่ต่างก็เป็น string
บอกใบ้ว่า method ทำหลายหน้าที่เกินไป — ตาม Wikipedia อธิบายไว้ว่ารายการ parameter ยาว “อาจบ่งชี้ว่าจุดประสงค์ของ function ถูกออกแบบมาไม่รอบคอบ” (ill-conceived) และควร refactor ให้แต่ละส่วนรับผิดชอบชัดเจนขึ้น ซึ่งสอดคล้องกับ Single Responsibility Principle
ขยายยาก — ทุกครั้งที่ต้องเพิ่มข้อมูลใหม่ให้ method ต้องแก้ signature และไล่แก้ทุกจุดที่เรียก การเปลี่ยนแปลงเล็ก ๆ กลายเป็นการแก้ไขที่กระจายไปทั่ว codebase
ทดสอบยาก — จำนวนชุดค่าที่เป็นไปได้ (combination) ของ parameter โตแบบ exponential เมื่อ parameter เพิ่มขึ้นทีละตัว ทำให้การเขียน test ให้ครอบคลุมทำได้ยากขึ้นเรื่อย ๆ
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างกลิ่น code
หัวข้อที่มีชื่อว่า “ตัวอย่างกลิ่น code”public class OrderService{ public Order CreateOrder( int customerId, string firstName, string lastName, string street, string city, string postalCode, string country, string shippingMethod, decimal discountPercent, bool isGiftWrapped) { // ต้องประกอบข้อมูล 10 ชิ้นให้ถูกลำดับทุกครั้งที่เรียก // street, city, postalCode, country เป็น string ทั้งหมด สลับกันได้ง่ายมาก var order = new Order(customerId, firstName, lastName); order.SetShippingAddress(street, city, postalCode, country); order.SetShippingMethod(shippingMethod); order.ApplyDiscount(discountPercent); order.IsGiftWrapped = isGiftWrapped; return order; }}refactor ด้วย Introduce Parameter Object และ Preserve Whole Object
หัวข้อที่มีชื่อว่า “refactor ด้วย Introduce Parameter Object และ Preserve Whole Object”จัดกลุ่ม parameter ที่ “ไปด้วยกันเสมอ” ให้เป็น value object เดียว แล้วส่ง object นั้นทั้งก้อนแทนการแตกเป็นชิ้น ๆ (Preserve Whole Object) — เทคนิคทั้งสองนี้มาจาก refactoring catalog ของ Martin Fowler โดยตรง:
public sealed class ShippingAddress{ public string Street { get; } public string City { get; } public string PostalCode { get; } public string Country { get; }
public ShippingAddress(string street, string city, string postalCode, string country) { Street = street; City = city; PostalCode = postalCode; Country = country; }}
public sealed class OrderOptions{ public string ShippingMethod { get; } public decimal DiscountPercent { get; } public bool IsGiftWrapped { get; }
public OrderOptions(string shippingMethod, decimal discountPercent, bool isGiftWrapped) { ShippingMethod = shippingMethod; DiscountPercent = discountPercent; IsGiftWrapped = isGiftWrapped; }}
public class OrderService{ public Order CreateOrder(int customerId, CustomerName name, ShippingAddress address, OrderOptions options) { // เหลือ 4 parameter แต่ละตัวสื่อความหมายในตัวเอง สลับกันไม่ได้เพราะ type ต่างกัน var order = new Order(customerId, name); order.SetShippingAddress(address); order.SetShippingMethod(options.ShippingMethod); order.ApplyDiscount(options.DiscountPercent); order.IsGiftWrapped = options.IsGiftWrapped; return order; }}ข้อดีแฝงที่ Fowler ชี้ไว้ในหนังสือ Refactoring คือ พอรวม parameter เป็น object แล้ว มักจะเห็นพฤติกรรมที่ควรย้ายเข้าไปอยู่ใน object ใหม่นั้นด้วย เช่น ShippingAddress อาจมี method Format() ของตัวเอง — นี่คือก้าวแรกของการ encapsulate ข้อมูลกลับเข้าที่ที่ควรอยู่
เมื่อการประกอบ object ยังซับซ้อนอยู่ ให้ใช้ Builder
หัวข้อที่มีชื่อว่า “เมื่อการประกอบ object ยังซับซ้อนอยู่ ให้ใช้ Builder”ถ้าจำนวน field ที่ optional มีมาก หรือมีหลายวิธีประกอบ Order ที่ต่างกันไปตามบริบท การสร้าง constructor ที่รับ parameter object ตัวเดียวอาจยังไม่พอ — Builder ช่วยให้ประกอบทีละขั้นตอนได้อ่านง่ายกว่า:
var order = new OrderBuilder() .ForCustomer(customerId, name) .ShipTo(address) .Using(options) .Build();flowchart LR
A[CreateOrder 10 parameters] --> B[Introduce Parameter Object]
B --> C[CreateOrder with ShippingAddress and OrderOptions]
C --> D[Still complex to assemble]
D --> E[Builder pattern step by step]