skip to content

รู้จักกับ Design Pattern - Behavioral (Part 3/3)

/ 24 min read

Share on social media

design-pattern-behavioral สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

Design Pattern Behavioral คืออะไร ?

Design Patterns Behavioral คือรูปแบบการออกแบบพฤติกรรม เป็นรูปแบบการออกแบบที่มุ่งเน้นไปที่ การสื่อสารและความสัมพันธ์ระหว่างวัตถุ (object) ต่างๆ ในระบบ Software รูปแบบเหล่านี้ช่วยให้นักพัฒนา Software สามารถออกแบบระบบที่ยืดหยุ่น เข้าใจง่าย และปรับเปลี่ยนได้ง่าย โดยไม่ต้องเขียน code ใหม่ทั้งหมดได้ ซึ่งใน Behavioral นี้มีทั้งหมด 10 รูปแบบได้แก่

  1. Chain of Responsibility รูปแบบการออกแบบเชิงพฤติกรรมที่ทำให้เราสามารถส่งต่อ request ต่างๆ ผ่านกลุ่มของตัวจัดการ (handler) โดยเมื่อได้รับ request มาแล้ว ตัวจัดการแต่ละตัวจะตัดสินใจว่าจะดำเนินการตามคำขอนั้นเลย หรือจะส่งต่อไปยังตัวจัดการถัดไปในกลุ่มได้
  2. Command รูปแบบการออกแบบเชิงพฤติกรรมที่เปลี่ยน request ให้เป็น object แบบ stand-alone ซึ่งภายใน object นั้นจะมีข้อมูลทั้งหมดเกี่ยวกับ request เอาไว้ การเปลี่ยนแปลงนี้ทำให้เราสามารถส่ง request ในรูปแบบของ argument ของ method ได้ รวมถึงสามารถหน่วงเวลาหรือจัดการลำดับการทำงานของ request หรือแม้แต่ยกเลิกคำสั่ง (undo) นั้นได้ด้วย
  3. Iterator รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราสามารถสำรวจข้อมูลใน collection (ชุดข้อมูล) ได้ โดยไม่ต้องรู้โครงสร้างภายในของ collection นั้นว่าเป็นโครงสร้างแบบไหน (เช่น list, stack เป็นต้น)
  4. Mediator รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราลดความยุ่งยากในการพึ่งพากันระหว่าง object ต่างๆ โดยรูปแบบนี้จะจำกัดการสื่อสารโดยตรงระหว่าง Object ด้วยกันเอง และบังคับให้พวกมันทำงานร่วมกันผ่าน Object ตัวกลางที่เรียกว่า “mediator” เท่านั้น
  5. Memento รูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้เราสามารถบันทึกและเรียกคืนสถานะก่อนหน้าของ Object ได้ โดยที่ไม่ต้องเปิดเผยรายละเอียดภายในของการทำงานได้
  6. Observer รูปแบบการออกแบบเชิงพฤติกรรมที่ให้เราสามารถสร้างกลไกการติดตาม (subscription) เพื่อแจ้งให้ Object หลายๆ ตัวทราบเกี่ยวกับ event ต่างๆที่เกิดขึ้นกับ Object ที่กำลังเฝ้าสังเกตอยู่ได้ (เป็นตัวที่คอยบอกตัวอื่นๆว่าตัวที่เฝ้าสังเกตอยู่มีบางอย่างเกิดขึ้น เพื่อให้จัดการ action ต่อได้)
  7. State รูปแบบการออกแบบเชิงพฤติกรรมที่ทำให้ Object สามารถเปลี่ยนแปลงการทำงานของตัวเองได้ เมื่อสถานะ (state) ภายในของมันเปลี่ยนไป ซึ่งภายนอกจะดูเหมือนกับว่า Object นั้นได้เปลี่ยนประเภท (class) ของตัวเองออกไปแทน (จัดการ state แบบ class)
  8. Strategy รูปแบบการออกแบบเชิงพฤติกรรมที่ให้เรากำหนดกลุ่ม algorithm ต่างๆ โดยแยกแต่ละ algorithm ออกเป็น class ของตัวเอง เพื่อให้ Object ที่ใช้ algorithm เหล่านี้สามารถสับเปลี่ยนกันได้
  9. Template Method รูปแบบการออกแบบเชิงพฤติกรรมที่กำหนดโครงร่างหลักของ algorithm ไว้ใน super class แต่เปิดให้ sub class ต่างๆ สามารถปรับแต่งขั้นตอนเฉพาะภายใน algorithm ของ sub class นั้นๆเองได้ โดยไม่ต้องเปลี่ยนแปลงโครงสร้างโดยรวมของ super class
  10. 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 ตัวแรกใน chain
  • ConsoleLogger จะมี next logger ของมันคือ ErrorLogger และ ErrorLogger มี next logger ของมันคือ FileLogger ทำให้เกิดเป็น chain ที่ส่งต่อกันไปได้
  • เมื่อ logMessage ถูกเรียกที่ loggerChain ข้อความนั้นจะถูกส่งต่อไปตาม chain จนกว่า logger ตัวที่จะสามารถจัดการมันได้ (โดยดูจาก level) ทำการประมวลผลมัน ในกรณีที่ไม่มี logger ไหนเลยที่สามารถจัดการกับ level ของ messages ข้อความนั้นก็จะถูกส่งผ่านสายโซ่ไปโดยไม่ถูกทำอะไร

Inherits
Inherits
Inherits
Logger
-level: number
-nextLogger: Logger
+setNextLogger(nextLogger: Logger) : : void
+logMessage(level: number, message: string) : : void
#write(message: string) : : void
ConsoleLogger
#write(message: string) : : void
ErrorLogger
#write(message: string) : : void
FileLogger
#write(message: string) : : void

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 ใช้อีกที

Implements
Implements
Uses
Uses
Uses
«interface»
Command
+execute() : : void
Light
+turnOn() : : void
+turnOff() : : void
TurnOnCommand
-light: Light
+execute() : : void
TurnOffCommand
-light: Light
+execute() : : void
RemoteControl
-command: Command
+setCommand(command: Command) : : void
+pressButton() : : void

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 ซึ่งจะมี method next() และ hasNext() ไว้ใช้สำหรับ action การ iterate ทีละ element
  • Concrete Iterator (NumbersIterator) เป็นส่วนจัดการการ iterate ภายใน Numbers collection และคอยเก็บว่าตอนนี้เรา iterate ไปถึงไหนแล้ว
  • Aggregate Interface (Aggregate) ถูก implement โดย Numbers ซึ่งจะเป็นตัวนิยาม method สำหรับการสร้าง Iterator (createIterator())
  • Concrete Aggregate (Numbers) เป็นส่วนเก็บ collection และเป็นตัว implement method ที่มีหน้าที่คืนค่า instance ของ NumbersIterator ออกมา

Implements
Implements
Creates
«interface»
Iterator<T>
+next() : : T
+hasNext() : : boolean
«interface»
Aggregate
+createIterator() : : Iterator<any>
Numbers
-collection: number[]
+createIterator() : : Iterator<number>
NumbersIterator
-collection: Numbers
-position: number
+next() : : number
+hasNext() : : boolean

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 และเป็นตัวที่มี method send ไว้ใช้สำหรับสื่อสาร
  • Concrete Mediator (ConcreteMediator) ทำหน้าที่จัดการการสื่อสารระหว่าง users (ซึ่งก็คือ object Colleague) จะมี method ชื่อ register ไว้เพิ่ม user คนใหม่เข้ามาใน chat room และใช้อีก method ชื่อ send ในการกระจาย (broadcast) ข้อความไปให้กับทุกคนได้
  • Colleague (Colleague abstract class) เป็นตัวแทน users ใน chat room โดย user แต่ละคนรู้แค่ว่า mediator ของตัวเองเป็นใครเท่านั้น และที่ตัว colleague class นี้จะมีการนิยาม method send และ receive ไว้
  • Concrete Colleague (User class) จะ implement การส่งและรับข้อความผ่านตัว mediator โดย user แต่ละคนจะส่งข้อความมาที่ chat room ได้ และตัว mediator จะทำการต่อไปยังคนอื่นๆ

Implements
Inherits
Aggregates
Uses
«interface»
Mediator
+send(message: string, colleague: Colleague) : : void
«abstract»
Colleague
-mediator: Mediator
+send(message: string) : : void
+receive(message: string) : : void
ConcreteMediator
-colleagues: Colleague[]
+register(colleague: Colleague) : : void
+send(message: string, colleague: Colleague) : : void
User
-mediator: Mediator
+name: string
+send(message: string) : : void
+receive(message: string) : : void

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) Class Editor จะมี content ต่างๆ ที่จะถือว่าเป็น state ของมัน และสามารถสร้าง snapshot ของ state ตัวเองได้ผ่าน method save (โดยในนั้นจะมีการคืนค่า EditorMemento ออกมา). นอกจากนี้ยังมี method restore สำหรับคืนค่า state ของตัวเองผ่าน memento ที่รับเข้ามาอีกด้วย
  • Caretaker (History) ทำหน้าที่จัดการประวัติต่างๆ ของ memento โดยที่ History จะไม่แก้ไขข้อมูลที่อยู่ใน mementos แต่จะเก็บ mementos เอาไว้ ซึ่ง mementos ที่ถูกเก็บพวกนี้ ก็คือ undo history ของเรานั่นเอง

Creates
Stores
EditorMemento
-state: string
+getState() : : string
Editor
-content: string
+setContent(content: string) : : void
+getContent() : : string
+save() : : EditorMemento
+restore(memento: EditorMemento) : : void
History
-mementos: EditorMemento[]
+addMemento(memento: EditorMemento) : : void
+getMemento(index: number) : : EditorMemento

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) จะมีการ implement Observer interface และจะเป็นตัวระบุว่าต้องมีการ update ตัวเองอย่างไร ตอนได้รับ notification จาก Subject
  • Concrete Subject (ConcreteSubject) ในตัวอย่างนี้ class ConcreteSubject จะทำหน้าที่เป็น concrete subject โดยตรง ซึ่งมีหน้าที่จัดการ state ของตัวเองและแจ้งให้ observers รู้ในตอนที่ state นั้นเกิดการเปลี่ยนแปลง
  • Concrete Observer (ConcreteObserver) เป็นตัวแทนของ concrete observer ที่มีหน้าที่ในการกระทำอะไรบางอย่างเพื่อตอบสนองการเปลี่ยนแปลงของ state ในตัว subject

Implements
Implements
Aggregates
«interface»
Observer
+update(subject: Subject) : : void
«interface»
Subject
+attach(observer: Observer) : : void
+detach(observer: Observer) : : void
+notify() : : void
ConcreteSubject
-observers: Observer[]
-state: number
+attach(observer: Observer) : : void
+detach(observer: Observer) : : void
+notify() : : void
+changeState(state: number) : : void
+getState() : : number
ConcreteObserver
+update(subject: Subject) : : void

ตัวอย่าง 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 โดย class NewStateActiveStateSuspendedState, และ ClosedState ซึ่งจะมีการสร้างเป็น common interface เอาไว้สำหรับให้ states ต่างๆ โดยสามารถทำการแก้ไขหรือ handle request ของ Context ได้
  • Concrete States (NewState, ActiveState, SuspendedState, ClosedState) เป็นตัวกำหนดว่าพฤติกรรมของ Context จะเป็นอย่างไร และทำหน้าที่ implement State interface โดย class แต่ละอัน จะมี method ชื่อ handle ไว้เพื่อเปลี่ยน state ไปเป็นอันใหม่และทำให้เกิดพฤติกรรมที่สัมพันธ์กับแต่ละ state

Implements
Implements
Implements
Implements
Current State
«interface»
State
+handle(context: Context) : : void
Context
-state: State
+constructor(state: State)
+transitionTo(state: State) : : void
+request() : : void
NewState
+handle(context: Context) : : void
ActiveState
+handle(context: Context) : : void
SuspendedState
+handle(context: Context) : : void
ClosedState
+handle(context: Context) : : void

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 สามารถทำได้ง่ายขึ้นเช่นกัน

8. รูปแบบ Strategy

Strategy pattern เป็น behavioral design pattern ที่ทำให้เราสามารถเลือกพฤติกรรมของ algorithm ตอน runtime ได้ โดยที่แทนที่เราจะ implement algorithm โดยตรง code strategry สามารถทำให้ code ได้รับ instruction ตอน runtime ว่าจะต้องใช้ algorithm ไหนในกลุ่มของ algorithms ทั้งหมดที่เรามีได้ (อารมณ์เตรียมวิธีการแต่ละแบบเอาไว้ แต่เรียกใช้จริงเป็นแบบไหนก็ขึ้นอยู่กับ case ตอนนั้น)

Pattern นี้จะนิยามกลุ่ม (family) ของ algorithms แล้วทำการห่อหุ้ม (encapsulates) แต่ละแบบแยกจากกัน และทำให้สามารถสลับเปลี่ยนกันได้ภายในกลุ่มเดียวกัน ซึ่งจุดเด่นของ Strategy ก็คือจะทำให้ algorithm ของเรานั้นเป็นอิสระจาก clients ที่ใช้มันอยู่ได้

ส่วนประกอบหลักของ Strategy pattern มีดังนี้

  • Context เป็นตัวที่จะเก็บ reference ที่ชี้ไปยัง concrete strategies ตัวหนึ่งๆ และ Context ตัวนี้จะติดต่อกับ strategy ที่มันชี้ไปถึงอยู่ ผ่าน strategy interface เท่านั้น
  • Strategy Interface เป็นตัวประกาศว่า algorithms อื่นๆ จะต้องมี interface แบบนี้ ซึ่ง Context จะใช้อันนี้ในการเรียก algorithm ที่นิยามไว้ใน Concrete Strategy
  • Concrete Strategies implement strategy interface และทำหน้าที่ implement ตัว algorithm จริงๆ เวลาที่ Context ต้องการใช้ algorithm ไหน จะต้องมาเรียกพวก concrete strategy ให้จัดเตรียมการทำงานของ logic ข้างในให้เรียบร้อย

ด้านล่างเป็นตัวอย่างการเขียน Strategy pattern สำหรับใช้ใน web application เกี่ยวกับวิธีการการส่ง notifications ในหลากหลายรูปแบบ (เช่น email, SMS, และ push notifications)

// Strategy Interface
interface NotificationStrategy {
send(user: string, message: string): void;
}
// Concrete Strategies
class EmailNotification implements NotificationStrategy {
send(user: string, message: string): void {
console.log(`Sending email to ${user} with message: ${message}`);
}
}
class SmsNotification implements NotificationStrategy {
send(user: string, message: string): void {
console.log(`Sending SMS to ${user} with message: ${message}`);
}
}
class PushNotification implements NotificationStrategy {
send(user: string, message: string): void {
console.log(`Sending push notification to ${user} with message: ${message}`);
}
}
// Context
class NotificationContext {
private strategy: NotificationStrategy;
constructor(strategy: NotificationStrategy) {
this.strategy = strategy;
}
public setStrategy(strategy: NotificationStrategy) {
this.strategy = strategy;
}
public notify(user: string, message: string): void {
this.strategy.send(user, message);
}
}
// Usage
const user = "John Doe";
const message = "Your order has been shipped!";
const context = new NotificationContext(new EmailNotification());
context.notify(user, message);
context.setStrategy(new SmsNotification());
context.notify(user, message);
context.setStrategy(new PushNotification());
context.notify(user, message);

จาก code ตัวอย่างนี้

  • Strategy Interface (NotificationStrategy) ส่วนนี้จะแทนด้วย NotificationStrategy ซึ่งจะมีการประกาศ method send เอาไว้ โดย method นี้จะต้องมีการ implement ใน concrete strategies ทุกๆ อัน
  • Concrete Strategies (EmailNotification, SmsNotification, PushNotification) Classes เหล่านี้จะ implement NotificationStrategy interface โดยแต่ละตัวจะแจ้งผู้ใช้ (notify) ต่างกันออกไป
  • Context (NotificationContext) Class นี้จะเก็บ reference เอาไว้ ซึ่งจะชี้ไปยัง strategy object และทำการมอบหมายให้ method notify นั้นไปทำงานบน strategy object ที่กำลังใช้อยู่ได้ ซึ่งมี method ชื่อ setStrategy ไว้เปลี่ยน strategy ตอนไหนก็ได้ตอน application runtime (ขณะใช้งานอยู่)

Implements
Implements
Implements
Uses
«interface»
NotificationStrategy
+send(user: string, message: string) : : void
EmailNotification
+send(user: string, message: string) : : void
SmsNotification
+send(user: string, message: string) : : void
PushNotification
+send(user: string, message: string) : : void
NotificationContext
-strategy: NotificationStrategy
+constructor(strategy: NotificationStrategy)
+setStrategy(strategy: NotificationStrategy) : : void
+notify(user: string, message: string) : : void

use case อื่นๆที่สามารถใช้ได้

  • Form Validation เราสามารถใช้ validation strategies ชนิดต่างๆ กันไปกับ forms ตามสถานการณ์ต่างๆได้ (เช่น registration form, checkout form) โดยที่ไม่ต้องเปลี่ยนแปลง form logic ที่อยู่ข้างในได้
  • Payment Processing การเลือก payment processing algorithms ที่ขึ้นกับว่าผู้ใช้เลือกใช้วิธีใหน (เช่น credit card, PayPal, cryptocurrency)
  • Content Rendering การเปลี่ยน content rendering strategies ใน web application ได้แบบ dynamic ซึ่งสามารถใช้ algorithms หลากหลายแบบได้สำหรับการแสดงผล เช่น charts หรือ reports
  • Compression การนำ compression algorithms ต่างๆ มาใช้กับ web assets (เช่น images, scripts) ซึ่งจะขึ้นอยู่กับว่าตัว client นั้นรองรับอะไรบ้างได้

Strategy pattern จะสร้างความยืดหยุ่นให้เราได้สูง ในแง่ที่ว่าเราสามารถเลือกได้ตอน runtime ว่าจะให้ใช้ algorithms อันไหน ซึ่งทำให้ implementation ของ algorithm หลุดออกจาก code ส่วนที่ใช้ออกไปได้

9. รูปแบบ Template Method

Template Method pattern นั้นเป็น behavioral design pattern ที่เป็นตัวกำหนดโครงสร้าง (skeleton) ของ algorithm โดยโครงสร้างนี้จะอยู่ใน superclass แต่จะทำให้ subclasses สามารถแก้ไข (override) บางขั้นตอนที่เฉพาะเจาะจงใน algorithm นั้นได้ โดยที่จะไม่ไปเปลี่ยนแปลงโครงสร้างทั้งหมดออกมาได้

การใช้งานนี้ดีตรงที่เราจะสามารถนำ algorithm ตัวเดิมที่ไม่ได้ขึ้นตรงกับ Class โดยตรงกลับมาใช้ใหม่ได้ และมั่นใจว่าโครงสร้างทั้งหมดจะยังคงเหมือนเดิมได้ ในขณะที่เราสามารถปล่อยให้ subclasses นั้นๆ เข้ามาสร้างพฤติกรรมที่ตรงกับแต่ละขั้นตอนเพิ่มเติมได้

ส่วนประกอบหลักของ Template Method pattern มีดังนี้

  • Abstract Class (Template) จะนิยาม abstract methods ไว้สำหรับขั้นตอนต่างๆ ที่จะต้องมีใน algorithm นอกจากนี้ยังต้องมีการ implement template method ไว้ด้วยเพื่อกำหนดโครงสร้าง (skeleton) ของ algorithm ซึ่งจะมีการเรียก abstract methods ต่างๆ ที่ subclass จะต้องไป implement
  • Concrete Classes ทำการ implement abstract methods ต่างๆ ที่อยู่ใน superclass เพื่อเพิ่มการทำงานจริงที่อยู่ในแต่ละขั้นตอนของ algorithm เข้าไปได้ โดยที่โครงสร้างทั้งหมดและลำดับขั้นตอนจะถูกควบคุมอยู่ที่ superclass แทน

ด้านล่างเป็นตัวอย่างการเขียน Template Method pattern สำหรับใช้ใน web application เกี่ยวกับ report หลากหลายรูปแบบ (เช่น Sales Report, Inventory Report)

abstract class ReportTemplate {
// Template method
public generateReport(): void {
this.collectData();
this.analyzeData();
this.presentData();
}
protected abstract collectData(): void;
protected abstract analyzeData(): void;
protected abstract presentData(): void;
}
class SalesReport extends ReportTemplate {
protected collectData(): void {
console.log("Collecting sales data...");
}
protected analyzeData(): void {
console.log("Analyzing sales data...");
}
protected presentData(): void {
console.log("Presenting sales report...");
}
}
class InventoryReport extends ReportTemplate {
protected collectData(): void {
console.log("Collecting inventory data...");
}
protected analyzeData(): void {
console.log("Analyzing inventory data...");
}
protected presentData(): void {
console.log("Presenting inventory report...");
}
}
// Usage
const salesReport = new SalesReport();
salesReport.generateReport();
const inventoryReport = new InventoryReport();
inventoryReport.generateReport();

จาก code ด้านบน

  • Abstract Class (ReportTemplate) Class นี้จะเป็นตัวกำหนด method ชื่อ generateReport ซึ่งทำหน้าที่กำหนดขั้นตอนหลักๆ ของการสร้าง report และประกาศ abstract methods ชื่อ collectDataanalyzeData และ presentData ซึ่งจะต้องมีการไป implement ใน subclasses ที่สร้างต่อมา
  • Concrete Classes (SalesReport, **InventoryReport)** Classes เหล่านี้จะ implement abstract methods ต่างๆ ที่นิยามไว้ใน ReportTemplate เพื่อสร้างพฤติกรรมที่เฉพาะเจาะจงสำหรับการเก็บข้อมูล วิเคราะห์ข้อมูล และ นำเสนอข้อมูล โดยคำนึงถึง sales และ inventory (ตาม business case) ตามลำดับ

Inherits
Inherits
«abstract»
ReportTemplate
+generateReport() : : void
#collectData() : : void
#analyzeData() : : void
#presentData() : : void
SalesReport
#collectData() : : void
#analyzeData() : : void
#presentData() : : void
InventoryReport
#collectData() : : void
#analyzeData() : : void
#presentData() : : void

use case อื่นๆ

  • User Registration Processes users ที่มีหลายประเภท (เช่น regular users, admin users) อาจจะต้องมีการลงทะเบียนในรูปแบบที่ต่างกันออกไป โดย template method สามารถนิยามว่าขั้นตอนหลักๆ เป็นอะไรบ้าง และปล่อยให้ใน subclasses มีการใส่ implementation ที่เฉพาะเจาะจงของแต่ละ users ลงไปได้
  • Data Processing Pipelines สำหรับ applications ที่มีการ process data แบบหลาย steps (เช่น การดึงข้อมูล, การแปลงรูปแบบ, การจัดเก็บ) โดย template method จะสร้างโครงสร้างของ pipeline หลักๆ และปล่อยให้ใน subclasses ใส่ logic เฉพาะเจาะจงที่ขึ้นกับแต่ละประเภทของข้อมูลลงไป
  • Web Page Rendering ใน web application อาจจะมี pattern สำหรับการสร้างหน้าเว็บที่เหมือนๆ กัน (เช่น header, body, footer) แต่ว่าอาจจะต้องการให้ข้อมูลบนหน้าเว็บเปลี่ยนไปในแต่ละหน้า template method ก็สามารถกำหนดว่าลำดับในการจัดวางอะไรก่อนหลังเป็นอย่างไร ในขณะที่ subclasses สามารถใส่ logic เพิ่มเติมเพื่อแสดงหน้าเว็บที่ตรงตามเงื่อนไข

Template Method pattern จะส่งเสริมความสามารถในการนำ code กลับมาใช้ใหม่ได้เป็นอย่างดี และเสริมสร้างความยืดหยุ่น, ขยายได้, เพื่อให้นักพัฒนาสามารถใช้งานโครงสร้างของ algorithm เดียวกัน ในขณะที่สามารถปรับเปลี่ยนรายละเอียดภายในได้ เพื่อให้เหมาะสมกับเงื่อนไขต่างๆ หรือความต้องการเฉพาะออกมาได้

10. รูปแบบ Visitor

Visitor pattern เป็น behavioral design pattern ที่ทำให้เราสามารถเพิ่ม operation ใหม่ๆ เข้าไป object structures ที่เรามีอยู่แล้วได้ โดยที่เราไม่จำเป็นต้องไปแก้ไขตัว objects ใน structures ตัวนั้นๆได ซึ่ง pattern นี้จะมีประโยชน์ในตอนที่เราจะต้องนำ operation หลากหลายแบบ ที่แต่ละอันไม่ค่อยเกี่ยวกับกันโดยตรง มาทำงานบน set ของ objects ต่างๆ โดยที่เราไม่ต้องการจะเข้าไปเปลี่ยน classes ของ objects เหล่านั้นออกมาได้

ถ้าจะบอกเป็นหลักการคร่าวๆ คือ ตัว pattern จะมีการแยก algorithms ออกจาก objects ที่ตัว algortihms นั้นมีผลกระทบโดยตรงออกมานั่นเอง

ส่วนประกอบหลักของ Visitor pattern จะมีดังนี้

  • Visitor Interface จะประกาศ set ของ visiting methods สำหรับแต่ละ concrete element class ที่สามารถนำไป visit ได้ ปกติ method signature ของ visiting methods จะมีส่วนประกอบเป็นตัว element ตัวนั้นๆ เพื่อให้ตัว visitor เข้าถึง internal state ของมันได้
  • Concrete Visitor implement visiting method แต่ละอันตามที่นิยามเอาไว้ใน Visitor interface โดย operation แต่ละอันจะถูกนิยามแยกออกจากกันชัดเจนใน concrete visitor class แต่ละตัว
  • Element Interface ประกาศ accept method ที่จะรับ visitor object เข้ามาเป็น argument
  • Concrete Element implement accept method ที่นิยามไว้ใน Element interface โดยที่ method นี้จะทำหน้าที่เรียกอีก method ชื่อ visit ที่อยู่ข้างใน visitor object และส่งตัวเองเข้าไปเป็น argument แทนตัว visit เพื่อให้ visitor ได้เข้าถึง elements ที่อยู่ในตัวเองได้
  • Object Structure จะเป็น collection object ที่จะมี method เดินทางไปยังแต่ละ elements ซึ่งเป็นการสร้าง high-level interface ขึ้นมาเพื่อให้ visitor สามารถแวะเข้าไปทำตาม logic ตัวเองกับแต่ละ element ได้

ด้านล่างเป็นตัวอย่างการเขียน Visitor pattern สำหรับเคสที่อยากนำมาใช้ใน web application ในเคสนี้จะเป็นเกี่ยวกับ elements บนเว็บที่มีหลากหลายชนิด แต่ต้องการนำ SEO analysis และ social media analysis มาดำเนินการเพิ่ม โดยที่เราไม่ต้องการไปแก้ elements เหล่านั้น

interface Visitor {
visitText(text: TextElement): void;
visitImage(image: ImageElement): void;
}
interface Element {
accept(visitor: Visitor): void;
}
class TextElement implements Element {
constructor(public text: string) {}
accept(visitor: Visitor): void {
visitor.visitText(this);
}
}
class ImageElement implements Element {
constructor(
public src: string,
public altText: string,
) {}
accept(visitor: Visitor): void {
visitor.visitImage(this);
}
}
class SEOAnalyzer implements Visitor {
visitText(text: TextElement): void {
console.log(`Analyzing SEO for text: ${text.text}`);
}
visitImage(image: ImageElement): void {
console.log(`Analyzing SEO for image with alt: ${image.altText}`);
}
}
class SocialMediaAnalyzer implements Visitor {
visitText(text: TextElement): void {
console.log(`Analyzing social media impact for text: ${text.text}`);
}
visitImage(image: ImageElement): void {
console.log(`Analyzing social media impact for image: ${image.src}`);
}
}
// Usage
const elements: Element[] = [
new TextElement("Hello, world!"),
new ImageElement("logo.png", "Company Logo"),
];
const seoAnalyzer = new SEOAnalyzer();
const socialMediaAnalyzer = new SocialMediaAnalyzer();
elements.forEach((element) => {
element.accept(seoAnalyzer);
element.accept(socialMediaAnalyzer);
});

จาก code ด้านบน

  • Visitor Interface (Visitor) จะมี methods อยู่ข้างในที่สำหรับใช้ในการเข้าไป visit text และ image ของ elements
  • Concrete Visitor (SEOAnalyzer, **SocialMediaAnalyzer)** implement Visitor interface และเป็นที่เก็บ actions เฉพาะเจาะจงที่ต้องการเพิ่มในแต่ละ element
  • Element Interface (Element) ประกาศว่าต้องมี method ชื่อ accept อยู่ข้างใน
  • Concrete Element (TextElement, **ImageElement)** implement Element interface และแต่ละตัวจะมี method ชื่อ accept ที่จะเรียก method ของ visitor อีกที โดยเลือก method ให้ตรงกับชนิดของ elements
  • Object Structure ในตัวอย่างนี้คือ array ชื่อ elements จะถูกใช้เป็นตัว object structure ซึ่งจะมีไว้ให้ visitors แวะเข้าไปทำงานกับ elements ต่างๆ ที่อยู่ข้างในได้

Implements
Implements
Implements
Implements
Accepts
Accepts
«interface»
Visitor
+visitText(text: TextElement) : : void
+visitImage(image: ImageElement) : : void
«interface»
Element
+accept(visitor: Visitor) : : void
TextElement
+text: string
+accept(visitor: Visitor) : : void
ImageElement
+src: string
+altText: string
+accept(visitor: Visitor) : : void
SEOAnalyzer
+visitText(text: TextElement) : : void
+visitImage(image: ImageElement) : : void
SocialMediaAnalyzer
+visitText(text: TextElement) : : void
+visitImage(image: ImageElement) : : void

use case อื่นๆ

  • Rendering Different Types of Content ใช้ Visitor pattern ในการจัดการ render content ในรูปแบบต่างๆ (ข้อความ,รูป, วิดีโอ) ให้มีความแตกต่างกันออกไปบน devices ได้
  • Serialization ทำ serialize เพื่อแปลง complex object structure (เช่น document object model) ให้เป็นรูปแบบที่แตกต่างกันไป (XML, JSON) โดยที่เราไม่ต้องไปเปลี่ยน objects นั้นๆได้ สามารถทำได้โดยใช้ visitors ชนิดต่างๆ กัน implement เพิ่มเข้าไป
  • Applying Operations on Form Fields นำไปใช้ได้ในระบบ form management ในการ validate, sanitize, หรือ operations อื่นๆ บน form fields ประเภทต่างๆ (เช่น การใส่ข้อความ, การทำ check box, และ การกด dropdown) ทำให้เราสามารถเพิ่ม operation เข้าไปได้อย่างง่ายดาย

Visitor pattern เหมาะกับ operations ที่มีลักษณะที่ต้องนำไปใช้กับ Class ที่มีจำนวนมากและมีความแตกต่างกันออกไป โดยจุดเด่นของ Visitor pattern จะช่วยให้เกิดการแยกการทำงานออกจากกันได้ (separation of concerns) ซึ่งยิ่งจะทำให้ระบบนั้นๆ ยิ่งดูแลรักษา และต่อยอดได้ง่ายขึ้นเช่นกัน

สรุปหัวข้อทั้งหมด

และนี่คือ Design Pattern ทั้งหมด 3 ชุดใหญ่ Creational, Structural และ Behavioral

  • Creational pattern เกี่ยวข้องกับการสร้าง object เน้นการสร้าง object โดยไม่ต้องพึ่งพา class ของ object โดยตรง (Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton)
  • Structural pattern เกี่ยวข้องกับโครงสร้างของ object เน้นการจัดการความสัมพันธ์ระหว่าง object (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy)
  • Behavioral pattern เกี่ยวข้องกับพฤติกรรมของ object เน้นการกำหนดวิธีการสื่อสารและการทำงานร่วมกันระหว่าง object (Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor)

Design Pattern นั้นช่วยให้นักพัฒนาซอฟต์แวร์สามารถออกแบบ code ได้อย่างมีประสิทธิภาพยิ่งขึ้น ช่วยให้โค้ดอ่านง่าย เข้าใจง่าย และดูแลรักษาง่าย รวมถึงช่วยลดจำนวน code ที่เขียนลงในแต่ละ use case ได้ ดังนั้นผู้อ่านควรศึกษาเพิ่มเติมเพื่อนำไปประยุกต์ใช้ต่อไปได้อย่างถูกต้องได้

Reference

https://refactoring.guru/design-patterns/behavioral-patterns

Related Post

Share on social media