Temporary Field
field ที่มีค่าเฉพาะบางสถานการณ์ นอกนั้นเป็น null/ไร้ความหมาย
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Temporary Field เกิดเมื่อ class มี field ที่ได้รับค่าและมีความหมายเฉพาะบางสถานการณ์เท่านั้น — โดยมากคือระหว่าง method ใด method หนึ่งหรือลำดับการเรียก method เฉพาะ — ส่วนเวลาอื่นมันเป็น null, ศูนย์ หรือค่าว่างที่ไร้ความหมาย field แบบนี้ไม่ได้มีอยู่เพราะเป็นคุณสมบัติที่เสถียรของ object แต่เกิดจากการหาทางลัด: แทนที่จะส่งค่าหลายตัวผ่าน parameter ของ method (ซึ่งจะทำให้ signature ยาวและกลายเป็น Long Parameter List) โปรแกรมเมอร์เลือกประกาศเป็น instance field ของ class แทน เพื่อให้ method หลายตัวใน algorithm เดียวกันเข้าถึงค่าเหล่านั้นได้โดยไม่ต้องส่งผ่าน parameter
ตัวอย่างคลาสสิกจากหนังสือ Refactoring ของ Martin Fowler คือ class Estimator ที่มี field อย่าง duration, average, standardDeviation ซึ่งถูกตั้งค่าเฉพาะตอนเรียก CalculateAverage() แล้วใช้ต่อใน CalculateStandardDeviation() — นอกบริบทการคำนวณนี้ field เหล่านั้นไม่มีความหมายอะไรเลย
กลิ่นนี้จัดอยู่ในกลุ่ม Object-Orientation Abusers (การใช้แนวคิด OOP ไม่ถูกวิธี) เพราะมันขัดกับสัญญาโดยนัยของ class: ผู้ใช้ class คาดหวังว่า field ทุกตัวที่ประกาศไว้ควรมีความหมายตลอดช่วงชีวิตของ object ไม่ใช่แค่ในหน้าต่างเวลาแคบ ๆ ระหว่าง method สองสามตัว
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- field ถูกกำหนดค่า (assign) ใน method หนึ่ง แล้วถูกอ่านใช้ในอีก method หนึ่งเท่านั้น ไม่มีที่อื่นใน class อ้างถึงมันเลย
- ก่อนเรียก method ที่ “เติมค่า” ให้ field นั้น ค่าของมันจะเป็น null,
default, ศูนย์ หรือ empty string เสมอ - มี logic ตรวจสอบ
if (field != null)หรือif (field.HasValue)กระจายอยู่หลายจุด เพื่อเช็คว่า “ตอนนี้ field ถูกเติมค่าแล้วหรือยัง” - ชื่อ field บอกใบ้ถึงการเป็นค่าชั่วคราวของ algorithm เช่น
_tempResult,_currentBatch,_workingSet - เปลี่ยนลำดับการเรียก method สองตัวแล้ว compile ผ่านปกติ แต่ผลลัพธ์กลับผิด (เพราะ field ที่ควรถูกเติมค่าก่อนหน้ายังไม่ถูกเติม)
- class ไม่ thread-safe เพราะ state ชั่วคราวถูกเก็บใน instance field ที่ใช้ร่วมกันระหว่างการเรียกพร้อมกันหลายครั้ง
- method ใน unit test ต้องเรียก method อื่นก่อนเสมอเพื่อ “เตรียม” field ให้พร้อม ก่อนจะทดสอบ method ที่ต้องการจริง ๆ
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”- ทำลายความคาดหวังพื้นฐานของ encapsulation — field ของ object ควรสะท้อน state ที่มีความหมายตลอดเวลา ไม่ใช่มีความหมายเฉพาะบางช่วง การอ่าน code จึงต้องไล่ตามลำดับการเรียก method เพื่อรู้ว่า field ไหน “พร้อมใช้” ตอนไหน ซึ่งเพิ่มภาระทางสมองอย่างมาก
- สร้างการต่อเชื่อมทางเวลา (temporal coupling) — method ต้องถูกเรียกตามลำดับที่ถูกต้องเท่านั้น compiler ไม่ช่วยบังคับสิ่งนี้ ถ้าสลับลำดับ code compile ผ่านแต่ให้ผลลัพธ์ผิดแบบเงียบ ๆ (silent failure) ดูกลิ่นที่เกี่ยวข้องคือ Temporal Coupling
- ทำให้ class ไม่ thread-safe โดยไม่รู้ตัว — ถ้า instance เดียวกันถูกใช้ซ้ำจากหลาย thread state ชั่วคราวใน field จะถูกเขียนทับกันข้ามการเรียก เกิด race condition ที่ debug ยาก
- ขยาย surface การทดสอบผิดจุด — เพื่อ unit test method เดียว ต้องเรียก method อื่นก่อนเพื่อเซ็ต field ให้ถูกต้อง ทำให้ test เปราะและผูกกับรายละเอียด implementation แทนที่จะทดสอบพฤติกรรมที่ต้องการ
- ทำให้ class รับผิดชอบเกินตัว — field ชั่วคราวมักเป็นสัญญาณว่ามี “อีกหนึ่งแนวคิด domain” ซ่อนอยู่ใน class นี้ (เช่น การคำนวณทางสถิติ, การประมวลผลเป็น batch) ที่สมควรแยกเป็น class ของตัวเอง การปล่อยไว้แบบนี้ทำให้ Single Responsibility Principle ถูกละเมิดไปด้วย
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”code ต่อไปนี้จำลองการคำนวณค่าสถิติของระยะเวลาการทำงาน (durations) คล้ายตัวอย่างคลาสสิกของ Fowler: _average และ _standardDeviation เป็น temporary field ที่มีความหมายเฉพาะระหว่างเรียก CalculateAverage() ตามด้วย CalculateStandardDeviation() เท่านั้น
// ก่อน refactor: temporary field ที่มีความหมายเฉพาะบางลำดับการเรียกpublic class DurationEstimator{ private readonly List<double> _durations;
// field เหล่านี้ไม่มีความหมายจนกว่าจะเรียก CalculateAverage() ก่อน private double _average; private double _standardDeviation;
public DurationEstimator(List<double> durations) { _durations = durations; }
public double CalculateAverage() { _average = _durations.Sum() / _durations.Count; return _average; }
// ต้องเรียก CalculateAverage() มาก่อน ไม่งั้นได้ผลลัพธ์ผิด // แต่ compiler ไม่บังคับลำดับนี้เลย public double CalculateStandardDeviation() { var variance = _durations .Select(d => Math.Pow(d - _average, 2)) .Sum() / _durations.Count;
_standardDeviation = Math.Sqrt(variance); return _standardDeviation; }}ปัญหา: สลับลำดับเรียก CalculateStandardDeviation() ก่อน CalculateAverage() แล้ว code compile ผ่านสบาย ๆ แต่ผลลัพธ์ผิดเงียบ ๆ เพราะ _average ยังเป็น 0
refactor ด้วย Extract Class (สร้าง method object)
หัวข้อที่มีชื่อว่า “refactor ด้วย Extract Class (สร้าง method object)”แยก temporary field และ logic ที่เกี่ยวข้องออกเป็น class ใหม่ที่มีอายุสั้น สร้างขึ้นเฉพาะสำหรับการคำนวณครั้งเดียว — เทคนิคนี้ตรงกับสิ่งที่ Fowler เรียกว่า Replace Method with Method Object
// หลัง refactor: Extract Class ดึง state ชั่วคราวออกเป็น class ของตัวเองpublic class DurationStatistics{ private readonly IReadOnlyList<double> _durations;
public DurationStatistics(IReadOnlyList<double> durations) { _durations = durations; Average = _durations.Sum() / _durations.Count; StandardDeviation = CalculateStandardDeviation(Average); }
// field เหล่านี้มีความหมายเสมอตลอดช่วงชีวิตของ object // ไม่ขึ้นกับลำดับการเรียก method ใด ๆ public double Average { get; } public double StandardDeviation { get; }
private double CalculateStandardDeviation(double average) { var variance = _durations .Select(d => Math.Pow(d - average, 2)) .Sum() / _durations.Count;
return Math.Sqrt(variance); }}
// การใช้งาน: ไม่มีลำดับการเรียกให้ทำผิดได้อีกต่อไปvar stats = new DurationStatistics(durations);Console.WriteLine($"{stats.Average} / {stats.StandardDeviation}");ผลลัพธ์ของการ refactor: Average และ StandardDeviation ถูกคำนวณครบใน constructor แล้วเปิดเป็น read-only property จึงไม่มีทางถูกอ่านก่อนที่จะถูกคำนวณ ไม่มี temporal coupling และ instance ใหม่แต่ละครั้งไม่ share state ข้าม thread
ทางเลือกอื่นเมื่อ state ชั่วคราวน้อย
หัวข้อที่มีชื่อว่า “ทางเลือกอื่นเมื่อ state ชั่วคราวน้อย”ถ้ามีค่าชั่วคราวแค่ 1-2 ตัวและไม่ได้ส่งต่อกันหลาย method การส่งเป็น parameter ตรง ๆ ก็เพียงพอ ไม่จำเป็นต้อง Extract Class ทั้ง class:
public double CalculateStandardDeviation(IReadOnlyList<double> durations, double average){ var variance = durations.Select(d => Math.Pow(d - average, 2)).Sum() / durations.Count; return Math.Sqrt(variance);}เมื่อ temporary field มีไว้เพื่อเลี่ยง null check ที่กระจายทั่ว class
หัวข้อที่มีชื่อว่า “เมื่อ temporary field มีไว้เพื่อเลี่ยง null check ที่กระจายทั่ว class”อีกกรณีที่พบบ่อยคือ field ถูกเช็ค != null ก่อนใช้งานในหลายจุด ทางแก้ตามแนวทาง Introduce Null Object คือแทนค่า null ด้วย object ตัวแทนที่มีพฤติกรรมเป็นกลาง (no-op) แทน เพื่อลบเงื่อนไขตรวจสอบที่กระจายอยู่ทั่ว class ออกไป
classDiagram
class DurationEstimator {
-durations
-average
-standardDeviation
+CalculateAverage
+CalculateStandardDeviation
}
note for DurationEstimator "average และ standardDeviation มีความหมายเฉพาะหลังเรียก method ตามลำดับ"
class DurationStatistics {
+Average
+StandardDeviation
}
note for DurationStatistics "คำนวณครบใน constructor ไม่มี temporal coupling"
DurationEstimator --> DurationStatistics : refactor เป็น
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Encapsulation — หลักการที่ Temporary Field ละเมิด เพราะ field ไม่ได้สะท้อน state ที่มีความหมายเสมอ
- Data Class — กลิ่นใกล้เคียงที่ class มี field แต่ไม่มี behavior ที่ควบคุมมันอย่างเหมาะสม
- Long Parameter List — สาเหตุตั้งต้นที่มักผลักดันให้เกิด Temporary Field แทนการส่ง parameter
- Temporal Coupling — ผลข้างเคียงหลักของ Temporary Field คือการบังคับลำดับการเรียก method ที่ compiler ไม่ช่วยตรวจสอบ
- Make Illegal States Unrepresentable — แนวทางออกแบบที่ป้องกันไม่ให้ state ที่ยังไม่พร้อม (field ว่าง) ถูกสร้างขึ้นได้ตั้งแต่แรก
- Single Responsibility Principle — Extract Class ที่ใช้แก้กลิ่นนี้มักเผยว่ามีความรับผิดชอบที่สองซ่อนอยู่ใน class เดิม