รู้จักกับ Design Pattern - Behavioral (Part 3/3)
[ สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
Design Pattern Behavioral คืออะไร ?
Design Patterns Behavioral คือรูปแบบการออกแบบพฤติกรรม เป็นรูปแบบการออกแบบที่มุ่งเน้นไปที่ การสื่อสารและความสัมพันธ์ระหว่างวัตถุ (object) ต่างๆ ในระบบ Software รูปแบบเหล่านี้ช่วยให้นักพัฒนา Software สามารถออกแบบระบบที่ยืดหยุ่น เข้าใจง่าย และปรับเปลี่ยนได้ง่าย โดยไม่ต้องเขียน code ใหม่ทั้งหมดได้ ซึ่งใน Behavioral นี้มีทั้งหมด 10 รูปแบบได้แก่
- Chain of Responsibility รูปแบบการออกแบบเชิงพฤติกรรมที่ทำให้เราสามารถส่งต่อ request ต่างๆ ผ่านกลุ่มของตัวจัดการ (handler) โดยเมื่อได้รับ request มาแล้ว ตัวจัดการแต่ละตัวจะตัดสินใจว่าจะดำเนินการตามคำขอนั้นเลย หรือจะส่งต่อไปยังตัวจัดการถัดไปในกลุ่มได้
- Command รูปแบบการออกแบบเชิงพฤติกรรมที่เปลี่ยน request ให้เป็น object แบบ stand-alone ซึ่งภายใน object นั้ นจะมีข้อมูลทั้งหมดเกี่ยวกับ request เอาไว้ การเปลี่ยนแปลงนี้ทำให้เราสามารถส่ง request ในรูปแบบของ argument ของ method ได้ รวมถึงสามารถหน่วงเวลาหรือจัดการลำดับการทำงานของ request หรือแม้แต่ยกเลิกคำสั่ง (undo) นั้นได้ด้วย
- Iterator รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราสามารถสำรวจข้อมูลใน collection (ชุดข้อมูล) ได้ โดยไม่ต้องรู้โครงสร้างภายในของ collection นั้นว่าเป็นโครงสร้างแบบไหน (เช่น list, stack เป็นต้น)
- Mediator รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราลดความยุ่งยากในการพึ่งพากันระหว่าง object ต่างๆ โดยรูปแบบนี้จะจำกัดการสื่อสารโดยตรงระหว่าง Object ด้วยกันเอง และบังคับให้พวกมันทำงานร่วมกันผ่าน Object ตัวกลางที่เรียกว่า "mediator" เท่านั้น
- Memento รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราสามารถบันทึกและเรียกคืนสถานะก่อนหน้าของ Object ได้ โดยที่ไม่ต้องเปิดเผยรายละเอียดภายในของการทำงานได้
- Observer รูปแบบการออกแบบเชิงพฤติกรรมที่ให้เราสามารถสร้างกลไกการติดตาม (subscription) เพื่อแจ้งให้ Object หลายๆ ตัวทราบเกี่ยวกับ event ต่างๆที่เกิดขึ้นกับ Object ที่กำลังเฝ้าสังเกตอยู่ได้ (เป็นตัวที่คอยบอกตัวอื่นๆว่าตัวที่เฝ้าสังเกตอยู่มีบางอย่างเกิดขึ้น เพื่อให้จัดการ action ต่อได้)
- State รูปแบบการออกแบบเชิงพฤติกรรมที่ทำให้ Object สามารถเปลี่ยนแปลงการทำงานของตัวเองได้ เมื่อสถานะ (state) ภายในของมันเปลี่ยนไป ซึ่งภายนอกจะดูเหมือนกับว่า Object นั้นได้เปลี่ยนประเภท (class) ของตัวเองออกไปแทน (จัดการ state แบบ class)
- Strategy รูปแบบการออกแบบเชิงพฤติกรรมที่ให้เรากำหนดกลุ่ม algorithm ต่างๆ โดยแยกแต่ละ algorithm ออกเป็น class ของตัวเอง เพื่อให้ Object ที่ใช้ algorithm เหล่านี้สามารถสับเปลี่ยนกันได้
- Template Method รูปแบบการออกแบบเชิงพฤติกรรมที่กำหนดโครงร่างหลักของ algorithm ไว้ใน super class แต่เปิดให้ sub class ต่างๆ สามารถปรับแต่งขั้นตอนเฉพาะภายใน algorithm ของ sub class นั้นๆเองได้ โดยไม่ต้องเปลี่ยนแปลงโครงสร้างโดยรวมของ super class
- Visitor รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราแยก algorithm ออกจาก object ที่ algorithm นั้นจะทำงานด้วยอยู่
เราจะมาทำความรู้จักทีละ Pattern กันว่ามีลักษณะเป็นอย่างไรบ้างผ่าน Typescript เช่นเดิม (เช่นเดียวกับหัวข้อก่อนหน้านี้)
1. รูปแบบ Chain of Responsibility
Chain of Responsibility pattern เป็น behavioral design pattern ที่อนุญาตให้ object ส่งต่อ request ไปตาม chain ของตัวจัดการ (handler) เมื่อได้รับ request มา handler แต่ละตัวจะมีสิทธิ์ในการตัดสินใจได้ว่ามันจะทำการประมวลผล request นั้นด้วยตัวมันเองเลยหรือจะส่งต่อ request นั้นไปให้กับ handler ตัวต่อ ไปใน chain ต่อไปเรื่อยๆแทน
Pattern นี้จะลดการ decouple ระหว่าง ผู้ส่ง request กับตัวที่รับ request ซึ่งเกิดขึ้นด้วยการเปิดโอกาสให้ Object มากกว่าหนึ่งตัวสามารถจัดการกับ request นั้นได้ ส่งเสริมหลักการของ principle of least knowledge ตรงที่ลดการ coupling ระหว่าง Class ต่างๆลงได้
โครงสร้างหลักของ Chain of Responsibility pattern โดยทั่วไปจะประกอบไปด้วยส่วนประกอบเหล่านี้
- Handler (ผู้จัดการ) ทำหน้าที่นิยามส่วนติดต่อ (Interface) สำหรับจัดการกับ request และในบางกรณีก็จะทำการ implement ความเกี่ยวโยงระหว่าง handler ที่เป็น successor (ตัวที่ต่อกัน) ด้วย โดยที่ handler นี้มีสิทธิ์ที่จะตัดสินใจว่าจะจัดการกับ request ด้วยตัวของมันเอง หรือจะส่งต่อ request ไปให้ handler ตัวถัดไปใน chain ได้
- Concrete Handler (ผู้จัดการแบบเฉพาะเจาะจง) ทำหน้าที่จัดการกับ requests ที่ตัวมันเองมีหน้าที่รับผิดชอบ อีกทั้งยังสามารถเข้าถึง successor ของมันได้อีกด้วย ถ้า Concrete Handler ตัวนั้ นสามารถจัดการกับ request ได้มันก็จะทำ อันไหนทำไม่ได้มันก็จะส่งต่อ request นั้นให้ successor ของมันต่อไป
- Client (ผู้ใช้) จะทำการเริ่มต้น request โดยส่งไปที่ Object Concrete Handler ที่เกี่ยวข้องใน chain เข้าไป
ด้านล่างนี้เป็นตัวอย่างง่ายๆของ Chain of Responsibility pattern ตัวอย่างนี้จะจำลองระบบ logging system โดยที่ logger แต่ละตัวสามารถ log messages ที่มี levels ต่างกันออกไปได้ และถ้าระดับ severity ไม่อยู่ในความรับผิดชอบก็จะส่ง logs นั้นต่อให้กับ logger ตัวถัดไปใน chain ต่อไป
abstract class Logger {
protected level: number;
// Next element in the chain of responsibility
protected nextLogger: Logger;
constructor(level: number) {
this.level = level;
}
setNextLogger(nextLogger: Logger): void {
this.nextLogger = nextLogger;
}
logMessage(level: number, message: string): void {
if (this.level <= level) {
this.write(message);
}
if (this.nextLogger != null) {
this.nextLogger.logMessage(level, message);
}
}
protected abstract write(message: string): void;
}
class ConsoleLogger extends Logger {
constructor(level: number) {
super(level);
}
protected write(message: string): void {
console.log("Standard Console::Logger: " + message);
}
}
class ErrorLogger extends Logger {
constructor(level: number) {
super(level);
}
protected write(message: string): void {
console.error("Error Console::Logger: " + message);
}
}
class FileLogger extends Logger {
constructor(level: number) {
super(level);
}
protected write(message: string): void {
console.log("File::Logger: " + message);
}
}
// Setting up the chain
let loggerChain: Logger = new ConsoleLogger(1);
const errorLogger = new ErrorLogger(2);
const fileLogger = new FileLogger(3);
loggerChain.setNextLogger(errorLogger);
errorLogger.setNextLogger(fileLogger);
// Making requests
loggerChain.logMessage(1, "This is an information.");
loggerChain.logMessage(2, "This is an error message.");
loggerChain.logMessage(3, "This is a debug level message.");
ในตัวอย่างนี้ ConsoleLogger, ErrorLogger และ FileLogger แต่ละตัวจะทำการจัดการกับข้อความ (messages) ที่มีระดับ (level) ที่ต่างกันออกไปใน chain ที่ถูกสร้างขึ้นโดยเชื่อมโยงเข้าหากันตามลำดับ ข้อความหนึ่งข้อความอาจจะถูกจัดการโดย logger คนใดคนหนึ่งในนี้หรือไม่ก็จะถูกส่งต่อไปตาม chain จนกว่าจะมี logger ที่สามารถจัดการกับมันได้ โดยดูจาก level ของข้อความนั้น
loggerChainถูกเริ่มต้นโดยมีConsoleLoggerเป็น handler ตัวแรกใน chainConsoleLoggerจะมี next logger ของมันคือErrorLoggerและErrorLoggerมี next logger ของมันคือFileLoggerทำให้เกิดเป็น chain ที่ส่งต่อกันไปได้- เมื่อ
logMessageถูกเรียกที่loggerChainข้อความนั้นจะถูกส่งต่อไปตาม chain จนกว่า logger ตัวที่จะสามารถจัดการมันได้ (โดยดูจาก level) ทำการประมวลผลมัน ในกรณีที่ไม่มี logger ไหนเลยที่สามารถจัดการกับ level ของ messages ข้อความนั้นก็จะถูกส่งผ่านสายโซ่ไปโดยไม่ถูกทำอะไร
use case ตัวอย่างอื ่นๆ
- Authentication and Authorization Middleware ใน web application เราสามารถใช้ชุดของ middleware มาจัดการด้าน authentication และ authorization. โดยแต่ละ middleware จะคอยตรวจสอบส่วนต่างๆ (เช่น มี token ไหม, token ถูกต้องไหม, user มีสิทธิ์ในการทำสิ่งนั้นหรือไม่) และจะส่ง request นั้นไปในส่วนถัดไปถ้าทุกอย่างตรงตามเงื่อนไข
- Input Validation สำหรับ form ที่ซับซ้อนหรือ API request เราสามารถแบ่ง validation ออกเป็นหลายๆ handler โดยแต่ละ handler จะคอยตรวจสอบส่วนต่างๆ ของ input (เช่น ตรวจสอบความถูกต้องของรูปแบบ, ตรวจสอบ constraint, ตรวจสอบความสัมพันธ์ระหว่าง field ต่างๆ) และส่ง request ต่อไปถ้าหากการตรวจสอบทุกอย่างผ่านเรียบร้อย
- Request Handling in Web Servers ใน web server เราใช้ handler หลายๆ ตัวในการจัดการ request โดยแบ่งตาม path, method (GET, POST, ฯลฯ), และเกณฑ์อื่นๆ ทำให้เราสามารถแบ่งส่วนต่างๆ ให้ชัดเจนและดูแลรักษา codebase ได้ง่ายขึ้น
2. รูปแบบ Command
Command pattern คือ behavioral design pattern รูปแบบหนึ่งที่เปลี่ยน request ธรรมดา ให้กลายเป็น object แบบ stand-alone ซึ่งภายใน object นั้นจะมีข้อมูลต่างๆ ของ request เก็บเอาไว้ การที่เราแปลงแบบนี้ทำให้เราสามารถใส่ request ต่างๆ เข้าไปใน method, ทำการหน่วงเวลา หรือจัดคิวของ request ได้ รวมถึงยังทำให้เราสามารถมีระบบ undo ที่สามารถย้อนกลับการทำงานได้ด้วย
การใช้ pattern นี้จะมีประโยชน์อย่างมากในการสร้างระบบ transaction ต่างๆ, การทำ log และ สถานการณ์ที่เราต้องออก request โดยที่เราอาจไม่รู้รายละเอียดของ operation ที่ request นั้นร้องขอ หรือไม่รู้ว่าใครจะเป็น receiver ของ request ตัวนั้น pattern นี้ก็จะสามารถดำเนินการสร้างในรูปแบบ object ออกเป็น request ออกมาก่อนได้
โดยทั่วไป Command pattern จะมี component หลักๆ ดังนี้
- Command Interface interface ที่มีการประกาศ method ไว้สำหรับ execute command
- Concrete Command เป็นส่วนที ่ implement Command interface และมีการนิยามความสัมพันธ์ระหว่าง object Receiver กับ action ต่างๆ
- Client เป็นส่วนที่สร้าง object Concrete Command และทำการกำหนดว่า ใครจะเป็น receiver
- Invoker เป็นส่วนที่บอกให้ command ทำการ execute request
- Receiver เป็นส่วนที่รู้ว่าต้องทำ operation อะไรบ้าง ที่สัมพันธ์กับการทำงานของ request นั้น โดยเราสามารถใช้ class ไหนก็ได้มาเป็น Receiver
นี่คือตัวอย่าง code ของ command สำหรับการควบคุม Remote ไฟ
// Command Interface
interface Command {
execute(): void;
}
// Receiver
class Light {
turnOn(): void {
console.log("The light is on");
}
turnOff(): void {
console.log("The light is off");
}
}
// Concrete Command to turn on the light
class TurnOnCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOn();
}
}
// Concrete Command to turn off the light
class TurnOffCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOff();
}
}
// Invoker
class RemoteControl {
private command: Command;
setCommand(command: Command): void {
this.command = command;
}
pressButton(): void {
this.command.execute();
}
}
// Client
const light = new Light();
const turnOnCommand = new TurnOnCommand(light);
const turnOffCommand = new TurnOffCommand(light);
const remote = new RemoteControl();
remote.setCommand(turnOnCommand);
remote.pressButton();
remote.setCommand(turnOffCommand);
remote.pressButton();
จาก code ด้านบน
- Command Interface (
Command) ส่วนนี้เราจะแทนด้วย interface ชื่อ Command ซึ่งจะมี method execute อยู่ภายใน - Concrete Command (
TurnOnCommandและTurnOffCommand) class เหล่านี้จะมีการ implement Command interface และจะห่อหุ้ม (encapsulate) ทั้ง action และ receiver เอาไว้ (ในที่นี้คือ Light) - Receiver (
Light) class นี้จะมี operation จริงๆ อยู่ (turnOnและturnOff) ซึ่งก็คือสิ่งที่ command จะสั่งให้ทำงาน - Invoker (
RemoteControl) class นี้จะเป็นตัวที่ขอให้ command ทำ request นั้นๆ โดยเป็นผู้เรียกใช้ method execute - Client ส่วนของ code ที่ทำการสร้าง commands ต่างๆ ขึ้นมาและกำหนด receiver ให้กับ command ด้วย นอกจากนี้ยังเป็นส่วนที่สร้าง invoker และกำหนด commands ให ้กับ invoker ใช้อีกที
use case อื่นๆที่ใช้ได้
- Undo/Redo Operations ใช้สร้างระบบ undo/redo สำหรับการกระทำต่างๆ เช่นการแก้ไขข้อความ หรือการปรับแต่ง object ต่างๆ ใน web-based graphic design tool
- Operation Queueing ใช้ในการจัด queur task ต่างๆ เช่น API calls หรือการ file uploads เพื่อให้ทำงานตามลำดับ รวมถึงยังสามารถ retry หรือ cancel ได้ด้วย
- User Interface Actions ทำการ map action ของ user (เช่น คลิกปุ่ม, การเลือกเมนู) เข้ากับ command ต่างๆ ทำให้สามารถเปลี่ยน command ได้แบบ dynamic ตามสถานการณ์ของ application (เช่น การเปิด/ปิด feature ต่างๆ)
- Transactional Behavior ใช้ wrap action หลายๆ อย่างที่ต้องถูกทำงานไปพร้อมกัน ในรูปแบบ transaction ทำให้มั่นใจได้ว่าทุกอย่างจะสำเร็จหรือล้มเหลวไปพร้อมๆ กัน ซึ่งเหมาะกับการใช้ใน complex form submissions ที่มีหลายขั้นตอน
- Logging and Auditing เราสามารถ log command ได้ตอนที่ถูก execute ซึ่งจะทำให้เรามีบันทึก (audit trail) ของสิ่งที่ user ทำ เป็นประโยชน์อย่างม ากในการ debug และดูว่า user ใช้งานระบบอย่างไร
Command pattern ให้วิธีการที่ยืดหยุ่นในการ encapsulate method invocation, requests, หรือ operations ไว้ใน object เดียว ทำให้เราสามารถทำการกำหนดค่า (parameterization & configuration) ต่างๆ ให้กับ actions ในระบบที่ซับซ้อนได้
3. รูปแบบ Iterator
Iterator pattern คือ behavioral design pattern ที่ให้วิธีการเข้าถึง element ต่างๆ ภายใน aggregate object โดยทำทีละตัว (sequentially) และไม่จำเป็นต้องเปิดเผยว่าจริงๆ แล้วข้างในเป็นอย่างไร
Pattern นี้ทำให้เราสามารถเดินทะลุ (traverse) object นั้นๆ ได้โดยไม่ต้องรู้เลยว่าภายใน object มีการเก็บข้อมูลไว้ในรูปแบบไหน โดยตัว pattern นี้จะทำการแยก algorithm ออกมาจาก operation ที่ทำโดยตัว object ซึ่งทำให้ algorithm เราสามารถทำงานได้โดยไม่ต้องรู้เลยว่า object นั้นจัดเก็บข้อมูลหรือทำงานอย่างไรได้
ส่วนประกอบหลักๆ ของ Iterator pattern
- Iterator Interface นิยามว่าต้องมีรูปแบบการทำงานอย่างไรบ้าง ซึ่งจะรวมถึง method ไว้สำหรับการเข้าถึง element ณ ตอนนั้น, ย้ายไปที่ element ถัดไป, และเช็คว่ามี element เหลืออยู่ไหม
- Concrete Iterator implement Iterator interface และเป็นส่วนที่คอยเก็บ state ว่าตอนนี้การเดินทางไปถึงไหนแล้ว และมีหน้าที่จัดการตำแหน่งล่าสุดของการทำ iteration
- Aggregate Interface นิยาม interface สำหรับการสร้าง Iterator object
- Concrete Aggregate implement Aggregate interface และเป็นส่วนที่คืนค่า Concrete Iterator ที่สอดคล้องกันออกมา
ตัวอย่างนี้จะเป็นการสร้าง collection class ที่สามารถถูก iterate ออกมาได้
// Iterator Interface
interface Iterator<T> {
next(): T;
hasNext(): boolean;
}
// Aggregate Interface
interface Aggregate {
createIterator(): Iterator<any>;
}
// Concrete Aggregate
class Numbers implements Aggregate {
private collection: number[] = [];
constructor(collection: number[]) {
this.collection = collection;
}
createIterator(): Iterator<number> {
return new NumbersIterator(this);
}
}
// Concrete Iterator
class NumbersIterator implements Iterator<number> {
private collection: Numbers;
private position: number = 0;
constructor(collection: Numbers) {
this.collection = collection;
}
next(): number {
// Return the next element in the collection
return this.collection.collection[this.position++];
}
hasNext(): boolean {
// Check if there is a next element in the collection
return this.position < this.collection.collection.length;
}
}
// Using the iterator
const numbers = new Numbers([1, 2, 3, 4, 5]);
const iterator = numbers.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
จาก code ตัวอย่าง
- Iterator Interface (
Iterator<T>) ส่วนนี้จะถูก implement โดยNumbersIteratorซึ่งจะมี methodnext()และhasNext()ไว้ใช้สำหรับ action การ iterate ทีละ element - Concrete Iterator (
NumbersIterator) เป็นส่วนจัดการการ iterate ภายในNumberscollection และคอยเก็บว่าตอนนี้เรา iterate ไปถึงไหนแล้ว - Aggregate Interface (
Aggregate) ถูก implement โดยNumbersซึ่งจะเป็นตัวนิยาม method สำหรับการสร้าง Iterator (createIterator()) - Concrete Aggregate (
Numbers) เป็นส่วนเก็บ collection และเป็นตัว implement method ที่มีหน้าที่คืนค่า instance ของNumbersIteratorออกมา
use case ตัวอย่างอื่นๆ
- Pagination เวลาเราต้องแสดงผล dataset ขนาดใหญ่ (เช่น ผลการค้นหา หรือ รายชื่อผู้ใช้) เราสามารถใช้ Iterator pattern มาจัดการทำ pagination เพื่อแบ่งข้อมูลออกเป็นหน้าๆ ได้
- Social Media Feeds ในการ iterate เพื่อแสดง posts ต่างๆ ใน social media feed ตัว iterator จะช่วยลดความซับซ้อนในการ fetch post ใหม่แบบ dynamic ตอนที่ผู้ใช้ scroll ดึง post เพิ่มขึ้นมา
- Undo History in Text Editors เราสามารถเก็บ action ของผู้ใช้ (เช่น การแก้ไขข้อความ) ในรูปแบบ list แล้วใช้ iterator เดินหน้า ถอยหลังเพื่อสร้างฟังก์ชัน undo/redo
- Navigating Through a Playlist ในโปรแกรม media ต่างๆ การ iterate เพลงหรือวิดีโอใน playlist ทำให้เราเล่นวน หรือสุ่มเพลงได้โดยไม่ต้องเผยให้เห็นโครงสร้างข้อมูลที่ใช้เก็บ playlist จริงๆ
Iterator pattern เป็น pattern ที่ถูกใช้อย่างแพร่หลาย ด้วยจุดเด่นในการทำให้เราเดินทางผ่าน data structure ในหลากหลายรูปแบบ ซึ่งมีประโยชน์มากสำหรับการจัดการข้อมูลที่ซับซ้อนใน web application ออกมา
4. รูปแบบ Mediator
Mediator pattern คือ behavioral design pattern ที่จะมาช่วยจัดเตรียม interface แบบรวมศูนย์ สำหรับจัดการชุดของ interfaces อื่นๆ ใน subsystem โดย pattern นี้จะนิยาม object ตัวหนึ่งขึ้นมา เพื่อห่อหุ้ม (encapsulate) ว่าจริงๆ แล้ว object กลุ่มนั้นมีการพูดคุยกันอย่างไรได้บ้าง
ด้วยวิธีนี้เราจะทำให้เกิดการทำ loose coupling โดยที่ object เหล่านั้นไม่จำเป็นต้องอ้างอิงหากันตรงๆ ซึ่งตัว mediator จะทำหน้าที่เป็นตัวกลางในการควบคุมกลุ่ม object เหล่านั้น ในแง่ว่าจะ interact กันอย่างไรหรือตอนไหนออกมาได้
ส่วนประกอบหลักของ Mediator pattern มีดังนี้:
- Mediator Interface นิยาม interface ที่ใช้ในการสื่อสารกับ object Colleague ต่างๆ
- Concrete Mediator มีการ implement Mediator interface และทำหน้าที่ประสานงานการสื่อสารระหว่าง object Colleague ด้วยกัน โดยตัวนี้จะรู้จักและเก็บ reference ของ colleagues ต่างๆ เอาไว้
- Colleague (หรือ Participant) นิยาม interface ของการสื่อสารที่ต้องผ่านการจัดการโดย Mediator โดย Colleague แต่ละตัวจะรู้ว่า Mediator ของตัวเองคือใคร
- Concrete Colleague ตัวที่ทำให้ Colleague สามารถสื่อสารหากันได้ (แทนที่จะต้องไปคุยกันเอง)
ด้านล่างเป็นตัวอย่างในการเขียน Mediator pattern. ตัวอย่างนี้จะ focus ไปที่ use case ของ web application เกี่ยวกับ chat room
interface Mediator {
send(message: string, colleague: Colleague): void;
}
abstract class Colleague {
constructor(protected mediator: Mediator) {}
public abstract send(message: string): void;
public abstract receive(message: string): void;
}
class ConcreteMediator implements Mediator {
private colleagues: Colleague[] = [];
public register(colleague: Colleague): void {
this.colleagues.push(colleague);
}
send(message: string, colleague: Colleague): void {
this.colleagues.forEach((col) => {
if (col !== colleague) {
col.receive(message);
}
});
}
}
class User extends Colleague {
constructor(
mediator: Mediator,
public name: string,
) {
super(mediator);
}
public send(message: string): void {
console.log(`${this.name} sends message: ${message}`);
this.mediator.send(message, this);
}
public receive(message: string): void {
console.log(`${this.name} received message: ${message}`);
}
}
// Usage
const mediator = new ConcreteMediator();
const john = new User(mediator, "John");
const jane = new User(mediator, "Jane");
mediator.register(john);
mediator.register(jane);
john.send("Hi there!");
jane.send("Hey!");
จาก code ด้านบน
- Mediator Interface (
Mediator) จะถูก implement โดยConcreteMediatorและเป็นตัวที่มี methodsendไว้ใช้สำหรับสื่อสาร - Concrete Mediator (
ConcreteMediator) ทำหน้าที่จัดการการสื่อสารระหว่าง users (ซึ่งก็คือ object Colleague) จะมี method ชื่อregisterไว้เพิ่ม user คนใหม่เข้ามาใน chat room และใช้อีก method ชื่อsendในการกระจาย (broadcast) ข้อความไปให้กับทุกคนได้ - Colleague (
Colleagueabstract class) เป็นตัวแทน users ใน chat room โดย user แต่ละคนรู้แค่ว่า mediator ของตัวเองเป็นใครเท่านั้น และที่ตัว colleague class นี้จะมีการนิยาม methodsendและreceiveไว้ - Concrete Colleague (
Userclass) จะ implement การส่งและรับข้อความผ่านตัว mediator โดย user แต่ละคนจะส่งข้อความมาที่ chat room ได้ และตัว mediator จะทำการต่อไปยังคนอื่นๆ
use case ตัวอย่างอื่น
- Complex Form Interactions ใน web form ที่ state หรือการแสดงผลของ field บางตัวจะขึ้นกับค่าของ field อื่นๆ เราสามารถใช้ mediator ในการประสานงานตรงนี้ ซึ่งจะช่วยลด coupling ลงโดยที่ control ในฟอร์มตัวต่างๆ จะไม่ต้องยุ่งกันเอง
- Component Communication in Single Page Applications (SPAs) ในการทำ SPA ตัว mediator จะเป็นตัวกลางในการทำ communication ระหว่าง component ต่างๆ ซึ่งจะทำให้เราลด tight coupling และลด complexity ลง
- Workflow Management mediator สามารถกำหนดลำดับในการทำงาน ของแต่ละขั้นตอน workflow ได้ภายใน web application โดยที่ mediator จะคอยจัดการ transitions และ state ต่างๆ ซึ่งจะเปลี่ยนไปตาม action หรือ input ของ user
- Game Development ใน games เราใช้ Mediator pattern ในการจัดการ interactions ต่างๆ ระหว่าง entity ต่างๆ ในเกม เช่น ตัวผู้เล่น, ศัตรู, หรือ อุปสรรคต่างๆ ด้วยวิธีนี้จะทำให้เราจัดการ game logic และ game state ไว้ในที่เดียว
Mediator pattern นั้นจะเข้ามาช่วยลดความซับซ้อน ในการพัฒนา web application โดยความซับซ้อนที่ลดลงนั้นเกิดการที่มันรวมศูนย์ (centralize) logic ในการสื่อสารทั้งหมดเอาไว้ ทำให้ code เราเข้าใจง่ายขึ้น และ ดูแลต่อง่ายขึ้นเช่นกัน
5. รูปแบบ Memento
Memento pattern เป็น behavioral design pattern ที่ทำให้เราสามารถเปลี่ยน object ให้กลับไปเป็น state ก่อนหน้าได้ (เหมือน undo หรือ rollback) จุดประสงค์หลักของการใช้ pattern นี้คือการเก็บ (capture) และดึง internal state ของ object ออกมา เพื่อที่เราจะได้เอากลับมาใช้เปลี่ยน object นั้นกลับไปเป็น state นั้นใหม่อีกรอบได้ ในขณะที่เรายังสามารถคงคุณสมบัติ encapsulation ของ OOP เอาไว้ได้เช่นเดิม โดย pattern นี้มีประโยชน์อย่างมากในการทำพวก undo mechanism ใน application ต่างๆได้
ส่วนประกอบหลักของ Memento pattern มีดังนี้
- Originator object ที่ต้องการจะ save และเรียกคืน (restore) state ตัว Originator จะสร้าง memento ซึ่งจะมี snapshot ของ internal state ณ ตอนนั้นๆ อยู่ข้างใน และใช้เจ้า memento นี้ในการเปลี่ยน state ของตัวเองกลับไปเหมือนเดิม
- Memento object ที่คอยเก็บ state ของ originator เอาไว้ โดยปกติแล้วการที่ structure ข้างในเป็นอย่างไรนั้นจะถูกซ่อนเอาไว้ และจะมีแค่ originator เท่านั้นที่รู้
- Caretaker object ที่คอยเก็บ memento ไว้หลายๆ อัน โดยจะ คอยบันทึก state ของ originator ผ่าน memento แต่มันจะไม่ทำงาน หรือเข้าถึงข้อมูลภายใน memento (มองง่ายๆเหมือน History นั่นแหละ)
ด้านล่างเป็นตัวอย่างการใช้ Memento pattern ตัวอย่างนี้จะเหมาะกับ web application ที่เป็นพวก editor แล้วมี undo function อยู่
// Memento
class EditorMemento {
private state: string;
constructor(state: string) {
this.state = state;
}
public getState(): string {
return this.state;
}
}
// Originator
class Editor {
private content: string = "";
public setContent(content: string) {
this.content = content;
}
public getContent(): string {
return this.content;
}
public save(): EditorMemento {
return new EditorMemento(this.content);
}
public restore(memento: EditorMemento) {
this.content = memento.getState();
}
}
// Caretaker
class History {
private mementos: EditorMemento[] = [];
public addMemento(memento: EditorMemento) {
this.mementos.push(memento);
}
public getMemento(index: number): EditorMemento {
return this.mementos[index];
}
}
// Usage
const editor = new Editor();
const history = new History();
editor.setContent("First");
history.addMemento(editor.save());
editor.setContent("Second");
history.addMemento(editor.save());
editor.setContent("Third");
console.log(editor.getContent()); // Output: Third
// Undo to 'Second'
editor.restore(history.getMemento(1));
console.log(editor.getContent()); // Output: Second
// Undo to 'First'
editor.restore(history.getMemento(0));
console.log(editor.getContent()); // Output: First
จาก code ด้านบน
- Memento (
EditorMemento) class นี้จะคอยเก็บ state ของ Editor (ซึ่งก็คือ originator) โดยจะมี method ชื่อgetStateไว้ใช้ดึงค่า state ออกมา - Originator (
Editor) ClassEditorจะมี content ต่างๆ ที่จะถือว่าเป็น state ของมัน และสามารถสร้าง snapshot ของ state ตัวเองได้ผ่าน methodsave(โดยในนั้นจะมีการคืนค่าEditorMementoออกมา). นอกจากนี้ยังมี method restore สำหรับคืนค่า state ของตัวเองผ่าน memento ที่รับเข้ามาอีกด้วย - Caretaker (
History) ทำหน้าที่จัดการประวัติต่างๆ ของ memento โดยที่ History จะไม่แก้ไขข้อมูลที่อยู่ใน mementos แต่จะเก็บmementosเอาไว้ ซึ่งmementosที่ถูกเก็บพวกนี้ ก็คือ undo history ของเรานั่นเอง
use case อื่นๆ
- Undo Mechanism เราใช้ทำพวก undo functionality ได้ใน application ต่างๆ เช่น text editors, graphic editors, หรือพวก web form ที่ user สามารถกดเพื่อเปลี่ยนกลับไปเป็น state ก่อนหน้าของ document หรือ form นั้นได้
- Feature Toggle Management ใช้ capture และ restore state ของระบบในพวก feature toggle systems การทำแบบนี้ทำให้เราสามารถทำ rollback ได ้อย่างรวดเร็ว เพื่อเปลี่ยนกลับคืนไป stable state ในกรณีที่การที่เพิ่ม feature ใหม่เข้าไปเกิดทำให้เกิด bug
Memento pattern นั้นมีประโยชน์ในการสร้างความสามารถจัดการ state เพิ่มเติมให้กับ web applications ด้วยคุณสมบัติที่ทำให้เราสามารถจัดการ state และ undo ต่างๆ ได้
6. รูปแบบ Observer
Observer pattern เป็น behavioral design pattern โดยจะมี object หนึ่งตัวเป็น subject คอยเก็บ list ของตัว observer ไว้ เวลาที่มีการเปลี่ยนแปลง state ใดๆ subject จะทำการแจ้ง (notify) ไปยัง observers ทั้งหมดแบบอัตโนมัติ (โดยปกติแล้ววิธีการแจ้งจะเป็นการเรียก method ของ observers นั้นๆ ซึ่งนี่ถือเป็นรูปแบบพื้นฐานของ publish-subscribe เลยก็ว่าได้) เพื่อให้สามารถรับรู้ (observe) และตอบสนองกับเหตุการณ์ (event) ใน object อื่นๆ โดยที่ทั้งหมดนี้เราไม่จำเป็นต้องมา coupling ระหว่าง object เหล่านั้นเลยได ้ (ต่างคนต่างจัดการแค่คุยผ่านสัญญาณออกมาพอ)
ส่วนประกอบหลักของ Observer pattern มีดังนี้
- Subject เป็นตัวเก็บ list ของ observers ไว้ และจะมี method ไว้เพิ่ม ลบ และแจ้ง observers
- Observer จะเป็น interface หรือ abstract class ที่นิยาม method (ชื่อ update) เอาไว้ โดย update method นี้จะถูก subject เรียกเพื่อแจ้ง observers เวลามี state เปลี่ยน
- Concrete Subject จะ implement subject interface และเก็บ state ของตัวเอง ซึ่งในตอนที่มีการเปลี่ยนแปลง state นั้นเองที่จะมีการแจ้งไปยัง observers ทั้งหมด
- Concrete Observer จะ implement observer interface และเก็บ reference ของ subject เอาไว้ เพื่อที่ว่าตัวมันเองจะ update ตัวเองได้แบบอัตโนมัติ ตอนได้รับแจ้งว่า state มีการเปลี่ยนแปลง
ด้านล่างเป็นตัวอย่างการเขียน Observer pattern โดยในตัวอย่างนี้จะโฟกัสไปที่ web application use case ที่ใช้ทำพวก notification system ง่ายๆกัน
interface Observer {
update(subject: Subject): void;
}
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
class ConcreteSubject implements Subject {
private observers: Observer[] = [];
private state: number;
attach(observer: Observer): void {
const isExist = this.observers.includes(observer);
if (isExist) {
return console.log("Subject: Observer has been attached already.");
}
this.observers.push(observer);
}
detach(observer: Observer): void {
const observerIndex = this.observers.indexOf(observer);
if (observerIndex === -1) {
return console.log("Subject: Nonexistent observer.");
}
this.observers.splice(observerIndex, 1);
}
notify(): void {
console.log("Subject: Notifying observers...");
for (const observer of this.observers) {
observer.update(this);
}
}
public changeState(state: number): void {
this.state = state;
this.notify();
}
public getState(): number {
return this.state;
}
}
class ConcreteObserver implements Observer {
update(subject: Subject): void {
if (subject instanceof ConcreteSubject && subject.getState() < 3) {
console.log("ConcreteObserver: Reacted to the event.");
}
}
}
// Usage
const subject = new ConcreteSubject();
const observer1 = new ConcreteObserver();
subject.attach(observer1);
const observer2 = new ConcreteObserver();
subject.attach(observer2);
subject.changeState(2);
subject.changeState(3);
จากตัวอย่างด้านบน
- Subject (
ConcreteSubject) class นี้จะเก็บ list ของ observers และทำการแจ้งพวก observers โดยใช้วิธีเรียก method update และจะมี method ต่างๆ ไว้สำหรับการเพิ่ม (attach) หรือ เอาออก (detach) ของ observers - Observer (
ConcreteObserver) จะมีการ implementObserverinterface และจะเป็นตัวระบุว่าต้องมีการ update ตัวเองอย่างไร ตอนได้รับ notification จาก Subject - Concrete Subject (
ConcreteSubject) ในตัวอย่างนี้ classConcreteSubjectจะทำหน้าที่เป็น concrete subject โดยตรง ซึ่งมีหน้าที่จัดการ state ของตัวเองและแจ้งให้ observers รู้ในตอนที่ state นั้นเกิดการเปลี่ยนแปลง - Concrete Observer (
ConcreteObserver) เป็นตัวแทนของ concrete observer ที่มีหน้าที่ในการกระทำอะไรบางอย่างเพื่อตอบสนองการเปลี่ยนแปลงของ state ในตัว subject
ตัวอย่าง use case อื่นๆ
- Real-time Data Updates ใช้ทำ real-time updates ใน application ต่างๆ เช่นการแสดงผลการแข่งขันกีฬาแบบสดๆ แสดงราคาหุ้นใน stock market หรือแสดง streaming data ในรูปแบบต่างๆ ซึ่ง UI นั้นจะถูกอัปเดตแบบ real-time ตอนที่ข้อมูลใหม่เข้ามาได้
- Event Management Systems ใน web applications ที่เป็นแบบ event-driven หรือ message-driven เราสามารถใช้ Observer pattern แจ้งให้ components ต่างๆ รู้เวลาที่มี event ต่างๆ ที่เกิดขึ้นในระบบ เช่น user login/logout เพื่อใช้ในการอัปเดต UI ให้สอดคล้องกัน
- Notification Systems การสร้าง notification system ซึ่งจะเป็นระบบที่เวลา user กระทำ (action) อะไรบางอย่าง หรือมี event บางอย่างเกิดขึ้น เราจะต้องส่งข้อความแจ้งไปหา users หรือระบบอื่นได้
- WebSocket Communication สำหรับ applications ที่ใช้ WebSockets ในการส่ง server events ไปหา clients อันนี้ Observer pattern จะสามารถจัดการ subscriptions สำหรับ messages ในหลายๆ แบบ และทำการอัปเดต components ทางฝั่ง client ให้ตรงกับข้อความเหล่านั้นออกมาได้
Observer pattern สำคัญมากในการออกแบบระบบที่มีความยืดหยุ่นและมีความเป็น decoupled system ออกมา ซึ่งหมายความว่า objects สามารถโต้ตอบกันได้โดยไม่ต้องมี dependencies ต่อกันได้ ซึ่ งมีความสำคัญเป็นอย่างยิ่งกับการพัฒนา web application ในสมัยนี้
7. รูปแบบ State
State pattern เป็น behavioral design pattern ที่จะทำให้ object หนึ่งมีความสามารถในการเปลี่ยนแปลงพฤติกรรม เมื่อ internal state ของมันเปลี่ยน การเปลี่ยนแปลงนี้ ถ้าดูจากภายนอกแล้วจะเหมือนกับว่า object ตัวนั้นเปลี่ยน class ของมันไปเลยก็ว่าได้
จุดเด่นของ pattern นี้คือจะห่อหุ้ม (encapsulate) พฤติกรรมต่างๆ ของ function ชุดหนึ่งๆเอาไว้ให้ขึ้นกับชนิดของ state object ซึ่งก็คือตัวแทนของ state ณ ตอนนั้นๆ และสิ่งนี้เองที่ทำให้พฤติกรรมของ object สามารถเปลี่ยนแปลงได้แบบ dynamic ตาม state ออกมาได้
ส่วนประกอบหลักของ State pattern มีดังนี้
- Context เป็นตัวเก็บ instance ของ ConcreteState subclass ซึ่งจะนิยามว่าตอนนี้ state เป็นอะไร
- State Interface เป็นตัวนิยาม interface ที่ไว้ห่อหุ้ม พฤติกรรมต่างๆ ที่สัมพันธ์กับ state นั้นๆ ของ Context
- Concrete States implement State interface และจะมีการใส่ logic เข้าไป เพื่อให้ได้ action เฉพาะเจาะจงออกมา ซึ่งจะเห็นได้ว่าพฤติกรรมของ Context object นั้นจะเปลี่ยนไปตาม state object ที่เปลี่ยนแปลง
ด้านล่างเป็นตัวอย่างการเขียน State pattern สำหรับใช้ใน web application เกี่ยวกับสถานะของ user account (เช่น new, active, suspended, closed)
interface State {
handle(context: Context): void
}
class Context {
private state: State
constructor(state: State) {
this.transitionTo(state)
}
public transitionTo(state: State): void {
console.log(`Context: Transition to ${(<any>state).constructor.name}.`)
this.state = state
this.state.handle(this)
}
public request(): void {
this.state.handle(this)
}
}
class NewState implements State {
public handle(context: Context): void {
console.log('User account is in new state, activating now.')
context.transitionTo(new ActiveState())
}
}
class ActiveState implements State {
public handle(context: Context): void {
console.log('User account is active. Suspending account.')
context.transitionTo(new SuspendedState())
}
}
class SuspendedState implements State {
public handle(context: Context): void {
console.log('User account is suspended. Closing account.')
context.transitionTo(new ClosedState())
}
}
class ClosedState implements State {
public handle(context: Context): void {
console.log('User account is now closed.')
}
}
// Usage
const context = new Context(new NewState())
context.request() // The account will be activated
context.request() // The account will be suspended
context.request() // The account will be closed
จาก code ด้านบน
- Context (
Context) จะเก็บ reference หนึ่งตัวที่ชี้ไปยัง instance ของ State subclass ไว้ ซึ่ง reference นี้จะบอกให้เรารู้ว่าตอนนี้ Context อยู่ใน state อะไร และให้เราเปลี่ยน state ได้ โดยการเปลี่ยนตำแหน่งที่ reference นี้ชี้ไป นอกจากนี้ยังใช้ state ตรงนี้ทำงานโดยมอบหมาย (delegates) พฤติกรรมต่างๆ ไปยัง current state ได้ - State Interface (
State) จะถูก implement โดย classNewState,ActiveState,SuspendedState, และClosedStateซึ่งจะมีการสร้างเป็น common interface เอาไว้สำหรับให้ states ต่างๆ โดยสามารถทำการแก้ไขหรือ handle request ของ Context ได้ - Concrete States (
NewState,ActiveState,SuspendedState,ClosedState) เป็นตัวกำหนดว่าพฤติกรรมของ Context จะเป็นอย่างไร และทำหน้าที่ implement State interface โดย class แต่ละอัน จะมี method ชื่อ handle ไว้เพื่อเปลี่ยน state ไปเป็นอันใหม่และทำให้เกิดพฤติกรรมที่สัมพันธ์กับแต่ละ state
use case อื่นๆ
- Shopping Cart Checkout Process เป็นการจัดการ process ในการ checkout บน e-commerce application เช่น การเพิ่มของ, ตรวจสอบข้อมูลการจัดส่ง, การชำระเงิน, ดูข้อมูลก่อนยืนยัน และ สถานะ completed ตรงนี้แต่ละ state ก็จะมีขั้นตอนต่างๆ ที่ไม่เหมือนกัน โย pattern นี้ก็สามารถทำให้เราจัดการ state โดยที่มี function ที่ไม่เหมือนกันออกมาได้
- Game States การจัดการ state ต่างๆ ภายในเกม (เช่น menu, playing, paused, game over) ซึ่งแต่ละ state ก ็จะมีวิธีการทำงานและการเปลี่ยน state ที่แตกต่างกันไปได้
- UI State Management การเปลี่ยน elements ต่างๆ บน UI แบบ dynamic ซึ่งจะเปลี่ยนไปตาม current state ของ application (เช่น ปุ่ม enabled/disabled หรือจะให้ form fields อันไหนแสดงบ้างไม่แสดงบ้าง เป็นต้น)
- Workflow Processes ใช้ทำให้ workflows ต่างๆ เป็นแบบอัตโนมัติ เช่น document approval processes ที่จะมี documents อยู่ใน state ได้หลายแบบ (เช่น drafted, submitted, reviewed, approved, rejected) ซึ่งแต่ละ state ก็จะอนุญาตให้ทำ actions ต่างออกจากกัน pattern นี้ก็จะสามารถทำให้เราสามารถมี action ที่แตกต่างกันในแต่ละ state พร้อมกับเก็บ state เอาไว้ได้
State pattern ให้วิธีการที่ทำให้ object ตัวหนึ่งๆ สามารถเปลี่ยนพฤติกรรมตอน runtime ได้อย่างเป็นระเบียบ โดยไม่ต้องเขียน if else ออกมาเยอะแยะได้ ช่วยทำให้การ maintain code สามารถทำได้ง่ายขึ้นเช่นกัน