Babel Coder

[Design Pattern#2] Tell don't ask หลักการเพื่อลดความยุ่งยากในการคุมสถานะอ็อบเจ็กต์

beginner

 บทความนี้เป็นส่วนหนึ่งของชุดบทความ Principle และ Design Pattern สำหรับโลก OOP สมัยใหม่

บทความก่อนหน้านี้เราได้รู้จักหลักการ Law of Demeter ที่บอกเป็นหลักการกว้างๆไว้ว่า อ็อบเจ็กต์ควรเข้าถึงได้เพียงหน่วย (unit) ที่สัมพันธ์โดยตรงกับมัน ไม่อนุญาตให้เข้าถึง unit ที่ไม่เกี่ยวข้องโดยตรง วันนี้เราจะมาคุยกันในหลักการที่เรียกว่า Tell don’t ask ซึ่งเป็นหลักการที่ใกล้เคียงกับ Law of Demeter จากในบทความที่แล้วครับ

จัดการสถานะ (state) ของอ็อบเจ็กต์อื่นด้วยตนเองนั้นไม่ดี

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

ความเป็นจริงคือเราเพียงบอกพ่อครัวว่า ช่วยทำต้มยำกุ้งล๊อบสเตอร์ ขอแบบเผ็ดน้อยนะ แค่นี้ทุกอย่างก็เรียบร้อยแล้วใช่ไหมครับ? นั่นละฮะคือหลักการของ Tell don’t ask

หลักการของ Tell don’t ask คือเราอย่าถามอ็อบเจ็กต์ว่าสถานะของอ็อบเจ็กต์คืออะไร เราไม่นำสถานะของอ็อบเจ็กต์ที่ถามมาได้เพื่อไปคำนวณอะไรซักอย่าง แต่เราจะบอกอ็อบเจ็กต์เพื่อให้ทำสิ่งที่เราต้องการแทน ลองพิจารณาตัวอย่างต่อไปนี้ครับ

function getAddress(user) {
  // ถ้า user มี address ให้คืนค่ากลับเป็น address
  if(user.address) {
    return user.address
  } else {
    return 'No address'
  }
}

จากตัวอย่างข้างบนเพื่อนๆคงเห็นแล้วว่าฟังก์ชัน getAddress ของเราดึงสถานะหรือ state ของอ็อบเจ็กต์ user ออกมาเพื่อตรวจสอบว่ามี address หรือไม่ ถ้าไม่มีจะคืนค่ากลับเป็น No address วิธีการนี้นั้นขัดกับหลักการ Tell don’t ask นั่นเป็นเพราะเราถาม (ask) อ็อบเจ็กต์ user ถึงสถานะที่มันเก็บเอาไว้ ฉะนั้นแล้วจึงควรเปลี่ยนการถามเป็นการบอกอ็อบเจ็กต์ user ถึงสิ่งที่เราต้องการให้ทำแทนดังนี้

class User {
  getAddress() {
    return this.address || 'No address'
  }
}

function getAddress(user) {
  // เราไม่ ask เพื่อดึงสถานะออกมาจาก user 
  // แต่เราจะ tell คือบอก user ว่าเราต้องการ address
  // ที่เหลือเป็นหน้าที่ของ user ที่จะจัดการกับสถานะของ address ด้วยตัวมันเอง
  // โดยเราไม่ต้องรับรู้ถึงสถานะในตัวมันเลย
  return user.getAddress()
}

ลองดูอีกซักตัวอย่างครับ สมมติเรามีหน้า dashboard สำหรับทั้ง admin และผู้ใช้งานระบบทั่วไป เมื่อผู้ใช้งานระบบเข้ามาเราต้องทำการตรวจสอบก่อน หากเป็นแอดมินเราจะแสดงหน้า dashboard พิเศษสำหรับแอดมิน แต่หากไม่ใช่แล้วก็ให้แสดงหน้า dashboard สำหรับผู้ใช้งานระบบทั่วไปแทน

if(user.isAdmin) {
  render(user.getAdminDashboard())
} else {
  render(user.getUserDashboard())
}

วิธีข้างต้นยังคงขัดกับหลัก Tell don’t ask เพราะเราถามถึงสถานะความเป็นแอดมินของ user ก่อนจะตัดสินใจว่าจะเลือกเพจแบบไหนมาแสดงผลดี จึงควรเปลี่ยนจากการถามเป็นการออกคำสั่งแทนแบบนี้

// บอก user ว่าต้องการ dashboard นะ
// ที่เหลือเป็นหน้าที่ของ user เองที่จะจัดการกับสถานะของตนเองอย่างไร
render(user.getDashboard())

คำถามชวนคิด: บริษัทของเราต้องออกรายงาน (Report) ทุกๆปีโดยรายงานดังกล่าวประกอบด้วยรายชื่อลูกค้าทั้งหมด (Customer) และรายชื่อผู้จัดการสาขาทั้งหมด (Manager) เราจึงออกแบบระบบของเราดังนี้

class Customer {
  private name : string;
  // ถ้าเป็น true เราจะเปิดเผยข้อมูลส่วนตัวคือ tel ในรายงานไม่ได้
  private isPrivate : boolean;
  private tel : string;
  
  getName() : string {
    return this.name;
  }
  
  isPrivate() : boolean {
    return this.isPrivate;
  }
  
  getTel() : string {
    return this.tel;
  }
}

class Manager {
  private name : string;
  // เป็นผู้จัดการสาขาอะไร?
  private branch : string;
  
  getName() : string {
    return this.name;
  }
  
  getBranch() : string {
    return this.branch;
  }
}

class Report {
  private customers : Customer[];
  private managers : Manager[];
  
  constructor(customers : Customer[], managers : Manager[]) {
    this.customers = customers;
    this.order = managers;
  }
  
  getHeaders() { ... }
  getFooters() { ... }
  
  print() {
    // พิมพ์ส่วนหัวของรายงาน
    render(this.getHeaders())
    // วนลูปรอบ customers ทั้งหมดเพื่อพิมพ์ชื่อออกมาในรายงาน
    this.customers.forEach(
      (customer) => {
        if(customer.isPrivate()) {
          render(customer.getName())
        } else {
          render(`${customer.getName()} ${customer.getTel()}`)
        }
      }
    )
    // วนลูปรอบ managers ทั้งหมดเพื่อพิมพ์ชื่อและสาขาออกมาในรายงาน
    this.managers.forEach(
      (manager) => render(`${manager.getName()} ${manager.getBranch()}`)
    )
    // พิมพ์ส่วนท้ายของรายงาน
    render(this.getFooters())
  }
}

const report = new Report(customers, managers)
report.print()

จากตัวอย่างโค๊ดนี้จะพบว่าเมธอด print ของเราเข้าถึงข้อมูลของทั้ง customers และ managers แบบตรงๆเลย เราต้องรับรู้สถานะภายในของพวกมันเพื่อจะสามารถสร้างรายงานออกมาได้ถูกต้อง เพื่อนๆลองหาวิธีใช้หลักการ Tell don’t ask เพื่อเปลี่ยนโค๊ดข้างบนให้ดีขึ้นกันครับ ลองแชร์โค๊ดของเพื่อนๆไว้ในคอมเม้นใต้บทความได้นะครับ


แสดงความคิดเห็นของคุณ


SaLamTam4 เดือนที่ผ่านมา

บรรทัดที่ 52-56 ยุบรวมเป็น render(customer.getDetail()) แล้ว logic การเช็ค isPrivate ย้ายไปอยู่ในตัวของ customer.getDetail() เอง Manager ก็คล้ายๆกัน ประมาณนี้ปะครับ ?


New Singha Rayathong5 เดือนที่ผ่านมา

จากคำถามควรโยน param เข้าไปเหมาะสมไหมครับ