Chain of Responsibility
December 31, 2021你想要設計日誌程式庫,先從簡單的開始,只有日誌層次、Logger
,進行日誌時,若 Logger
的日誌層級等於或小於 log
方法指定的層級,才會輸出日誌:
enum Level {INFO, WARNING, SEVERE};
class Logger {
private Level level;
Logger setLevel(Level level) {
this.level = level;
return this;
}
void log(Level level, String msg) {
if(this.level.compareTo(level) <= 0) {
out.printf("%s - %s: %s%n", level, LocalDateTime.now(), msg);
}
}
}
使用上很簡單,就是建個 Logger
後設定層級,在必要的地方使用 log
:
var logger = new Logger().setLevel(Level.WARNING);
logger.log(Level.WARNING, "警訊日誌");
訊息傳播/各自職責
接著新的需求加入了,日誌會有階層關係,例如,在 cc.openhome
套件的 Logger
物件,希望會是 cc.openhome.pattern
的 Logger
實例之父日誌物件,這時你修改了 Logger
:
class Logger {
private String name;
private Logger parent;
private Level level;
Logger(String name) {
this.name = name;
}
Logger setParent(Logger logger) {
this.parent = logger;
return this;
}
Logger getParent() {
return this.parent;
}
Logger setLevel(Level level) {
this.level = level;
return this;
}
void log(Level level, String msg) {
if(this.level.compareTo(level) <= 0) {
out.printf("%s %s - %s: %s%n",
name,
level,
LocalDateTime.now(),
msg
);
}
}
}
在需求中,具有父子階層關係的 Logger
,子 Logger
處理完日誌訊息後,日誌訊息要傳給父 Logger
,看看需不需要也輸出日誌訊息,為此,你先寫了個簡單的程式,設定父子關係並傳播日誌訊息:
var parent = new Logger("cc.openhome").setLevel(Level.WARNING);
var child = new Logger("cc.openhome.pattern")
.setLevel(Level.SEVERE)
.setParent(parent);
var msg = "嚴重訊息";
var level = Level.SEVERE;
child.log(level, msg);
if(child.getParent() != null) {
child.getParent().log(level, msg);
}
當然,Logger
絕對不會只有兩層,這時要考慮的是,該如何進行日誌傳播,以及在哪判斷日誌層級,你想了一下,既然 Logger
物件本身知道自身層級以及父 Logger
,相關邏輯放入 Logger
不就好了:
class Logger {
private String name;
private Logger parent;
private Level level;
Logger(String name) {
this.name = name;
}
Logger setParent(Logger logger) {
this.parent = logger;
return this;
}
Logger getParent() {
return this.parent;
}
Logger setLevel(Level level) {
this.level = level;
return this;
}
void log(Level level, String msg) {
if(this.level.compareTo(level) <= 0) {
out.printf("%s %s - %s: %s%n",
name,
level,
LocalDateTime.now(),
msg
);
if(getParent() != null) {
getParent().log(level, msg);
}
}
}
}
這麼一來,Logger
的日誌訊息,自然就可以依設定的關係進行傳播了:
var parent = new Logger("cc.openhome").setLevel(Level.WARNING);
var child = new Logger("cc.openhome.pattern")
.setLevel(Level.SEVERE)
.setParent(parent);
child.log(Level.SEVERE, "嚴重訊息");
當然,你還可以繼續完善這個 Logger
,例如設計一個 Logger.getLogger
,可以自動判斷指定的名稱,建立對應的 Logger
、設定父子關係什麼的,這是另一個故事了…
Java 的實現
Java 標準 API 的 java.util.logging
,其中就包含了 Chain of Responsibility 的實現,以完成日誌訊息的傳播與處理,只不過它的 Logger
要更複雜一些,可允許使用者設定 Handler
、Formatter
與 Filter
,實現各種日誌的輸入輸出、格式化,以及更複雜的訊息過濾。
如果你有寫過 Java 的 Web 容器應用程式,應該會聯想到 Filter
,確實地,它也是 Chain of Responsibility 的實現,而且 Filter
的 doFilter
傳入的 FilterChain
,允許自訂 Filter
時,決定要不要將請求傳播下去:
public class PerformanceFilter extends HttpFilter {
@Override
protected void doFilter(
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
long begin = System.currentTimeMillis();
chain.doFilter(request, response);
getServletContext().log("Request process in " +
(System.currentTimeMillis() - begin) + " milliseconds");
}
}
這是因為 FilterChain
的 doFilter
實作概念類似以下:
Filter filter = filterIterator.next();
if(filter != null) {
filter.doFilter(request, response, this);
}
else {
targetServlet.service(request, response);
}
像這類物件彼此會有連結關係,有訊息時就依連結關係傳播給各物件,讓各物件自行決定處理方式的概念,Gof 稱為 Chain of Responsibility。
彼此之間的連結關係,是視各自需求而定,有可能像是 Logger
的樹狀階層,也有可能是類似 Filter
的線性關係,這影響的只是走訪各物件時的方式,重要的是,各物件只處理份內之事,要說原則的話,大概是以 single responsibility 的角度來思考。
這類設計可讓客戶端在使用 Logger
、自訂日誌層級、Handler
等時,或者在 Web 容器中自定 Filter
這類物件時,可以有機會自掃門前雪;相對地,這也表示,自訂這類物件時,不要讓他們有什麼相依性,像是處理日誌時設定 Handler
時,別去管父 Logger
怎麼處理,或在 Web 容器中的 Filter
,最好設計上各自獨立,別去考慮什麼前後順序關係之類的。