DEV Community

Chakrit Likitkhajorn
Chakrit Likitkhajorn

Posted on • Updated on • Originally published at chrisza.me

Code เหมือนจะ Clean

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

ซึ่งพออ่านแล้วดูดีมากเลย เหมือนเป็นประโยคภาษาอังกฤษติดต่อกัน เข้าใจง่าย (ยิ่งพอเป็นภาษา 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
Enter fullscreen mode Exit fullscreen mode

โค้ดชุดนี้ดูเผินๆ เหมือนจะ 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
Enter fullscreen mode Exit fullscreen mode

คำถามที่ผมถามคือ อ้าว แล้ว @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
Enter fullscreen mode Exit fullscreen mode

มันชัดเจนว่าแต่ละ Method มี Dependency อะไรบ้าง

เวลาเราอ่านที่

def decrease_company_total_balance(company, transaction)
  company.balance = company.balance - transaction.amount
  company.save()
end
Enter fullscreen mode Exit fullscreen mode

เราก็รู้เลยว่ามันมาจาก Caller เท่านั้น

เวลาเราจะต้องย้าย Method นี้ไปที่ Object อื่น ก็สามารถย้ายได้ทันทีอีกต่างหาก

การมี Method ที่ส่งต่อค่ากันผ่านการกำหนดค่า Field ใน Object นั้นทำให้

  1. ไล่ตามยากว่า Method นี้มี Input space ที่เป็นไปได้อย่างไรบ้าง
  2. ย้าย 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
Enter fullscreen mode Exit fullscreen mode

กรณีนี้เนี่ย @amount มันรับมาจากภายนอกผ่าน add, initialize เพราะฉะนั้น การที่ Method tax จะคุยกับ add, initialize ผ่าน @amount ก็เข้าใจได้ เพราะต่อให้เราเขียน

def report
  "This invoice cost #{@amount} with tax #{tax(@amount)}"
end
Enter fullscreen mode Exit fullscreen mode

สุดท้ายถ้ากลับมาคำถามที่ว่า @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)