ช่วงนี้ผมได้มีโอกาส 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)