ช่วงนี้ผมได้มีโอกาส Review code เป็นจำนวนมาก
ผมพบ Pattern แบบหนึ่งที่หลายคนเข้าใจผิดว่าเป็นโค้ดที่ดีมีคุณภาพอ่านง่าย
class Invoice
def pay_invoice
make_sure_invoice_approved
create_transaction_entry
decrease_company_total_balance
record_tax_deduction
end
def x
end
def y
end
end
ซึ่งพออ่านแล้วดูดีมากเลย เหมือนเป็นประโยคภาษาอังกฤษติดต่อกัน เข้าใจง่าย (ยิ่งพอเป็นภาษา Ruby ยิ่งดูดี)
แต่พอไปดูแต่ละ Method ข้างในหน้าตาจะเป็นประมาณนี้
def make_sure_invoice_approved
@invoice = Invoice.find(@id)
raise Error if !@invoice.approved?
end
def create_transaction_entry
@transaction = Transaction.process(@invoice)
end
def decrease_company_total_balance
@invoice.company.balance = @invoice.company.balance - @transaction.amount
@invoice.company.balance.save()
end
def record_tax_deduction
TaxEntry.record(@transaction)
end
โค้ดชุดนี้ดูเผินๆ เหมือนจะ Clean และสะอาด แต่จริงๆ แล้วไม่เลย เพราะมันมี Implicit dependency เยอะมาก
หมายถึงว่า การคุยกันระหว่าง Method แต่ละตัวทำผ่านการเซ็ต Field ใน Object โดยที่ไม่ประกาศอย่างชัดเจนว่าแต่ละ Method ต้องการ Input-Output เป็นอะไร
แล้วมันไม่ดียังไงเหรอ?
ถ้่าสมมติมีคนถามว่า เราใช้เลขอะไรในการตัดยอดเงินรวมของบริษัท ผมอ่านจากโค้ดนี้ก้อนเดียวในคลาสนี้ ผมไม่อ่านตรงอื่นเลยนะ อ่านแค่ตรงนี้
def decrease_company_total_balance
@invoice.company.balance = @invoice.company.balance - @transaction.amount
@invoice.company.save()
end
คำถามที่ผมถามคือ อ้าว แล้ว @invoice มาจากไหน? @transaction มาจากไหนเนี่ย? ก็ถูกส่งมาจาก Method ไหนซัก Method ในคลาสนี้แหละ
แล้ว Method ไหนว้าาาาาาาาาาาาาาา
ถ้า Method ที่ยุ่งกับ @invoice, @transaction มีแค่ pay_invoice
อย่างเดียวก็โชคดีนะ แต่ถ้าเกิดว่าทั้ง def x
และ def y
ก็มายุ่งกับ @invoice, @transaction ล่ะ... แปลว่า x
และ y
สามารถมีผลกับโค้ดบรรทัดนี้ได้ทั้งหมด ก็ต้องไล่โค้ดอย่างละเอียดเลยว่าคนที่ยุ่งได้ทั้งหมดมีกี่คนในคลาส
ถ้ามี Bug เกิดแถวๆ นี้ ผมก็ต้องพิจารณาทุกอย่างที่ยุ่งกับ @invoice, @transaction ทั้งๆ ที่มันอาจจะไม่เกี่ยวกันเลยก็ได้
ซึ่งในกรณีนี้ ถ้าเราไม่พยายามทำให้มันดูเป็นประโยคภาษาอังกฤษมากเกินไป แต่ทำแบบนี้
def pay_invoice
invoice = make_sure_invoice_approved
transaction = create_transaction_entry(invoice)
decrease_company_total_balance(invoice.company, transaction)
record_tax_deduction(transaction)
end
มันชัดเจนว่าแต่ละ Method มี Dependency อะไรบ้าง
เวลาเราอ่านที่
def decrease_company_total_balance(company, transaction)
company.balance = company.balance - transaction.amount
company.save()
end
เราก็รู้เลยว่ามันมาจาก Caller เท่านั้น
เวลาเราจะต้องย้าย Method นี้ไปที่ Object อื่น ก็สามารถย้ายได้ทันทีอีกต่างหาก
การมี Method ที่ส่งต่อค่ากันผ่านการกำหนดค่า Field ใน Object นั้นทำให้
- ไล่ตามยากว่า Method นี้มี Input space ที่เป็นไปได้อย่างไรบ้าง
- ย้าย Method ออกจาก Object ยากมาก
แล้วเมื่อไหร่ที่คุยกันผ่าน Field ล่ะ
สำหรับผมกฎง่ายๆ ที่ทำให้ Field ทุก Field ใน Object จะต้องรับมาจากระบบภายนอกเท่านั้น
รับมาบันทึกเลยหรือรับมาคำนวนบางอย่างก่อนใส่ก็ได้ทั้งนั้น
เช่น
class Invoice
def initialize(amount)
@amount = amount
end
def tax
@amount * 0.07
end
def report
"This invoice cost #{@amount} with tax #{tax}"
end
def add(amount)
@amount = @amount + amount
end
end
กรณีนี้เนี่ย @amount มันรับมาจากภายนอกผ่าน add, initialize เพราะฉะนั้น การที่ Method tax จะคุยกับ add, initialize ผ่าน @amount ก็เข้าใจได้ เพราะต่อให้เราเขียน
def report
"This invoice cost #{@amount} with tax #{tax(@amount)}"
end
สุดท้ายถ้ากลับมาคำถามที่ว่า @amount มาจากไหน อะไรที่เป็นไปได้บ้าง มันก็ตอบว่า มาจากระบบภายนอก Class อยู่ดี จำกัด Flow ที่เป็นไปได้ไม่ได้
ซึ่งอันนี้ต่างกับข้างตัวอย่างแรกที่ @invoice ถูกสร้างขึ้นด้วย Method ภายใน Class ไม่มีทางมาจากภายนอกได้ ดังนั้น การจำกัดไม่ให้มันเป็น Field จึงเป็นการบอกอย่างชัดเจนว่ามันไม่ได้เป็น Input ที่มาจากระบบภายนอก มันเป็น Input ที่สร้างขึ้นภายในตัวเองนะ
และเมื่อ Input space วิธีการเข้าถึง Input ของแต่ละ Method ที่เป็นไปได้ลดลง เราก็จะ Refactor ย้ายของ และทำความเข้าใจเชิงลึกได้ง่ายขึ้น
ดังนั้นบางครั้งแค่อ่านง่ายอ่านสวยเป็นภาษาอังกฤษต่อเนื่องไม่มีอย่างอื่นมากวนใจ ไม่ใช่ Clean code นะครับ บางทีมันทำให้เราย้ายหรือแก้ไขหรือดูรายละเอียดการทำงานลึกๆ ไม่ได้เลยนอกจากอ่านสวยเป็นประโยคภาษาอังกฤษอย่างเดียว
(บทความนี้ไม่ได้มีความเป๊ะมาก ถ้าจะอธิบายมากกว่านี้ต้องลงลึกไปจนถึงระดับที่ว่าทำไม Object-oriented code ที่ดีถึงต้องเป็น Class เล็กๆ เลย การสื่อสารระหว่าง Method ผ่าน Parameter และผ่าน Private field มีผลต่างกันยังไง แต่อันนี้ขอบ่นคร่าวๆ ก่อนครับ)
Top comments (0)