Code เหมือนจะ Clean
Published: 2020-03-04 13:15:56

ช่วงนี้ผมได้มีโอกาส Review code เป็นจำนวนมาก

ผมพบ Pattern แบบหนึ่งที่หลายคนเข้าใจผิดว่าเป็นโค้ดที่ดีมีคุณภาพอ่านง่าย

```Ruby
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 ข้างในหน้าตาจะเป็นประมาณนี้


```Ruby
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 เป็นอะไร

แล้วมันไม่ดียังไงเหรอ?

ถ้่าสมมติมีคนถามว่า เราใช้เลขอะไรในการตัดยอดเงินรวมของบริษัท ผมอ่านจากโค้ดนี้ก้อนเดียวในคลาสนี้ ผมไม่อ่านตรงอื่นเลยนะ อ่านแค่ตรงนี้

```Ruby
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 ทั้งๆ ที่มันอาจจะไม่เกี่ยวกันเลยก็ได้


ซึ่งในกรณีนี้ ถ้าเราไม่พยายามทำให้มันดูเป็นประโยคภาษาอังกฤษมากเกินไป แต่ทำแบบนี้

```Ruby
 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 อะไรบ้าง

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

```Ruby
def decrease_company_total_balance(company, transaction)
  company.balance = company.balance - transaction.amount
  company.save()
end
```

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

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

การมี Method ที่ส่งต่อค่ากันผ่านการกำหนดค่า Field ใน Object นั้นทำให้
1. ไล่ตามยากว่า Method นี้มี Input space ที่เป็นไปได้อย่างไรบ้าง
2. ย้าย Method ออกจาก Object ยากมาก

## แล้วเมื่อไหร่ที่คุยกันผ่าน Field ล่ะ

สำหรับผมกฎง่ายๆ ที่ทำให้ Field ทุก Field ใน Object จะต้องรับมาจากระบบภายนอกเท่านั้น

รับมาบันทึกเลยหรือรับมาคำนวนบางอย่างก่อนใส่ก็ได้ทั้งนั้น

เช่น
```Ruby
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 ก็เข้าใจได้ เพราะต่อให้เราเขียน

```Ruby
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 มีผลต่างกันยังไง แต่อันนี้ขอบ่นคร่าวๆ ก่อนครับ)