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

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 ก็แค่ถือไว้หนึ่งตัว

// Before
class Person {
constructor(
public name: string,
public areaCode: string,
public number: string,
) {}
telephoneNumber(): string {
return `(${this.areaCode}) ${this.number}`;
}
}
// After
class 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();
}
}
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
Person หนึ่งตัวแยกออกเป็น Person บวกกับ TelephoneNumber ที่เป็นเจ้าของ
  1. ตัดสินใจว่าจะแบ่งความรับผิดชอบอย่างไร แล้วสร้าง class เปล่าสำหรับส่วนที่กำลังจะแยกออกมา ถ้าชื่อเดิมไม่เข้ากับ class ที่เหลือแล้ว ก็เปลี่ยนชื่อเสียด้วย
  2. เพิ่มลิงก์จาก class เดิมไปยัง class ใหม่ — โดยทั่วไปเป็น field ที่ถือ instance ของ class ใหม่
  3. ย้าย field ที่เกี่ยวข้องข้ามไปทีละตัว โดยใช้ Move Field และรักษาให้ชุด test เป็นสีเขียวหลังแต่ละครั้ง
  4. ย้าย method ที่อยู่กับ field เหล่านั้นข้ามไป โดยใช้ Move Function เริ่มจากตัวที่อยู่ระดับล่างสุด
  5. แทนที่การเข้าถึง field โดยตรงของ class เดิมด้วยการเรียกผ่าน instance ใหม่
  6. รัน test หลังการย้ายทุกครั้ง แต่ละขั้นตอนเล็กพอที่แถบสีแดงจะชี้ไปยังการเปลี่ยนแปลงเดียว
  7. ทบทวน 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 นี้ซับซ้อนเพราะมีหลาย responsibilityclass ที่ extract ออกมาจะยังพึ่งพา class เดิมอยู่มาก

⚠️ ไม่ควร Extract Class เมื่อ:

  • ยังไม่รู้ว่า responsibility ที่แท้จริงของ class นี้คืออะไร
  • class ที่ extract จะมี coupling สูงกับ class เดิม — ไม่ได้ลด complexity จริง ๆ
  • ทำเพื่อให้ “ดูเหมือน” ทำตาม Single Responsibility Principle
Extract Class แก้ smell ใดเป็นหลัก?
อะไรคือสัญญาณที่หนักแน่นว่า class ควรถูกแยก?
การ refactor ใดที่ทำงานหนักภายในกลไกของ Extract Class?
อะไรคือสิ่งตรงข้ามของ Extract Class?