Code เหมือนจะ Clean
Chakrit Likitkhajorn
Posted on March 4, 2020
ช่วงนี้ผมได้มีโอกาส 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 มีผลต่างกันยังไง แต่อันนี้ขอบ่นคร่าวๆ ก่อนครับ)
Posted on March 4, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024