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

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 ให้​ครอบคลุม​ทำได้​ยาก​ขึ้น​เรื่อย ๆ

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;
}
}

จัด​กลุ่ม 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 ข้อมูล​กลับ​เข้า​ที่​ที่​ควร​อยู่

ถ้า​จำนวน 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]