/ OOD

[365 วันแห่งโปรแกรม #day60] Chain of Responsibility Pattern

วันที่หกสิบของ ‪#‎365วันแห่งโปรแกรม กลับมาสู่เรื่องดีไซน์แพทเทิร์นอีกครั้งครับ วันนี้เราจะคุยเกี่ยวกับ Chain of Responsibility Pattern ซึ่งเป็นแพทเทิร์นในกลุ่มของ Behavioral pattern


Chain of Responsibility Pattern

แปลตรงตัวเลยครับแพทเทิร์นนี้ คือการประมวลผล event แล้วโยน (Chain) ให้คนอื่นต่อไปเรื่อยๆ

ตัวอย่างโดยทั่วไปของแพทเทิร์นนี้ก็คือการ log event ครับ event ที่เราจะ log นั้นมีอาจจะมีความสำคัญต่างกัน ดังนั้นเราก็อาจจะจัดเก็บหรือแสดงผลด้วยวิธีที่ต่างกัน เช่นถ้าเป็น info ให้พิมพ์อกมาทาง console อย่างเดียว ถ้าเป็น error ให้พิมพ์ลงไฟล์ด้วย

Implementation

ผมก็ขอใช้ตัวอย่างเรื่อง Logger นั่นแหละครับง่ายดี ในการ implement นั้นเราต้องสร้าง Abstraction ของ Logger ขึ้นมาก่อน แล้วก็ค่อย derive ไป implement เป็น logger แบบต่างๆ ดังแผนภาพ

Chain of Responsibility Pattern

ขั้นแรกเราก็สร้าง AbstractLogger ครับ เมธอดที่สำคัญคือ setNextLogger() ซึ่งใช้สำหรับเซ็ตว่า Logger ตัวถัดไปที่จะได้รับ event คือใคร

public abstract class AbstractLogger {
   public static int INFO = 1;
   public static int DEBUG = 2;
   public static int ERROR = 3;

   protected int level;

   //next element in chain or responsibility
   protected AbstractLogger nextLogger;

   public void setNextLogger(AbstractLogger nextLogger){
      this.nextLogger = nextLogger;
   }

   public void logMessage(int level, String message){
      if(this.level <= level){
         write(message);
      }
      if(nextLogger !=null){
         nextLogger.logMessage(level, message);
      }
   }

   abstract protected void write(String message);

}

จะเห็นว่าโค้ดข้างต้นเลือกใช้ abstract class ครับ แทนที่จะเป็น interface เนื่องจากว่าเมธอด setNextLogger() และ logMessage() นั้นมี implement แบบเดียวกันในทุก Logger เราจึงควรมา implement ที่นี่ที่เดียวไปเลย

ใน abstraction นี้มีการกำหนดค่าคงที่ INFO, DEBUG, ERROR ไว้เพื่อใช้แทนระดับความสำคัญของ event นั้นๆ และมีตัวแปร level สำหรับใช้เก็บว่า Logger นั้นๆ ใช้ handle event ที่มี level ต่ำสุดแค่ไหน เช่นบอกว่า ConsoleLogger จะ handle เฉพาะ event ระดับ INFO ขึ้นไป ก็ใส่ level = INFO (ในตัวอย่างจะใช้วิธี injection เพื่อกำหนดว่า Logger ไหนรับ event ตั้งแต่ level ไหน)

ต่อไปจะเป็นการสร้าง Logger สำหรับ Log ค่าขึ้นบน console ครับ

public class ConsoleLogger extends AbstractLogger {

   public ConsoleLogger(int level){
      this.level = level;
   }

   @Override
   protected void write(String message) {		
      System.out.println("Standard Console::Logger: " + message);
   }
}

เราก็แค่สร้าง Logger ขึ้นมาแล้วก็ implement เมธอด write() ว่าถ้า Logger ของเราจัดการ event นั้นได้ให้ทำอะไร ในที่นี้ก็คือพิมพ์ค่าออกมาบน console

ต่อไปก็สร้าง Logger ที่เหลือเลยครับ โดยทั่วไปจะเหมือนกันหมดเว้นแต่ เมธอด write ที่ทำงานไม่เหมือนกัน (ในตัวอย่างนี้ใน write() ของทุก Logger จะให้พิมพ์ค่าบน console และบอกด้วยว่าเป็น Logger ไหนที่รับ event นั้นไปทำ)

public class ErrorLogger extends AbstractLogger {

   public ErrorLogger(int level){
      this.level = level;
   }

   @Override
   protected void write(String message) {		
      System.out.println("Error Console::Logger: " + message);
   }
}


public class FileLogger extends AbstractLogger {

   public FileLogger(int level){
      this.level = level;
   }

   @Override
   protected void write(String message) {		
      System.out.println("File::Logger: " + message);
   }
}

เมื่อสร้าง Logger ครบแล้วตอนนี้ก็ถึงเวลาที่จะเอา Logger เหล่านี้ไปใช้ครับ

สมมติว่าเราจะให้ ConsoleLogger รับ event ตั้งแต่ level INFO, ErrorLogger รับ event ตั้งแต่ level ERROR, และ FileLogger รับ event ตั้งแต่ level DEBUG เราก็ต้องประกาศตัวแปรดังนี้

private static AbstractLogger getChainOfLoggers(){
    AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
    AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
    AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);
}

เสร็จแล้วก็เซ็ตครับให้ Logger ตัวไหนโยน event ต่อให้ใคร
private static AbstractLogger getChainOfLoggers(){
....
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
}

สุดท้ายก็คืนค่า Logger ตัวแรกที่จะเรียกเมื่อเกิด event กลับไปครับ

private static AbstractLogger getChainOfLoggers(){
    ....
    return errorLogger;	
}

หลังจากนั้นเวลาเอาไปใช้ก็เรียก getChainOfLoggers() เพื่อขอ Logger แล้วก็สั่ง logMessage() ได้เลย เช่น

AbstractLogger loggerChain = getChainOfLoggers();

loggerChain.logMessage(AbstractLogger.INFO, "This is an information.");

===output===

Standard Console::Logger: This is an information.

จะเห็นว่ามีแค่ ConsoleLogger ตัวเดียวที่ทำงานเพราะมันรับ Log ได้ตั้งแต่ level INFO ขึ้นไป
ต่อมาลอง Log ที่ระดับ DEBUG ครับ

loggerChain.logMessage(AbstractLogger.DEBUG, "This is an debug level information.");

===output===

File::Logger: This is an debug level information.
Standard Console::Logger: This is an debug level information.

จะเห็นว่าคราวนี้มี FileLogger เพิ่มเข้ามา เพราะ FileLogger รับ DEBUG ขึ้นไปได้ ส่วน ConsoleLogger ก็รับได้เพราะ event นี้สำคัญกว่า INFO

สุดท้ายเราลอง Log ที่ระดับ ERROR ครับ

loggerChain.logMessage(AbstractLogger.ERROR, "This is an error information.");

===output===

Error Console::Logger: This is an error information.
File::Logger: This is an error information.
Standard Console::Logger: This is an error information.

คราวนี้จะเห็นว่าทำงานทั้ง 3 อันเลยครับ เพราะทุกอันมี level ต่ำกว่าหรือเท่ากับ Error หมดเลย

ต่างจาก Decorator pattern ตรงไหน?

จะเห็นว่าทั้ง Chain of Responsibility Pattern และ Decorator pattern ต่างก็ทำให้เราสามารถ chain event ได้ แต่จริงๆ แล้ว Chain of Responsibility เน้นที่เรื่องของการ handle event ส่วน Decorator pattern เน้นที่การเพิ่ม Behavior อีกทั้งใน Chain of Responsibility เราสามาถ terminate chain ทิ้งตรงไหนก็ได้ แต่ใน Decorator pattern ทำไม่ได้

References

Chain-of-responsibility pattern - Wikipedia

Chain of Responsibility Pattern - TutorialsPoint

#‎day60 #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...