iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Software Development

幫自己搞懂物件導向和設計模式系列 第 29

Command 命令模式

當一個請求 (request) 進入系統之後,通常我們就會立即的處理它。但如果我們不想這麼直接的去處理這些請求,而是先讓這些需求排隊、依序進入,甚至做一些預先處理,可以怎麼做呢?

在命令模式當中,我們將請求本身封裝成一個物件,並且將「接受請求」和「執行請求」兩件事情分開處理。

如果以現實生活中的例子來看,在比較有一點規模的餐廳當中,「處理客人點餐」和「實際準備餐點」是不同的人,譬如服務生 (server) 以及廚師 (chef)

在下面我們就以餐廳來當例子,這裡分別有

  • Order 介面:當中包含了 execute 方法,以及其他相關資訊,譬如餐點內容與指定廚師
  • AppetizerOrder, MainCourseOrder, DessertOrder 類別:分別將客戶點餐的需求包裝成物件
  • Chef 類別:實際執行需求(準備餐點)的角色
  • Server 類別:處理請求(處理點餐)的角色,過程中可以視情況拒絕某些請求

程式碼範例如下:

interface Order {
  chef: Chef
  food: string
  execute(): void;
}

class AppetizerOrder implements Order {
  chef: Chef
  food: string

  constructor(chef: Chef, appetizer: string) {
    this.chef = chef
    this.food = appetizer
  }

  execute(): void {
    this.chef.prepareAppetizer(this.food)
  }
}

class MainCourseOrder implements Order {
  chef: Chef
  food: string

  constructor(chef: Chef, mainCourse: string) {
    this.chef = chef
    this.food = mainCourse 
  }

  execute(): void {
    this.chef.prepareMainCourse(this.food)
  }
}

class DessertOrder implements Order {
  chef: Chef
  food: string

  constructor(chef: Chef, dessert: string) {
    this.chef = chef
    this.food = dessert
  }

  execute(): void {
    this.chef.prepareDessert(this.food)
  }
}

class Chef {
  name: string

  constructor(name: string){
    this.name = name
  }
  
  prepareAppetizer(appetizer: string): void {
    console.log(`Chef ${this.name} is preparing appetizer ${appetizer}`)
  }

  prepareMainCourse(mainCourse: string): void {
    console.log(`Chef ${this.name} is preparing main course ${mainCourse}`)
  }

  prepareDessert(dessert: string): void {
    console.log(`Chef ${this.name} is preparing dessert ${dessert}`)
  }
}

class Server {
  private orders: Order[] = []
  private chefs: { [key: string]: number | any } = {}

  constructor() {}

  addOrders(orders: Order[]): void {
    orders.forEach(order => {
      const { chef } = order

      if (this.chefs[chef.name] >= 3) {
        return console.log(`Sorry, chef ${chef.name} is too busy to take ${order.food} order`)
      } else {
        this.chefs[chef.name] = this.chefs[chef.name] ? this.chefs[chef.name] + 1 : 1
        this.orders.push(order)
      }
    })
  }

  execute(): void {
    this.orders.forEach(order => order.execute())
  }
}

介面和類別都建立好了之後,就讓我們來實際開一間餐廳吧!首先這裡有一位服務人員,和兩位廚師

const server = new Server()
const chefGordan = new Chef('gordan')
const chefRamsay = new Chef('ramsay')

接著,餐廳開門之後,陸陸續續有下面七個訂單(請求)出現

const order1 = new AppetizerOrder(chefGordan, 'salad')
const order2 = new AppetizerOrder(chefRamsay, 'edamame')
const order3 = new MainCourseOrder(chefRamsay, 'steak')
const order4 = new MainCourseOrder(chefRamsay, 'duck')
const order5 = new MainCourseOrder(chefRamsay, 'chicken')
const order6 = new DessertOrder(chefGordan, 'cake')
const order7 = new DessertOrder(chefRamsay, 'ice cream')

最後,服務人員收集完訂單之後,就可以讓廚師們開始執行任務

server.addOrders([order1, order2, order3, order4, order5, order6, order7])
server.execute()

不過為了不讓廚師太過操勞,因此服務人員會追蹤廚師目前的工作量,如果超過一定數量,就會直接拒絕客人,不會讓這個請求進入到廚房去

所以最後的得到的結果會是

Sorry, chef ramsay is too busy to take chicken order
Sorry, chef ramsay is too busy to take ice cream order
Chef gordan is preparing appetizer salad
Chef ramsay is preparing appetizer edamame
Chef ramsay is preparing main course steak
Chef ramsay is preparing main course duck
Chef gordan is preparing dessert cake

優點與缺點

命令模式的優點在於,我們「接收請求」、「執行請求」兩個不同的關注點分離。而當我們有專門負責「接收請求」的角色出現之後,就可以在實際執行之前,預先做不同的處理,譬如

  • 安排請求處理的行程
  • 決定接受或拒絕請求
  • 決定重做或撤銷請求

不過當然缺點就是,程式碼會比原本的複雜許多囉


上一篇
Chain of Responsibility 責任鏈模式
下一篇
Observer 觀察者模式
系列文
幫自己搞懂物件導向和設計模式30

尚未有邦友留言

立即登入留言