Replace Subclass with Fields
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Replace Subclass with Fields สลายลำดับชั้นเล็ก ๆ ที่ subclass ไม่มี behavior ใด ๆ — มีเพียง ค่าคงที่ที่ต่างกัน ที่ถูกคืนกลับจาก method ที่ override เท่านั้น แต่ละ method แบบนั้นกลายเป็น field บน class concrete เดียว และค่าที่แยกความต่างของ subclass ก็ย้ายเข้าไปอยู่ใน constructor ของ class นั้น ลำดับชั้นหายไป ส่วนข้อมูลที่เข้ารหัสไว้ยังคงอยู่ในรูป field ธรรมดา
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”คุณเจอ subclass ที่ทุก override เป็นบรรทัดเดียวคืนค่า literal: isMale() { return true; }, code() { return "M"; } subclass นั้นไม่เพิ่ม logic ใด ไม่ถือ state ต่อ instance และไม่เคยเปลี่ยนคำตอบ ทั้ง type — พร้อมไฟล์ของตัวเอง constructor ของตัวเอง ช่องของตัวเองในลำดับชั้น — มีอยู่เพียงเพื่อ hard-code ค่าคงที่สองค่า นั่นคือ Lazy Class ที่แต่งตัวเป็น subtype และทำให้ความแตกต่างระหว่าง variant มองเห็นได้ยากกว่าการเขียน field เรียงกันเป็นแถว
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”Person ถูกแยกเป็น subclass Male กับ Female แต่ละตัว override method เพื่อคืนค่าคงที่ตายตัว หลังจากนั้น Person เดียวถือค่าคงที่เหล่านั้นไว้เป็น field ที่ factory ตั้งให้
// Before — subclasses that only return constantsabstract class Person { abstract isMale(): boolean; abstract code(): string;}
class Male extends Person { isMale(): boolean { return true; } code(): string { return "M"; }}
class Female extends Person { isMale(): boolean { return false; } code(): string { return "F"; }}
// After — one class, the constants become fieldsclass Person { private constructor( private readonly male: boolean, private readonly genderCode: string, ) {}
static createMale(): Person { return new Person(true, "M"); } static createFemale(): Person { return new Person(false, "F"); }
isMale(): boolean { return this.male; } code(): string { return this.genderCode; }}# Before — subclasses that only return constantsclass Person: def is_male(self): raise NotImplementedError
def code(self): raise NotImplementedError
class Male(Person): def is_male(self): return True
def code(self): return "M"
class Female(Person): def is_male(self): return False
def code(self): return "F"
# After — one class, the constants become fieldsclass Person: def __init__(self, male, code): self._male = male self._code = code
@classmethod def create_male(cls): return cls(True, "M")
@classmethod def create_female(cls): return cls(False, "F")
def is_male(self): return self._male
def code(self): return self._code// Go has no inheritance, so the "before" would already be modelled with an// interface plus two empty structs whose methods return constants — pure// boilerplate. The idiomatic shape is the "after": one struct with fields,// built by constructor functions.
type Person struct { male bool code string}
func NewMale() Person { return Person{male: true, code: "M"} }func NewFemale() Person { return Person{male: false, code: "F"} }
func (p Person) IsMale() bool { return p.male }func (p Person) Code() string { return p.code }// Rust has no inheritance. The "before" — a trait with two unit structs// whose impls return constants — adds nothing but ceremony. The idiomatic// form holds the data in fields on one struct, with constructor functions.
struct Person { male: bool, code: char,}
impl Person { fn new_male() -> Self { Person { male: true, code: 'M' } }
fn new_female() -> Self { Person { male: false, code: 'F' } }
fn is_male(&self) -> bool { self.male }
fn code(&self) -> char { self.code }}classDiagram
class Person {
<<abstract>>
+isMale() bool
+code() string
}
class Male
class Female
Person <|-- Male
Person <|-- Female
class PersonAfter["Person"] {
-male: bool
-code: string
+isMale() bool
+code() string
} กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ใช้ Replace Constructor with Factory Function กับ superclass เพื่อให้ caller สร้าง instance ผ่าน method factory แทนการเรียก
new Subclass()ตรง ๆ - สำหรับแต่ละ method ที่ subclass override เพื่อคืนค่าคงที่ ให้เพิ่ม field บน superclass ที่ถือค่านั้นไว้
- ทำให้แต่ละ method factory ส่งค่าคงที่ที่เหมาะสมตอนสร้าง instance ของ superclass
- เปลี่ยน method ของ superclass ให้คืนค่า field ใหม่แทนการเป็น abstract
- รัน test ทุกจุดเรียกควรสร้าง instance ของ superclass ด้วยค่า field ที่ถูกต้องแล้ว
- ลบ subclass ที่ว่างเปล่าทิ้ง เมื่อไม่เหลืออะไรอ้างถึงแล้ว
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”หยิบสิ่งนี้มาใช้เมื่อใดก็ตามที่งานเดียวของ subclass คือการ hard-code ค่าคืนกลับ — ไม่มี field เพิ่ม ไม่มี behavior จริง ไม่มี logic ต่อการเรียก การพับค่าเหล่านั้นเข้าไปเป็น field จะลบ type หนึ่งออก ทำให้ลำดับชั้นแบนลง และจัดเรียง variant ให้ความแตกต่างอ่านได้เป็นข้อมูลแทนที่จะเป็นนิยาม class
อย่า นำมาใช้ถ้า subclass มี behavior จริงที่แตกต่างกัน หรือถ้าคุณคาดว่าจะงอก behavior ขึ้นมา เพราะถ้าเป็นเช่นนั้น ลำดับชั้น (หรือ strategy) ก็ยังคุ้มค่าที่จะมีอยู่ ท่ากลับกันคือ Replace Fields with Subclass — เลื่อน field ที่แยกความต่างกลับขึ้นไปเป็น subtype เมื่อ behavior ไม่ใช่เพียงข้อมูล เริ่มแยกตัวออกตาม field นั้น
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Replace Subclass with Fields เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| subclass ต่างกันแค่ค่า constant ไม่มี behavior ต่างกัน | subclass มี behavior ที่แตกต่างกัน — ต้องการ subclass จริง ๆ |
| hierarchy ที่ลึกสำหรับแค่ data variation | field เพิ่มแล้วจะทำให้ superclass มี conditional มากขึ้น |
| ต้องการ simplify class ที่มี subclass แค่ override constant | client ต้อง instanceof check ซึ่ง defeat วัตถุประสงค์ |
⚠️ ไม่ควร Replace Subclass with Fields เมื่อ:
- subclass มี method ที่ต่างกัน แม้แค่บางส่วน
- field เพิ่มแล้วทำให้ต้องมี switch statement แทน polymorphism
- จำนวน variant มีมากและน่าจะเพิ่มขึ้น