Method
結構可以定義同類型的資料,而同類型的資料通常又會有同類型的行為。因為 Go 有 Anonymous Function,有寫過 Javascript 可能第一個想到的解法是這樣:
結構可以定義同類型的資料,而同類型的資料通常又會有同類型的行為。因為 Go 有 Anonymous Function,有寫過 Javascript 可能第一個想到的解法是這樣:
Carbon 本身並不複雜,它使用兩個物件,分別繼承了原生 PHP [DateTime
][] 與 [DateInterval
][] 類別,並實作了新的行為,讓它更好使用。
以下會翻 Carbon 1.22.1 版來說做明。
在學 Design Pattern 時,常會聽到要「多用組合,少用繼承」。繼承這麼可怕,怎麼至今大多數語言都支援呢?這表示,繼承雖然有風險,但能避開風險的話,它仍然是個好用的觀念。像 Carbon 就是一個很好的例子,使用繼承擴展功能後,反而受到大多數開發者的喜愛。
我們這幾天可以一起來看看 Carbon 怎麼安全地實作繼承。
物件導向設計原則中,其中有一個原則是--里氏替換原則,身為一個子類,如果要繼承家業的話,必須要把父類原本做的事做好才行,就算有想要改善或是調整,也不能破壞行為。
所以,首先我們來看 Carbon
類別繼承了 [DateTime
][] 哪些實作,來了解它是改善調整,還是破壞行為。
使用 IDE 可以很清楚知道下面這些方法有做覆寫:
public function __construct($time = null, $tz = null) |
以下來看看這些方法到底做了哪些事:
__construct
這裡可以注意到,建構子中間多了 if
判斷;後面在傳 $tz
前,還有做一層手腳,把這兩部分拿掉的話,就跟原本的 DateTime
完全一樣了。
其中,if
判斷主要的任務是為了在測試階段時,要把「現在」替換成指定的時間點。而指定的時間是放在靜態變數裡,建構時再去取得靜態變數(getTestNow()
)。
這要怎麼用呢?比方說,我們要測試跨年前 10 秒會不會自動啟動煙火機制,直接使用 date
指令調電腦時間實在是太蠢了,來看看 Carbon 怎麼做:
$realNow = new Carbon(); |
輸出結果如下:
Real: 2017-12-22 18:51:32 |
這還有另一個更顯著的好處:測試時間再怎麼長,任何時間點拿的 now
都會是同一個時間。
$realNow = new Carbon(); |
輸出結果如下:
Real: 2017-12-22 18:55:44 |
如果程式需要依賴「現在」的話,將是非常好用的功能。
然而,它的啟動條件是先做設定「現在時間」(Carbon::setTestNow()
),啟動前並不影響任何行為;啟動後則是位移時間,最終還是會傳正確 $time
格式給父類別。
safeCreateDateTimeZone()
則是在做正規化 DateTimeZone
和一些 TimeZone 格式錯誤時的錯誤處理,而且避開了 Bug #52063。
因此這兩個功能都有加強原建構子的功能,並沒有破壞行為。
createFromFormat
開頭的 if
判斷和正規化 DateTimeZone
,與建構子 safeCreateDateTimeZone()
在做的事類似。
這邊會執行父類別建立 DateTime 的方法,接著 setLastErrors()
是存放建立時遇到的錯誤(parent::getLastErrors()
)。會這麼做的理由在最後面:因為 Carbon 設計這個 function 預期錯誤會丟例外,而不是 DataTime 回傳 false
。
如果是丟例外的話,需要有個地方取得錯誤訊息。是的,所以需要覆寫 getLastErrors
,來取得剛剛呼叫 setLastErrors()
時傳入的 parent::getLastErrors()
。這些過程有點繞,總之,Carbon 的目的是為了要把它改成「錯誤丟例外」。
如果 DateTime 成功建立,則會使用 instance()
轉換成 Carbon,再回傳出去。
原則上,這是一個工廠方法,所以回傳的物件應該會是 Class 本身,因此行為有點不同(回傳的是 Carbon 而不是 DateTime),但使用上並不會有任何影響。
getLastErrors
原始碼很單純,會這樣寫的理由請參考 createFromFormat
覆寫的原因。
實際會使用到的時機是在接 createFromFormat
方法所丟出的例外:
try { |
輸出如下:
A four digit year could not be found |
setDate
原始碼註解有提到,這裡是為了 workaround 修這個 Bug
setTimezone
正規化傳入的 Timezone,減少問題發生的機率。
modify
翻了一下 commit 記錄與 issue,這是為了修 DateTime 的 bug。
另外還有一個 CarbonInterval
類別繼承 [DateInterval
][]。它只有覆寫 __construct
的實作,換言之,它只改變了建立物件的方法,其他的行為都沒有改變。
由上面的分析看來,Carbon 並沒有改變原本物件的行為,因此我們甚至可以拿 Carbon 來取代任何需要使用 DateTime 的方法,達到里氏替換原則的精髓!
今天翻過它跟父類別,也就是跟原有功能有相關的程式,明天來看看擴充的功能有哪些,而這也正是 Carbon 吸引人的地方!
原文:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
時間旅行一直以來都是電影或動漫的經典主題;時間處理也是--它是程式語言的經典卡關問題。
Carbon 是 PHP 的第三方時間處理套件。它繼承了原生的 Datatime,並新增了許多語意化的行為,讓處理時間的難度降低許多。
比方說:想像自己是未來世界的特南克斯,乘坐的時光機程式是用 PHP 寫的,那該如何知道 20 年前的 timestamp 呢?
讓 Carbon 來處理就很簡單:
use Carbon\Carbon; |
或是維斯在黃金弗利沙毀滅地球的時候,決定出手倒退時光。那他怎麼定位出三分鐘前的時間點呢?
對 Carbon 來說只是小菜一碟:
use Carbon\Carbon; |
總括來說,Carbon 處理了下面的問題:
語意化的取值方法,如:
Carbon::now(); // 現在 |
語意化的比較方法,如:
$time1->lessThan($time2); // $time1 是否比 $time2 早 |
頭痛的時區問題
echo Carbon::now('Asia/Taipei'); |
明天就來看看 Carbon 是如何解決這些問題的。
Clients should not be forced to depend on methods that they do not use.
大家在使用 Laravel 或是其他套件時,相信都用的非常開心。
但是否有想過,為何這些套件會這麼好用?新增功能,加個檔案就行了;修改功能,加個檔案就行了;移除功能,改個設定就行了。怎麼會這麼簡單?
而為何自己做的共用套件卻是常常被人嫌?該要有的功能都實作出來了呀,一樣都是共用,為何命運大不同?
這些套件會有這麼多 Star,當然是有原因的。
首先,套件是需要精心設計的。它們會遵守物件導向設計原則,做出適合擴展的設計,大家才能順利寫出客製化功能。
再來,不僅要有設計,也要有夠完整的測試。測試除了測功能,確保套件行為正常之外,還會測「身為開發者,會如何使用程式」;同時,測試也會是最好的範例文件。
最後還要有簡單易懂的說明文件,才能讓路過的開發者,在最短的時間理解套件的功能,並可以知道套件是否適用於自己的專案上。
無論是設計、測試或是文件,都是針對多數開發者需求而做的,也因此,大家才能夠愉快地開發。身為一個開發者,先對所有開源作者致上十二萬分的敬謝之意。
往後的日子裡,會開始研究套件的設計,了解巨人的肩膀是如何實作出來的,期望自己在提升設計能力之後,有朝一日也能成為開源作者的一員。