今天要介紹的 pattern 是 Strategy Pattern。跟昨天的 Template Pattern 一樣,個人覺得在 design patterns 中,Strategy Pattern 也是數一數二常用卻不自知的 pattern 了。
例如,你今天要做一個購書網站,假設你發現不同語言的使用者有很不一樣的習慣,像是中文使用者喜歡買書店推薦的書,而英文使用者喜歡購買新出的書。這時候根據使用者的語言去排序首頁顯示的書籍,就是一個很重要的功能,也是一個很適合使用 Strategy Pattern 的情境。
Strategy Pattern 的概念是先定出多種不同做法之間共通的 interface,把不同的做法變成一個一個獨立的 strategy class,然後再根據需求去決定現在要使用哪一個 strategy 並放到 context 裡–因為現在所有 strategy 都有一個共通的 interface,所以 context 可以無痛地切換不同的 strategy。
因此 Strategy Pattern 特別適合用在:當你有多種方式可以去做某件事,但是你要根據某些條件,去選擇某一種方式的時候。
使用 Typescript,以剛剛的書店為例,我們原本的程式碼可能像這樣。
class Book {
title: string;
recommended: boolean;
publishedDate: Date;
constructor(title: string, recommended: boolean, publishedDate: Date) {
this.title = title;
this.recommended = recommended;
this.publishedDate = publishedDate;
}
}
class BookStore {
allBooks: Book[];
constructor() {
const book1 = new Book('book 1', true, new Date('2010-01-01'));
const book2 = new Book('book 2', false, new Date('2019-09-17'));
this.allBooks = [book1, book2];
}
public showHomePage(userLanguage: string) {
const books = [...this.allBooks];
if (userLanguage == 'en') {
books.sort((bookA, _) => {
return bookA.recommended ? 1 : -1;
})
} else if (userLanguage == 'zh') {
books.sort((bookA, bookB) => {
return bookA.publishedDate > bookB.publishedDate ? 1 : -1;
})
}
console.log(books);
}
}
const bookStore = new BookStore()
console.log('zh')
bookStore.showHomePage('zh')
console.log('en')
bookStore.showHomePage('en')
// OUTPUT:
// zh
// [
// Book {
// title: 'book 1',
// recommended: true,
// publishedDate: 2010-01-01T00:00:00.000Z
// },
// Book {
// title: 'book 2',
// recommended: false,
// publishedDate: 2019-09-17T00:00:00.000Z
// }
// ]
// en
// [
// Book {
// title: 'book 2',
// recommended: false,
// publishedDate: 2019-09-17T00:00:00.000Z
// },
// Book {
// title: 'book 1',
// recommended: true,
// publishedDate: 2010-01-01T00:00:00.000Z
// }
// ]
現在讓我們改用 Strategy Pattern 的方式來處理。首先我們把英文跟中文共通的功能抽出來當成 interface,也就是 sortBooks
,然後我們再建一個 context class 讓它可以支援不同的 strategy,並開放一個 public method 給其他的程式使用,以這個例子來說就是 homePageBooks
,然後我們的 showHomePage
就可以用 context object 的 homePageBooks
來決定要呈現的順序。
class Book {
title: string;
recommended: boolean;
publishedDate: Date;
constructor(title: string, recommended: boolean, publishedDate: Date) {
this.title = title;
this.recommended = recommended;
this.publishedDate = publishedDate;
}
}
// Strategy is the common interface for different algorithms.
interface Strategy {
sortBooks(books: Book[])
}
class EnglishStrategy implements Strategy {
public sortBooks(books: Book[]) {
books.sort((bookA, _) => {
return bookA.recommended ? 1 : -1;
})
}
}
class ChineseStrategy implements Strategy {
public sortBooks(books: Book[]) {
books.sort((bookA, bookB) => {
return bookA.publishedDate > bookB.publishedDate ? 1 : -1;
})
}
}
class DefaultStrategy implements Strategy {
public sortBooks(books: Book[]) {}
}
// Context is the object to be used by the client (BookStore).
class Context {
strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
public setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
public homePageBooks(allBooks: Book[]): Book[] {
const newOrder = [...allBooks];
this.strategy.sortBooks(newOrder);
return newOrder;
}
}
class BookStore {
allBooks: Book[];
constructor() {
const book1 = new Book('book 1', true, new Date('2010-01-01'));
const book2 = new Book('book 2', false, new Date('2019-09-17'));
this.allBooks = [book1, book2];
}
public showHomePage(userLanguage: string) {
let context: Context;
if (userLanguage == 'en') {
context = new Context(new EnglishStrategy());
} else if (userLanguage == 'zh') {
context = new Context(new ChineseStrategy());
} else {
context = new Context(new DefaultStrategy());
}
console.log(context.homePageBooks(this.allBooks));
}
}
const bookStore = new BookStore()
console.log('zh')
bookStore.showHomePage('zh')
console.log('en')
bookStore.showHomePage('en')
winston
是 Javascript 中有名的 logging 套件,我們可以用它來管理 log 要輸出到哪裡去,例如說 STDOUT 或是某個檔案或是資料庫等等,而這個支援多種輸出的功能,也就是使用了 Strategy Pattern 的概念。
Log 輸出的目的地,在 winston
中,稱作 transport。我們可以把 transport 想像成 Strategy Pattern 中的 strategy,而每個 transport 都要實作 log()
這個 method。當主程式要使用 winston
前,會設定需要的 transport。
例如說,我們現在想要把 log 輸出到 console,首先我們會先產生一個 logger object,logger object 就像是 strategy pattern 中的 context。再來我們會用 logger.add()
method 來加入 console transport,然後我們就可以用像是 logger.info()
的方式,來輸出 log。
const winston = require('winston');
const logger = winston.createLogger();
logger.add(new winston.transports.Console());
logger.info('An info level log message');
Strategy Pattern 的優點是可以讓我們在 runtime 時,自由地去切換需要的 strategy,提供更大的彈性。另外,因為不同的邏輯都各自存放在不同的 strategy class 裡面,所以也更好維護和擴充。
但是如果今天你的 strategy 其實只有少數幾個,而且也不太會變動的話,導入 strategy pattern 反而會讓你的程式變得更複雜,就像我們上面書店的例子一樣,導入 strategy pattern 之後,程式碼行數變成原本的兩倍多,所以這之間要如何權衡拿捏,就要看各個狀況而定。
Template Pattern 是透過 inheritance 的方式,產生不同的 subclass,來改變一個功能中的某些部分。Strategy Pattern 則是透過 composition 的方式,把不同的 strategy 放到 context 中,來改變一個功能中的某些部分。因此 Template Pattern 的改變是 class 層的變化也是靜態的,而strategy pattern 的改變是 object 層的變化,它是相對動態的,它可以在執行過程中再去改變。
作者:Maso