主流的編程語(yǔ)言中,表示出現(xiàn)錯(cuò)誤的手段不外乎兩種:
- 函數(shù)調(diào)用返回錯(cuò)誤碼
- 函數(shù)調(diào)用拋出異常
C語(yǔ)言就屬于前者,它的fopen(3)函數(shù)在成功打開文件時(shí)返回一個(gè)FILE指針,失敗時(shí)返回NULL,并將錯(cuò)誤代碼寫入全局變量errno;Ruby語(yǔ)言屬于后者,它的File.open方法在找不到文件時(shí)拋出ENOENT的異常,在File.open之外包裹一層begin...rescue...end即可捕捉其拋出的異常。
Common Lisp的錯(cuò)誤機(jī)制叫做狀況系統(tǒng)(Condition System),與異常機(jī)制相似,可以實(shí)現(xiàn)拋出異常(使用函數(shù)error)和捕捉異常(使用宏handler-case)。但與其它語(yǔ)言的異常機(jī)制不同的地方在于,狀況系統(tǒng)有一種名為RESTART的特性,能夠由使用者、或者由代碼決定是否要、以及如何從錯(cuò)誤中恢復(fù)過(guò)來(lái)。聽起來(lái)很別致也很詭異,不妨繼續(xù)往下看。
假設(shè)我有一個(gè)Web框架,它提供了將日志記錄到文件中的功能。這個(gè)功能需要先調(diào)用名為init-logger的函數(shù)進(jìn)行初始化,該函數(shù)實(shí)現(xiàn)如下
(defun init-logger ()
(with-open-file (s "/tmp/log/web.log"
:direction :output
:if-exists :supersede)
(format s "Logger module starting...")))
這個(gè)函數(shù)被框架的初始化代碼所調(diào)用,如下
(defun init-framework ()
(format t "Framework starting...~%")
(init-plugin))
(defun init-plugin ()
(format t "Plugins starting...~%")
(init-logger))
而框架的初始化代碼則由應(yīng)用的初始化代碼所調(diào)用,如下
(defun init-app ()
(format t "Application starting...~%")
(init-framework))
如果在目錄/tmp/log不存在時(shí)調(diào)用init-app函數(shù),我將會(huì)收到一個(gè)錯(cuò)誤,說(shuō)明其無(wú)法找到/tmp/log/web.log這個(gè)文件。為了避免錯(cuò)誤打斷了應(yīng)用的正常啟動(dòng)流程,可以讓init-app函數(shù)負(fù)責(zé)創(chuàng)建這個(gè)用于存放日志文件的目錄。將init-app函數(shù)改寫為如下形式
(defun init-app ()
(format t "Application starting...~%")
(ensure-directories-exist "/tmp/log/")
(init-framework))
盡管這種做法確實(shí)可行,但它導(dǎo)致應(yīng)用層的代碼(init-app函數(shù))必須了解位于底層的日志模塊的實(shí)現(xiàn)細(xì)節(jié)。直覺(jué)告訴我這樣子是不對(duì)的,框架的事情應(yīng)該由框架本身提供方案來(lái)解決。借助Common Lisp的restart功能,框架確實(shí)可以對(duì)外提供一種解決方案。
首先需要主動(dòng)檢測(cè)用于存放日志文件的目錄是否存在。借助UIOP這個(gè)包可以寫出如下代碼
(defun init-logger ()
(let ((dir "/tmp/log/"))
(unless (uiop:directory-exists-p dir)
(error 'file-error :pathname dir))
(with-open-file (s (format nil "~Aweb.log" dir)
:direction :output
:if-exists :supersede)
(format s "Logger module starting..."))))
接著,init-logger需要主動(dòng)為調(diào)用者提供目錄不存在的問(wèn)題的解決方案,一種辦法就是當(dāng)這個(gè)目錄不存在時(shí),可以由調(diào)用者選擇是否創(chuàng)建。使用Common Lisp的restart-case宏,我將init-logger改寫為如下形式
(defun init-logger ()
(let ((dir "/tmp/log/"))
(restart-case
(unless (uiop:directory-exists-p dir)
(error 'file-error :pathname dir))
(create-log-directory ()
:report (lambda (stream)
(format stream "Create the directory ~A" dir))
(ensure-directories-exist dir)))
(with-open-file (s (format nil "~Aweb.log" dir)
:direction :output
:if-exists :supersede)
(format s "Logger module starting..."))))
此時(shí)如果調(diào)用第一版的init-app函數(shù),那么init-logger仍將拋出異常(類型為file-error與之前一樣)并將我?guī)氲絊BCL(我用的是SBCL)的調(diào)試器中,但看到的內(nèi)容會(huì)稍有不同
error on file "/tmp/log"
[Condition of type FILE-ERROR]
Restarts:
0: [CREATE-LOG-DIRECTORY] Create the directory /tmp/log ;; <- 新增了這一行
1: [RETRY] Retry SLIME REPL evaluation request.
2: [*ABORT] Return to SLIME's top level.
3: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {1001F958A3}>)
在Restarts中新增了名為create-log-directory的一項(xiàng),這正是在init-logger中通過(guò)restart-case定義的新的”恢復(fù)措施“。我輸入0觸發(fā)這個(gè)restart,Common Lisp會(huì)回到它被定義的restart-case宏相應(yīng)的子句中執(zhí)行其中的表達(dá)式,也就是調(diào)用CL:ENSURE-DIRECTORY-EXIST函數(shù)創(chuàng)建/tmp/log。
如果總是希望執(zhí)行CREATE-LOG-DIRECTORY這個(gè)選項(xiàng)來(lái)創(chuàng)建存放日志文件的目錄,可以直接在代碼中指定,只需要配合使用Common Lisp的handler-bind和invoke-restart函數(shù)即可,最終init-app函數(shù)的實(shí)現(xiàn)如下
(defun init-app ()
(format t "Application starting...~%")
(handler-bind
((file-error #'(lambda (c)
(declare (ignorable c))
(invoke-restart 'create-log-directory))))
(init-framework)))