เนื้อแท้ของการ Implement Polymorphism in OOP (ภาค 2)

chrisza4

Chakrit Likitkhajorn

Posted on February 23, 2020

เนื้อแท้ของการ Implement Polymorphism in OOP (ภาค 2)

ตอนที่แล้วผมจบไว้ที่ผมบอกว่า If-else ทุกตัวสามารถแปลงเป็น Polymorphism ได้เสมอ และผมมองว่าการที่โปรโมตว่า If-else เป็น Smell ที่ควรใช้ Polymorphism แก้เป็นการโปรโมตที่อันตราย

ถามว่าทำไม

พอเรากำหนดแบบนี้แล้ว Polmyporphism อันตรายยังไงบ้าง เรามาดูเคสกันดีกว่า

สมมติว่าเราทำระบบระบบซื้อของเข้าบริษัท โดยมีเงื่อนไขง่ายๆ ว่า ราคาเกิน 50,000 บาท ต้องส่งข้อความเตือน CEO เวลา Approve

ถ้าเราใช้ If-else ง่ายๆ ก็จะได้แบบนี้

public class PurchaseOrder {
  public void Approve() {
    this.approveState = ApprovalState.Approved;

    if (this.total > 50000) {
      Notification.sendTo(UserGroup.CEO);
    }
  }
}

เราสามารถแปลง If-else ตรงนี้เป็น Factory, Polymorph ได้ด้วยวิธีนี้

public class PurchaseOrderApprover {
  public void Approve() {
    this.approveState = "Approved";
  }
}

public class HighValuePurchaseOrderApprover: PurchaseOrderApprover {
  public void Approve() {
    super.Approve();
    Notification.sendTo(UserGroup.CEO);
  }
}

public class PurchaseOrderApproverFactory {
  public static PurchaseOrderApprover GetApprover(PurchaseOrder po) {
    if (po.total > 50000) return new HighValuePurchaseOrderApprover();
    return new PurchaseOrderApprover();
  }
}

public class PurchaseOrder {
  public void Approve() {
    PurchaseOrderApproverFactory.GetApprover(this).Approve();
  }
} 

การอ่าน

ก่อนอื่นผมนิยามก่อนว่า การอ่านโค้ดนี่คือการอ่านให้คนอื่นฟัง การอ่านเพื่อการสื่อสารกับเพื่อนร่วมงาน

ถ้าวันนี้มีคนมาถามว่า ขั้นตอนการอนุมติ PO ของบริษัทเป็นอย่างไร เราต้องอ่านคลาส PurchaseOrderFactory และคลาส PurchaseOrderApprover ประกอบกัน ถึงจะตอบได้ว่า การอนุมัติเป็นอย่างไร

ซึ่งในวันนี้ที่เรามีการอนุมัติสองแบบ การต้องอ่านสองคลาสมาประกอบกันเพื่อตอบคำถาม มันทำให้เราตอบคำถามได้ยาก

ดังนั้นผมมองว่าในแง่การอ่าน ตอนนี้ If-else ง่ายๆ อ่านง่ายกว่า

แต่... ถ้าสมมติว่าเรามีวิธีการอนุมัติ 7 case แบบนี้

public class PurchaseOrderApproverFactory {
  public static PurchaseOrderApprover GetApprover(PurchaseOrder po) {
    if (po.total > 50000) return new HighValuePurchaseOrderApprover();
    if (po.items.include("specialItem")) return SpecialCase1Approver();
    if (po.RiskLevel == Level.High) return HighRiskPoApprover();
    if (po.RiskLevel == Level.Medium) return MediumRiskPoApprover();
    if (po.createdBy.authorityLevel == Level.SeniorManagement) return AutomaticApprover(;
    // ... ขี้เกียจคิดเพิ่มแล้ว
  }
}

เวลาที่คนมาถามเราว่า ระบบเราอนุมัติยังไงบ้าง แทนที่เราจะเล่าทั้งหมด เรามักจะตอบว่า "พี่ครับ มันมีทั้งหมด 7 กรณี ขึ้นอยู่กับราคา ความเสี่ยง และตัวของที่ซื้อ ของพี่เป็นแบบไหน"

ซึ่งจะเห็นว่ามันล้อกับโค้ดที่เขียนอยู่ตรงนี้เป๊ะๆ เลย

ในทางตรงข้ามในกรณีนี้ การที่เราเขียนด้วย if-else จำนวนมากๆ ในโค้ดหลัก มันทำให้เรามองไม่ออกว่ามีทั้งหมด 7 กรณีขึ้นอยู่กับอะไรบ้าง

เราก็จะเล่าได้แค่ว่า

"ผมขออ่านก่อนนะครับ อ้อ กรณีแรก บลาๆๆๆ กรณีสอง บลาๆๆๆๆ"
(ผ่านไป 5 นาที อีกฝั่งเริ่มหงุดหงิด)
"น้องพอๆ พี่ไม่สนใจกรณีที่น้องว่ามาเลย พี่มี PO แบบนี้ การอนุมัติทำงานยังไงบ้าง"
"เอิ่มมมมมมม ผมไม่แน่ใจว่าของพี่เข้า If ตัวไหน เดี๋ยวผมขอกลับไปไล่โค้ดก่อนนะครับ"
"..........."

ดังนั้น Polymorph ในแง่นี้มันช่วยให้เราอ่านได้ง่าย โดยที่ผมนิยามการอ่านว่า การอ่านให้คนที่ไม่เข้าใจโค้ดฟัง

จะเห็นว่ามันมีสมดุลของมันอยู่ ไม่ใช่กฎตายตัวว่า Polymorph อ่านง่ายเสมอ

คำถามง่ายๆ เลย คือ คุณน่าจะมีโอกาสสื่อสารเงื่อนไขแยกจากการทำงานจริงหรือเปล่า?

การแก้ไข

บนตัวอย่างนี้ การแก้ไขที่ทำได้ง่ายขึ้นคือ เพิ่มวิธีการ Approve แบบใหม่ อย่างเช่น สมมติว่าอยู่ดีๆ มีคนบอกว่า ถ้าใบสั่งซื้อส่งให้ Vendor คนนี้ เวลา Approve ให้เตือนพี่เอก (นามสมมติ) ด้วย เพราะพี่เอกสามารถต่อราคากับ Vendor เจ้านี้ได้

มันจะเห็นชัดเลยว่าทางที่เราจะไปคือ

  1. เพิ่มโค้ด if หนึ่งบรรทัดใน Factory ให้สร้าง Approver ตัวใหม่ สมมติว่า ชื่อ VendorSpecificPurchaseApprover โดยตั้งเงื่อนไขถ้าเป็น Vendor พิเศษทำแบบนี้
  2. เพิ่มโค้ดใหม่หมดที่ VendorSpecificPurchaseApprover โดยที่เราสามารถเพิ่มโค้ดใหม่ได้โดยไม่ต้องแคร์โค้ดเก่าเลย

(ซึ่งอันนี้จะตรงกับหลัก OCP - Open-closed principle)

ในขณะที่ถ้าเราเขียน if-else เราต้องหาให้ดีกว่า if ตัวนี้จะอยู่ตรงไหน แล้วมันจะยังไป Execute ตัว Approval Logic ที่เป็นมาตรฐานได้ถูกต้องมั้ย จะลงไปกระทบบรรทัดอื่นๆ หรือเปล่า

แต่การแก้ไขที่ทำได้ยากขึ้นล่ะ?

สมมติว่าเราบอกว่าอยากกรณีที่เงินเกิน 500,000 บาท ต้องได้รับการอนุมัติจากบอร์ดบริหารเกิน 3 คน และกรณีนี้ ไม่สามารถเปลี่ยนสถานะเป็น Approved ได้ทันที ต้องเป็น Pending ก่อน

ถ้าเราจะใช้กลยุทธ์เดิมในการแก้

  1. เพิ่มโค้ด if หนึ่งบรรทัดใน Factory ให้สร้าง Approver ตัวใหม่
  2. เพิ่มโค้ดใหม่หมดที่ตัว Approver ใหม่นี้

จะเห็นว่ามันไม่ได้ เพราะ Base class ยังเปลี่ยนสถานะของใบสั่งซื้อเป็น Approved อยู่เลย

ถ้าเราจะแก้ก็จะมีสองทางที่คิดออก

  1. เอา this.approveState = ApprovalState.Approved; ออกจาก Base class ไปเขียนใน 7 class ทั้งหมด 😱
  2. สร้าง Base class เปล่าๆ ที่ไม่ทำอะไรเลย แล้วก็มีอีก Class นึงที่เป็น Base ของ 7 ตัวเดิม (จะชื่ออะไรดีฟะถึงจะอ่านรู้เรื่อง) ให้ Inherit ตัว Base class เปล่าๆ ก็จะได้ประมาณนี้
public class PurchaseOrderApprover {
  public void Approve() {
    // No operation
  }
}

public class ImmediatePurchaseOrderApprover {
  public void Approve() {
    this.approvalState = ApprovalState.Approved;
  }
}

public class HighValuePurchaseOrderApprover: ImmediatePurchaseOrderApprover { }

public class LowValuePurchaseOrderApprover: ImmediatePurchaseOrderApprover { }

public class SuperHighValueOrderApprover: PurchaseOrderApprover { 
  public void Approve() {
    this.approvalState = ApprovalState.Pending;
  }
}

จะเห็นว่าเป็นการแก้ไขที่ลำบากมากๆ เมื่อเทียบกับตอนที่เขียน If ธรรมดา เพราะต้องปรับโครงสร้าง Inheritance Tree อีก ปรับยังไงให้สวย ตั้งชื่อยังไงอีก

แล้วถ้าการแก้ไขที่เข้ามามักจะเป็นลักษณะนี้ ผมก็ไม่เห็นว่าการฝืนไปใช้ Polymorphism จะมีประโยชน์อะไร เอาจริงๆ แค่เห็น Base class เป็น Class เปล่าก็แปลกแล้ว (ซึ่งตรงนี้จริงๆ เราใช้ Interface ช่วยได้ แต่ผมเอาให้เห็นภาพง่ายๆ ก่อนเลยไม่ไปไกลขนาดนั้น)

ในแง่ของการแก้ไข ผมพอจะสรุปเป็นหลักคร่าวๆ ได้ว่า

การใช้ Polymorphism ในการจัดการ If-else ทำให้การแก้ไขโค้ดโดยเพิ่มเคสทำได้ง่าย แต่การแก้ไขส่วนที่แชร์กันทำได้ยาก

ดังนั้นสุดท้ายคุณก็ต้องประเมินเอานั่นแหละว่า เราจะมีโอกาสแก้ไขแบบไหนมากกว่ากัน

ส่งท้าย

สุดท้ายผมเขียนยาวมา 2 ตอนนี้ ผมก็มีสิ่งหนึ่งที่ผมว่าคนที่อ่านมานานจนมาถึงตรงนี้ สามารถประยุกต์ใช้ได้ทันที และเป็นสิ่งที่ผมอยากเห็น

คือผมคิดว่าแทนที่เราจะ Code review กันแบบนี้

1

ผมอยากเห็นอะไรแบบนี้มากกว่า

2

สิ่งที่แตกต่างกันอย่างมากคือ แบบแรกเราออกแบบโดยที่คิดว่าสิ่งหนึ่งคือถูก สิ่งหนึ่งคือผิด

อย่างหลัง เราออกแบบโดยมีสติเข้าใจว่าเราต้องการอะไร มี Assumption อะไรอยู่เบื้องหลังการออกแบบโค้ด และพูดมันออกมาให้คนอื่นเข้าใจด้วย เพื่อจะได้ทำงานเป็นทีมได้ง่ายขึ้นอีกในอนาคต

ผมคิดว่าแนวทางหลังเป็นแนวทางที่ดีสำหรับคนที่รักการเขียนโค้ดที่ดูแล แก้ไข ต่อเติมได้ง่ายครับ

สุดท้ายจริงๆ คือผมขอกลับไปเรื่องที่บ่นว่า การโปรโมตว่า if-else มัน Smell ใน Object-oriented มัน อาจจะ Do more harm than good แปลว่าอะไร?

คือตรงนี้ ผมว่า คุณเข้าใจ Smell ว่า if-else มันมี Alternative มีวิธีอื่นในการเขียน นี่ดีครับ เป็นอะไรที่ผมสนับสนุนให้รู้ ผมเลยเห็นด้วยกับประโยคที่ว่า If-else เป็น Smell ให้ฉุกคิดว่า เขียนแบบนี้ดีหรือเปล่า หรือใช้วิธีอื่นดีนะ

แต่มันก็ไม่ได้แปลว่าต้องแก้ไขทุกรอบน่ะครับ หลายๆ แล้วพอใช้คำว่า Code smell หลายๆ คนก็เลยคิดว่า "เป็นบาป" และ "ต้องแก้ไขเสมอ" ซึ่งถ้าไปแบบนั้น ผมว่ามันทำให้โค้ดยิ่งพังหนักครับ


ปล. มีมิตรท่านหนึ่งทักมาว่า การเคลมว่านี่เป็นเนื้อแท้ของ Polymorph อาจจะไม่ถูกนักเพราะ Polymorphism มันมีวิธีมากกว่าการ Implement ด้วย Construct ของ Object-oriented language ล้วนๆ เพิ่มเติมที่นี่้

ซึ่งผมเห็นด้วย เลยขอเปลี่ยนชื่อบล็อกทีหลังเป็น "เนื้อแท้ของการใช้งาน Polymorphism in OOP" และขอบคุณ ณ ที่นี้ที่ทักมา

แต่ผมจะขอขยายความว่าที่ผมใช้คำว่า "เนื้อแท้" ไม่ได้แปลว่ามันคือ "ถูกต้องตามมาตรฐาน" ผมใช้คำว่า "เนื้อแท้" แทนคำว่า "สาเหตุเบื้องลึก" ของการใช้ Polymorphism ในภาษา Object-oriented ซึ่งสำหรับผม เทคนิคก็เรื่องหนึ่ง ก็ว่ากันไปตามภาษาและ Construct ที่มี แต่สาเหตุเบื้องลึกที่ทำให้มันมีประโยชน์มากพอที่ควรจะพูดถึงและควรจะรู้จักไว้ใช้งาน คือการที่มันอนุญาตให้เราอธิบายเรื่องราวในธุรกิจในมุมมองที่หลากหลายได้ครับ

💖 💪 🙅 🚩
chrisza4
Chakrit Likitkhajorn

Posted on February 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related