Extract Class
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”class หนึ่งค่อย ๆ รับความรับผิดชอบที่สองเข้ามาแบบเงียบ ๆ ให้ดึง field และ method ที่รับใช้งานชุดที่สองออกไปเป็น class ใหม่ แล้วให้ class เดิมถือ reference ไปหา class ใหม่นั้น จากนั้นแต่ละ class จะเหลือเหตุผลในการเปลี่ยนแปลงเพียงข้อเดียวที่ชัดเจน
อาการของปัญหา
หัวข้อที่มีชื่อว่า “อาการของปัญหา”นี่คือยารักษา Large Class — และเจาะจงกว่านั้นคือ class ที่มีสองความรับผิดชอบพันกันอยู่ สัญญาณบ่งชี้: กลุ่มย่อยของ field ที่เปลี่ยนแปลงด้วยกันเสมอ method ที่ทำงานกับกลุ่มย่อยนั้นเท่านั้น หรือชื่อที่ต้องใช้คำว่า “and” เพื่ออธิบายว่า class ทำอะไร เมื่อคุณสามารถลากเส้นที่สะอาดผ่านข้อมูลของ class ได้ ครึ่งหนึ่งในแต่ละด้านก็ต้องการเป็น type ของตัวเอง
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”Person เก็บและจัดรูปแบบเบอร์โทรไปด้วย ตอนแรก field เบอร์โทรกับการจัดรูปแบบอยู่บน Person ทั้งหมด หลัง refactor ทั้งสองส่วนรวมกันเป็น class TelephoneNumber และ Person ก็แค่ถือไว้หนึ่งตัว
// Beforeclass Person { constructor( public name: string, public areaCode: string, public number: string, ) {}
telephoneNumber(): string { return `(${this.areaCode}) ${this.number}`; }}
// Afterclass TelephoneNumber { constructor(public areaCode: string, public number: string) {}
toString(): string { return `(${this.areaCode}) ${this.number}`; }}
class Person { constructor(public name: string, public telephone: TelephoneNumber) {}
telephoneNumber(): string { return this.telephone.toString(); }}# Beforeclass Person: def __init__(self, name, area_code, number): self.name = name self.area_code = area_code self.number = number
def telephone_number(self): return f"({self.area_code}) {self.number}"
# Afterclass TelephoneNumber: def __init__(self, area_code, number): self.area_code = area_code self.number = number
def __str__(self): return f"({self.area_code}) {self.number}"
class Person: def __init__(self, name, telephone): self.name = name self.telephone = telephone
def telephone_number(self): return str(self.telephone)// Beforetype Person struct { Name string AreaCode string Number string}
func (p Person) TelephoneNumber() string { return fmt.Sprintf("(%s) %s", p.AreaCode, p.Number)}
// Aftertype TelephoneNumber struct { AreaCode string Number string}
func (t TelephoneNumber) String() string { return fmt.Sprintf("(%s) %s", t.AreaCode, t.Number)}
type Person struct { Name string Telephone TelephoneNumber}
func (p Person) TelephoneNumber() string { return p.Telephone.String()}// Beforestruct Person { name: String, area_code: String, number: String,}
impl Person { fn telephone_number(&self) -> String { format!("({}) {}", self.area_code, self.number) }}
// Afterstruct TelephoneNumber { area_code: String, number: String,}
impl TelephoneNumber { fn to_string(&self) -> String { format!("({}) {}", self.area_code, self.number) }}
struct Person { name: String, telephone: TelephoneNumber,}
impl Person { fn telephone_number(&self) -> String { self.telephone.to_string() }}classDiagram
class PersonBefore {
name
areaCode
number
telephoneNumber()
}
class PersonAfter {
name
telephone
telephoneNumber()
}
class TelephoneNumber {
areaCode
number
toString()
}
PersonAfter --> TelephoneNumber : has a
PersonBefore ..> PersonAfter : Extract Class กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ตัดสินใจว่าจะแบ่งความรับผิดชอบอย่างไร แล้วสร้าง class เปล่าสำหรับส่วนที่กำลังจะแยกออกมา ถ้าชื่อเดิมไม่เข้ากับ class ที่เหลือแล้ว ก็เปลี่ยนชื่อเสียด้วย
- เพิ่มลิงก์จาก class เดิมไปยัง class ใหม่ — โดยทั่วไปเป็น field ที่ถือ instance ของ class ใหม่
- ย้าย field ที่เกี่ยวข้องข้ามไปทีละตัว โดยใช้ Move Field และรักษาให้ชุด test เป็นสีเขียวหลังแต่ละครั้ง
- ย้าย method ที่อยู่กับ field เหล่านั้นข้ามไป โดยใช้ Move Function เริ่มจากตัวที่อยู่ระดับล่างสุด
- แทนที่การเข้าถึง field โดยตรงของ class เดิมด้วยการเรียกผ่าน instance ใหม่
- รัน test หลังการย้ายทุกครั้ง แต่ละขั้นตอนเล็กพอที่แถบสีแดงจะชี้ไปยังการเปลี่ยนแปลงเดียว
- ทบทวน public surface ของทั้งสอง class แล้วบีบให้แคบลง เพราะ class ที่แยกออกมาอาจซ่อนรายละเอียดที่ class เดิมเคยจำเป็นต้องเปิดเผยได้
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”แยก class เมื่อกลุ่มย่อยของ field และ method ก่อตัวเป็นกลุ่มก้อนที่ชัดเจน เมื่อ class สรุปได้ยากโดยไม่ใช้คำว่า “and” หรือเมื่อความรับผิดชอบหนึ่งเปลี่ยนแปลงด้วยเหตุผลที่ต่างไปจากส่วนที่เหลือโดยสิ้นเชิง
ต้นทุนคือมี class เพิ่มมาอีกหนึ่งตัวให้ต้องตั้งชื่อ สร้าง และไล่ตามอ่าน บวกกับชั้น delegation อีกชั้น ถ้า class เดิม ไม่ได้ ทำสองงานจริง การแยกออกมาก็เป็นแค่พิธีกรรมเปล่า ๆ ท่ากลับกันคือ Inline Class ถ้า class ใหม่ไม่เคยโตจนมีความรับผิดชอบของตัวเอง ก็พับกลับเข้าไปได้
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Extract Class เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| class มี field/method ที่แบ่งออกเป็นกลุ่มได้ชัด | class ใหม่จะมีแค่ 1-2 field และไม่มี behavior |
| นักพัฒนาต้องรู้เรื่องมากเกินไปเพื่อแก้ class เดียว | กำลัง extract เพื่อ architecture ล่วงหน้าโดยไม่มีเหตุผลปัจจุบัน |
| test ของ class นี้ซับซ้อนเพราะมีหลาย responsibility | class ที่ extract ออกมาจะยังพึ่งพา class เดิมอยู่มาก |
⚠️ ไม่ควร Extract Class เมื่อ:
- ยังไม่รู้ว่า responsibility ที่แท้จริงของ class นี้คืออะไร
- class ที่ extract จะมี coupling สูงกับ class เดิม — ไม่ได้ลด complexity จริง ๆ
- ทำเพื่อให้ “ดูเหมือน” ทำตาม Single Responsibility Principle