over 2 years ago

推薦序

人們常說,要改變一個人的行為,先改變其心態和思維。唯有發自內心的信念轉變,才能誘發我們的行為改變,最終創造具體成效。 p. 7

企業教練管理方法強調以人為本,著重於個人潛能的激發,協助尋找最適合個人發展的工作方式,從而有效且快捷地達到目標。身為主管須以幫助部屬實現他們的理想作為目標,以達成團隊的成就。 p. 18

1. 透視教練式領導

技術上的指導並不是最重要的,如何協助球員排除心理上的障礙才是重點。他認為,只要內心的障礙能排除,一個人的潛能就可以發揮出來,也就能自然而然順著身體及球的節奏把網球打好。 p. 40

碰到問題,先不做任何的評斷及建議,而是用提問的方式,讓當事人去思考、回答。然後,教練根據當事人的回答,分辨狀況及可能性,但仍不存有定見,而是繼續提出探索性的提問。在反覆地『發問、傾聽、分辨、回應』之下,逐漸讓當事人在自省、自覺的過程中,找到適合自己的答案p.52

很重要的是,所問的問題必須是『開放式』,不是『封閉式』的。 p. 52

所以,提出的問題必須能讓對方提供有用的資訊,作為你客觀分析判斷的基礎。提問還必須有引導作用,有啟發性,才能達到效果。 p. 53

教練式領導則掌握了人性的本質:人的內心底層是愛好自由的,不喜歡被干預,同時也都想呈現自己美好的一面。因此,在自覺、自發下,學習動機越強,也愈能發揮自信及潛能,將事情做好。 p. 54

自我察覺並不是自我批判,而是客觀、清楚地覺知到,自己呈現了甚麼樣的行為、產生什麼樣的情緒、有什麼樣的觀點。 p. 55

在指導式的管理下,部屬常花時間及心思在理解或猜測主管的意思,而不是把腦筋動在工作本身。 p. 58

績效 = 潛力 - 干擾。教練式領導的目的就是要盡量減少干擾,讓部屬的潛能完全展現在績效上。 p. 58

每個人的各型與觀點、做事方法各有不同。適合你的方法,不見得可以套用在別人身上。因此,你的經驗不等同於我的經驗,你的答案不見得就是我的最佳答案。 p. 60

教練式領導的一個重要精神是,相信當事人找到的答案才是最適合他的答案。 p. 60

教練的價值在於幫助他人『改變模式』,而不只是『解決問題』。因此,它著重的是『人的發展』,重點在『人』,從人的改變來消弭問題。 p. 61

現在的領導人必須能夠平衡地扮演三個角色:主管、老師及教練p. 64

領導人在扮演『主管』的角色時,影響力的基礎來自於『以權力服人』。有些狀況,主管擁有最終決定的權力。... (中略) ... 主管若過於依賴這個角色,時時戴著這頂高帽,不肯脫下,就只能帶領出一個唯唯諾諾、因恐懼而工作的團隊。 p.65

領導人在扮演『老師』的角色時,影響力的基礎來自於『以能力服人』。... (中略) ... 如果領導人過於依賴這個角色,過於『好為人師』時,往往會成為團隊的瓶頸,因為他的指示容易成為團隊的標準答案,部屬會忽略向外追求專業知識及技術的提升。反之,若不會適時扮演這個角色,則會讓部屬失去方向感、或得不到該有的指導,如同在迷霧中盲目摸索。 p. 66

領導人在扮演『教練』的角色時,影響力的基礎來自於『以德服人』。... (中略) ... 如果領導人過於沉溺於這個角色,可能導致團隊效率不彰。但如果不會扮演這個角色則無法激勵人心、培育人才或讓團隊發揮創意。 p. 67

走向教練式領導,領導人在心態上面臨的調整有下列幾項:

  • 信任他人 ... (中略) ... 相信每個人都渴望呈現自己好的一面,而這個渴望是把事情做對、做好的一大動機。
  • 開放好奇 ... (中略) ... 部屬的答案讓你有疑慮時,依舊不評斷、維持著好奇心,繼續了解他的用意。
  • 調整關係 ... (中略) ... 兩者將從上下關係調整為工作夥伴關係,一起各盡所能把工作做好,不僅如此,還是成長夥伴,彼此可以相互溝通討論、一起成長。 p. 71 ~ 72

2. 薩提爾教練模式

若是不管什麼狀況,都習慣用同樣的行為模式,就必然會出問題。 p. 76

習慣性的行為模式常常是身不由己的,當事人並不自覺。 p. 76

薩提爾模式是用人性關懷的角度 (人本信念),來探索行為背後的緣由,並從中找到轉化的方法 (冰山理論),以協助當事人重新學習及成長。 p. 78

每個人的答案都在自己身上。 p. 81

畢馬龍效應 (The Pygmalion Effect) ... (中略) ... 其含意是當我們以正向的眼光看待一個人時,就會激勵出他的正向表現,反之,則將引發負向的表現。 p. 82

若管理者對部屬不斷施予負面評價、責難,則部屬的表現與績效也會跟著變差。因此,教練式領導人首先必須對部屬帶有正面信念,排除負面信念對部屬潛力發揮的不當干擾。 p. 83

薩提爾模式的冰山理論指出:人的行為,包括所做的或所說的,只是冰山露出水面的一部分 ... (中略) ... 在冰山下潛藏著『情緒』、『觀點』、『期待』、『渴望』等四個要素。這些要素的內容,左右了冰山上層的作為及言語。 p. 84

確認目標 -> 辨識並排除冰山盲點的干擾,開創新的可能 -> 在行動中落實改變。 p. 90

辨識並排除干擾的引導次序

  • 讓當事人先冷靜下來
  • 確定當事人有改變的動機
  • 行為策略先於觀點策略 p. 97

3. 訂定談話目標

另一方面,目標的訂定是否得當也會影響到談話的成效,因此,不僅要設定談話目標,同時要訂出適當的目標。 p. 109

在目標的設定上,我們提供以下三個原則:

  • 訂定結果導向的目標
  • 將負向目標改為正向目標
  • 將大目標分解成小目標 p. 113

4. 探索行為效果

所謂的行為盲點,在工作上我們雖然訂了目標,也認真採取行動,但行動的內容和效果可能和目標背道而馳,還不自知。 p. 124

當主管事必躬親,凡事都要問、凡事都要管,或著太頻繁地追蹤進度時,部屬就容易依賴主管,或是不敢自作主張,因此這種作法或行為與『希望部屬能夠自動自發』的目標是背道而馳的。 p. 125

當我們帶著『無知好奇』的心態時,就自然會問出開放式問句。因此,心態是封閉時,問句也是封閉的;心態是開放時,問句也是開放的。 p. 136

經常問出封閉式問句,以至於無法達到讓對方自己看到盲點、自己找到答案的效果、究其原因,往往是因為主管已經習慣馬上思考問題、解決問題。 p. 136

5. 同理回應情緒

藉由對情緒的探討,我們可以了解一個人的思維模式以及引發行為的心路歷程,並且藉著調整觀點、期待及渴望,使情緒及行為也跟著改變。 p. 144

感性的需求是人類內在一大渴望,情緒的出現,往往是因為這個基本的渴望沒有被滿足。因此,一個善意的同理回應,是平撫情緒的最佳良藥。一旦情緒平復,才有理性溝通及思考的可能性。 p. 146

口頭上表示接受談話結論,並不表示內心也接受,情緒感受才是真實的內在意願。 p. 149

顯現在外的情緒,不一定是內在的真正感受。 p. 156

這是一個很重要的步驟,如果劉偉 (範例中的當事者) 情緒仍然不好,表示談話結論並不能切種劉偉的內心需要,就必須再繼續深入探究。 p. 157

當主管從『我』以及『解決問題』為出發點,往往看不到問題真正的核心,更看不到部屬的情緒。於是,問題或許暫時解決,但部屬累積在心中的抗拒並未排除,故無法真正學習到問題的解決方式。 _p. 158

負面情緒就像是一堵無形的牆,擋在面前,阻礙了將別人所說的道理及教導聽進心裡的機會。 p. 158

6. 理性論證觀點

自動化思考的成因是,每個人的腦海中都積存及潛藏了許多既定的觀點,形成個人主觀意識,因此遇事容易不假思索,直接套用。 p. 163

核心觀點是一個人思維及行為的自我依據。它一方面造就個人特有的人生觀、價值觀。形成個人風格,但同時也制約了當事人。 p. 165

信念沒有絕對的對錯,但過於僵固的信念,或過於堅持自己的信念,將對自己、他人及情境造成不利的影響,也侷限了一個人的發展。 p. 166

7. 改變不切實際的期待

渴望是抽象的概念,而期待是具體的方法。 p. 187

渴望是普世皆然的,而期待往往是因人而異的。 p. 187

第一種盲點是:將『渴望』與『期待』混為一談,於是他人無法明確理解自己的需要,最後導致自己的渴望得不到滿足。 p. 188

第二種盲點是:雖然明確地知道對自己及對他人的期待,但這些期待卻是不容易實現,甚至是不可能實現的,然而自己卻仍然固守著這些期待,以至於無法滿足自身的渴望,最後導致失去努力的動機,甚至做出不具適應性的行為。 p. 188

人類行為及思維的成形涉及家庭、社會等根深蒂固的複雜影響,是經過長期的日積月累而來,因此自我覺察及調整的過程不是一蹴可幾的。 p. 203

教練式領導的效果是可以期待的,但是也需要有務實的認知。如果想要以一次教練式的晤談,就讓部屬馬上大躍進,或是產生完全符合自己設想的改變或調整,那就是不切實際的期待了。主管必須接受部屬的成長是需要時間的,也必須了解並非用了教練式引導,就具有讓頑石點頭的神通。 p. 203

8. 增強改變動機

觀點、期待及行為的結果,和渴望的真正滿足有落差,正是本書所謂的盲點。 p. 209

如果主管發現部屬不理想的行為模式是來自渴望的不當篇正,就可以先引導部屬確認他所重視的渴望,並分析它的影響,接著再協助他覺察被他忽略的渴望,以達到平衡地滿足所有渴望的目標。 p. 214

渴望適度地被滿足可以激發部屬的責任感,反之則容易引發抗拒及怨懟,而挫傷工作意願。然而,在講求績效的職場上,人性的基本渴望卻常常被主管忽略,有時更反其道而行,以壓制人性的方式追求績效,造成部屬怨聲載道,士氣低落。 p. 227

綜合而言,薩提爾教練模式一方面引領部屬進行理性的思維程序,辨認並排除冰山各層面的盲點,由內而外產生有效的行為模式,另一方面又深入部屬的內在需求,啟迪成長的動力,是理性感性兼具的方法。 p. 229

9. 踏出實踐的第一步

建議你先應用在有潛力、值得培養的部屬身上,出發點是培育人才。其次可以應用在整體績效表現讓你滿意,但在某方面又讓你頭痛的部屬身上,出發點是協助他排除盲點。 p. 231

哪些對象不是和應用教練式領導呢?首先是比較資淺的部屬,因為他們最需要的可能是一些基礎的知識,此時就比較適合直接傳授知識給他們,也就是戴上『老師』的帽子。其次是績效一直未達標準,卻沒有動機改善的部屬,此時領導人該做的,可能是要戴上『主管』的帽子,讓他們知道不改善的後果,最後還可能要使用主管的職權,安排他們轉任至適合的崗位,或著請他們離開公司。 p. 231

結語

我們覺得人是無法改變的,往往是因為我們太急著看到結果而忽略了過程。每個行為的背後,都存在著深層的因素及動機,只有探究這個心路歷程,並重整、建立新的路徑,表面的行為才會改變。 p. 245

許多人 (尤其是資深且有能力的人) 往往需要先覺察自己的處境,並且找到適合自己的方法。才能由內而外真正的改變。 p. 246

這書是在 Agile Meetup 2016年二月聚會聽完作者本人的演講才知道的,但手上的書一堆,也就一直沒有再買進,最近碰巧想試著練練鋼筆字養心,就跟著練字帖一起買回來看,因為已經聽過演講,看起來也就很快,但這次感觸卻更多,因為裡面很多內容都可以套用在自己身上,試著去分析自己的盲點,挺有意思的。

 
over 2 years ago

Translated from "For Faster Java Collections, Make Them Lazy" by Mike Duigou, Java Magazine March/April 2016, page 28-33. Copyright Oracle Corporation.

延遲,讓Java容器更快

如何透過對ArrayList與HashMap加入延遲操作以增進效率與減少記憶體使用量

Java 核心函式庫的社群已經努力透過延遲的方式改善 Java Collections Framework,在軟體中,延遲性是一種架構性或系統性的方式,延後產生結果,一直等到結果是真正確定被需要的時候。許多運算可以被拆解成大量的副運算,延遲性延緩副運算的執行直到其他副運算或整個運算需要那些副運算的結果。

以非延遲性方式完成一個運算是執行整個一連串副運算然後結合其結果產生最終的結果,更有效率的延遲方式是一開始就結合副運算的結果,當您發現需要一個結果,但因為其副運算還未執行而不存在時,您才執行該副運算取得部分的結果,剛開始,您從沒有任何已完成的結果開始,不斷地累積副運算完成的結果,讓額外的副運算完成,最終完成整個運算的結果。如果您有個運算的結果,其中若干個副運算因為它們對於決定最終結果而言是不需要的,其結果從未計算,延遲性就能成功節省時間。

任何時間,一個運算的結果有潛在性或可能對最終結果而言是不需要的,延遲該運算到其結果是真正有需要的時候是有意義的。延遲性的常見例子是表示式的計算,細想下列程式:

int x = 5;
int y = 3;
if (x < 2 && y < 7) {
    ...

計算這表示式最簡單的方式是計算每一個子句然後結合其結果:

5 < 2 => FALSE
3 < 7 => TRUE
FALSE && TRUE => FALSE

注意到,如果第一個子句的結果是 false,則整個表示式的結果永遠會是 false,因此我們不需要煩惱第二個子句的計算直到第一個子句的結果是 true,對 Java 程式來說,Java Language Specification 規定表示式的子句從左到右依序計算,如果任何子句對其結果是不需要的,就不會被計算,這通常會節省計算,通常也是很有用:

if (foo != null && foo.bar() == 3) {

在這例子中,要呼叫 bar() 函式的前提是 foo 是非空值,如果 foo 是空值,企圖計算表示式的第二個子句會導致拋出 NullPointerException,Java 的延遲運算規則確保第二個子句只在第一個子句的結果是 true 才執行,邏輯的 AND (&&) 運算可以視作與巢狀等效,因此

if (foo && bar && baz) {

和下面是等效的

if (foo)
    if (bar)
        if (baz)

這重寫的巢狀條件是同樣讓它更清楚為什麼某些不需要的子句不用計算,延遲運算同樣適用在邏輯的 OR (||) 運算上,例如:

if (true || something) {

這條件表示式中的 something 子句永遠不會被計算,因為表示式的結果可以在它被計算前直接決定。

表示式延遲運算的例子在寫有效率的,或者可能更重要的是簡潔的程式是很有用的,其他形式的延遲性一樣有助益,但程式的流程決策與能得到的助益之間的關聯不是那麼直接或立即。

如果一個計算的結果是偶而或時常被捨棄不被使用,那直到它是必要前,避免使用其需要的資源去計算它是很有意義的,最明顯能被節省的資源是 CPU 週期,但延遲性能同樣避免配置記憶體以節省記憶體,或是避免使用非必要的檔案、sockets、執行緒、資料庫連線等以節省系統資源,根據情境,這些節省的資源可以是很龐大的。

實作延遲性可以是一個改善系統效能的關鍵最佳化策略,透過避免非必要的工作來提升效能,而不是改善執行該工作的效率,延遲性類似減少應用程式對資料庫的查詢次數能改善 30% 效能,與之對比,在相同程式中,改善資料庫查詢效能只能改善 3% [譯注:這應該是在查詢本身寫的就不錯的前提下才成立]。如果可行,將您的時間花在前者,會比較有效果。

在少數的應用程式中,我發現延遲性在記憶體使用量提供至少 20% 的節省量及減少小量的記憶體重整次數。

延遲性容器的挑戰

在已大幅最佳化的 Java Collections Framework 中,實作延遲性成為分析程式行為後的結果,Oracle Performance Scalability and Reliability (PSR) 團隊分析某些 Oracle 框架與在些框架上執行的應用程式,PSR 團隊發現應用程式與中介軟體都常配置 ArrayListHashMap,但擁有該容器的物件,在整個生命週期中,卻不曾使用過它們,大約 2% 的 ArrayListHashMap 不曾接收到任何元素,更進一步分析發現,容器不是總是被使用,只有某些情況下使用,PSR團隊完成某些工作想了解:是否能重構這些應用程式,在一個共同的基礎類別下,用不同類別處理容器被使用與不被使用的情境,將容器只會部分被使用的情況定義在一個子類別中,這方式並不是那麼容易實現,因為很多情況類似下列的例子:

public abstract class RestRequest {

    protected final Map<String, String> httpHeaders = new HashMap<>();
    protected final Map<String, List<String>> httpParams = new HashMap<>();
    protected final Set<Cookie> httpCookies = new HashSet<>();

RestRequest 是個虛構的範例類別,在應用程式中用來處理HTTP REST查詢,REST是透過HTTP通訊建立API的一種常用方式,在這例子中,每個 RestRequest 物件是一種特殊格式化的 HTTP 請求,代表一個到主機應用程式提供的 REST API 呼叫,RestRequest 實體需呈現接收到的 HTTP 訊息中重要的部分,包含 HTTP 標頭 (httpHeaders)、來自查詢字串或表單資料的 HTTP 參數 (httpParams) 及 HTTP Cookies (httpCookies),這些類型 HTTP 特性可能呈現在一個 HTTP 訊息中,但確切的使用則由每個應用程式的 REST API 與使用 REST API 的客戶端決定。

因為對任一個請求而言,HTTP 特性的使用是不確定的,包含 RestRequest 中針對任一個潛在特性的資料結構都是不確定的,HTTP 標頭、參數及 cookies 都是常見且是 HTTP 協定需要的部份,但應用程式並沒有被強迫使用全部的特性,甚至可能全都不用,有些 REST API 可能使用 HTTP 參數,而其他可能使用 cookies 或標頭,一個可能應付選擇性需求的方式是提供多種 RestRequest 類別的變形,表達 HTTP 特性所有可能被使用的組合,然後,這將會是擾人且不方便使用。即使確定某種特定的請求會使用某個特定 HTTP 特性,通常這特性只在少部分的請求中被使用,像是相同的請求,已認證的使用者可能使用 cookies,但未認證的使用者則未必使用,可能大多數的請求來自未認證的使用者。

讓程式邏輯較簡單的方式是有一個 RestRequest 類別且有 httpParams 欄位,總是初始化並可以使用,不論是否被使用 (待會會回到這點)。

由於框架或應用程式無法重構以消除這不可缺少但不經常使用 [譯註:我個人原文中的 in 位置錯了,若照翻,不可缺少但經常使用,語意上怪怪的] 的 httpParamshttpCookies 欄位,因此需要替代方案。

整體的目標是除非需要不然避免在類別中持有像 httpParams 這種欄位的成本以改善應用程式的效能,一種方案是在 RestRequest 中延遲 httpParams 欄位的初始化,即只有在需要時才建立 HahspMap,這將在所有要用到 httpParams 的地方需要額外的保護檢查:

if( httpParams != null )

但因為 RestRequest 的設計是可以被繼承的,所有繼承 RestRequest 的子類別在使用 httpParams 時,將需有類似的檢查,因為有許多既有的程式沒有這些檢查,讓 RestRequest 突然停止欄位的初始化是不合理的。

允許 httpParams 有時候可以是空值,多出來的保護檢查可能會造成程式的邏輯變複雜,若在 RestRequest 的其他欄位使用同樣的方法,RestRequet 的函式與子類別將過度或重複地檢查 RestRequest哪些特性有被使用,隨著時間,不可避免地,錯誤會在忘記檢查 RestRequest 的其他部分時偷偷溜進來,忘記一個對可能是空值的檢查可能耗費您一整天!

有一個解法,通常很有用,但不適合在這個案例中,維持 httpParams 的宣告不變

protected final Map<String, List<String>> httpParams;

但將初始化移到建構子中,在建構子中,如果可以判斷請求沒有使用 HTTP 參數,則可以將 httpParams 欄位初始化為 Collections.emptyMap(),不用為每個請求單獨建立一個 HashMap 實體 (在 Collections 工具類別中,使用像 Collections.emptyMap() 和其他 emptyList()emptySet() 相似的輔助函式,以提供空容器來說是一個效率高的方式,這些空容器常用來取代建立一個獨立實體但沒有內容的情況,它們常會比回傳一個空值要好,因為不是回傳空值,對空值得檢查就可以被消除。)

在首次進行於框架與應用程式中透過避免建立 HashMapArrayList 實體的方式加入延遲性後,很清楚的,最有效率的方式是在 Java Collection Framework 本身的內部實作延遲性。

Java 8 同樣新引入一個顯著的延遲性實作:Streams API,這函式庫將延遲性作為核心原則。

更新 Java Collections

對 Java 基礎類別例如 ArrayListHashMap 進行修改是一件很嚴肅的事情,有眾多的程式員及無數程式碼使用這些類別,不論版本,程式員與程式都預期 Java 會提供可靠、一致的行為和效能,Java Collections Framework 是與開發員和與程式承諾提供特定行為的合約,實質上,不可能在更新或新的釋出版本中,以改變合約的方式改善 JDK 類別的功能,對 API 合約進行小的微調是可能的,但大多數能讓 Java Colletions Framework 改善都是內部改變,即使是內部改變,也必須小心考慮以確保這些改變不會造成副作用與非預期的行為改變。

稍早,我提到像 RestRequest 這樣有些欄位可能是空的類別難以使用,可能是空的 public 或 proected 欄位,使用它們之前需額外空座,任何試圖讀取欄位都要一個檢查,確認欄位不是空的,在允許空值的程式中,沒有一致性地檢查空值是常見的錯誤,一般會建議不允許 protected 或 public 欄位是空值,當欄位是 private,處理可能是空值的情況是較能控制的,因為所有指向此欄位的參考都在同一個檔案中,且較容易理解在不同的物件狀態下可能的值。

ArrayListHashMap 都使用 package private 陣列欄位作為核心的資料結構儲放元素或項目,除了 ArrayListHashMap 物件本身,這陣列是 ArrayList 唯一配置的記憶體,對空的或接近空的 HashMaps 來說,這陣列是最大的記憶體配置,以延遲性更新這兩個類別的重點是增加檢查陣列欄位可能是空值。

延遲性加到 ArrayListHashMap 中最主要的助益是它延後或可能避免配置陣列的記憶體,直到有第一個元素加到容器中,為背後的陣列配置記憶體在某些應用程式中成本是很高的,對其他應用程式來說,特別是未使用的容器本身是短生命週期的,甚至不知道記憶體已被配置,其助益就是節省初始化所需的計算。

由於欄位指向一個陣列,JVM 在索取某個位置的元素或取得陣列長度時就已經要檢查陣列是否為空值,增加的保護檢查是暗地檢查空值的明確版本,這意味著,增加這保護檢查不會對效能造成損失。

第二個要考慮的是額外的保護檢查與不同方式的配置邏輯,可能改變 HotSopt 如何內嵌函式或更重要的是不內嵌函式,這可能暗中損害效能,內嵌是 HotSopt 為小函式最佳化的一種方法,當 HotSopt 編譯一段呼叫一個短的函式或是小函式的程式碼時,它常會以該函式的實際內容取代函式的呼叫,HotSpot 有個大小上限決定是否要內嵌,試驗發現,我們在關鍵的函式加入延遲性並未接近內嵌上限,且在幾個地方重複使用既有的內部函式,可以改善 HotSpot 內嵌的效果,仍有少數非關鍵的函式,在加入延遲性後有稍微變慢,不過,在一般的評測與效能測試中,這變動產生效能的淨成長,到目前為止,我還沒發現任何案例在加入延遲性後,產生明顯不預期的副作用。

結論

在評估一個 JVM 應用程式的記憶體使用量時,除了總使用量外,還需要考慮到更多面向,因為 JVM 使用垃圾回收機制管理記憶體,您還必須考慮記憶體的配置率與垃圾回收的壓力,記憶體配置率指的是應用程式配置的新物件數量與配置的記憶體大小之間的比例,應用程式在記憶體配置率上變化很大,且它是影響生產量的一個關鍵因素,和配置率相關的是垃圾回收機制花在未使用的物件上的消耗,垃圾回收的壓力指的是您需要犧牲多少生產力,用在垃圾回收機制上,確保應用程式總是有足夠的空閒記憶體空間,一般來說,大幅降低配置率同時能減少必要的記憶體回收數量。

在一般框架的應用程式中,將 ArrayListHashMap 改成延遲初始化能產生大約 1% 到 2% 的記憶體使用量與配置率,幾乎沒有可量測的效能增益,同樣重要的,沒有應用程式增加記憶體使用量或降低效能,在少數的應用程式中,我發現延遲性在記憶體使用量提供至少 20% 的節省量及減少小量的記憶體重整次數,對某些應用程式來說,這是明顯的助益,同時對大多數的應用程式來說,能提高少許的助益,且沒有已知因延遲性改善所造成的負面影響。

再次思考 RestRequest 範例,用延遲性改變 ArrayListHashMap 的使用會改變行為與效能嗎?RestRequestArrayListHashMap 欄位仍然是非條件是初始化的 final 實體,這意味著 RestRequest 的使用方式未改變,程式不需要為欄位可能是空值而加檢查,實際上,即使,我們仍希望越多的容器實體可以是延遲初始化,這會延後建立元素陣列,以節省記憶體與 CPU 週期。

在任何以改善超過 20 年以上的軟體系統,像是 Java,這很難找到任何實作上的改變能單方面對任何情況造成助益,仔細思量這變化,我能確定一般對 ArrayListHashMap 的使用沒負面影響,在關鍵函式像 HashMap.get() 效能分析通常測試其 Java bytecode 與 CPU 週期的消耗,這些函式每年在數百萬的 JVM 中被執行數百萬兆次,有些微小的效能消耗變動,將消耗從一個執行路徑移到另一個較晚的路徑上,是可以被接受的,但任何效能的下降可能是不可接受的,可能需要從其他更大的效能增益來補償。

問題的分析從觀察應用程式與框架的行為開始,希望有某個方式能減少未使用容器的消耗,這種從上而下的效能分析,到目前為止,是改善應用程式效能最好的方式。

延遲性的其他可能已經考慮用在 Java Collections Framework,最期待發生的改變會是 HashMap 如何建立與改變其容器的大小,HashMap 典型的使用模式建議其實作在較少元素的情況下 (及在有較多元素的容器中第一次呼叫 get() 時) 使用不同的資料結構能會來帶助益。

有 Java 函式庫有其他使用延遲性的例子,最常見的是在 hashCode 函式中快取雜湊值計算的結果,這為 String 與其他類別帶來明顯的效能助益,其他快取的案例同樣被加入,有些快取避免重複的計算以改善效能,有些則是多次運算產生相同資料結構作為結果以節省記憶體,額外的快取與其他延遲的初始化,只有被證實是有幫助的就會被加到後續的 Java 更新中,同樣,當快取是浪費的或是為維護快取造成額外的消耗則會被移除,大多數的情況,因為它們不會改變 API,加到 Java 函式庫的延遲性改善可以帶來助益且沒有不好的影響。

Java 8 同樣新引入一個顯著的延遲性實作:Streams API,這函式庫將延遲性作為核心原則,且頻繁地較宣告式方式提供更好的效能。

延遲性是一個重要的最佳化方式,並已經為 Java 函式庫帶來顯著的助益,如果您需要為一個正在使用的函式庫改善效能,當其他主要的最佳化方式像是改善演算法,已經被使用過了,您應該考慮它。

Learn More
How laziness affects the sizes of an ArrayList allocation

譯者的告白
這次標題的翻譯,我換了好幾個版本,最一開始是:懶,讓 Java Collection 更快,但總覺得沒有抓到原文的重點,後來是用反義詞讓整個意思更清楚 (或是說故意造成反差)。

突然跳到 March/April 雙月刊是因這篇文章讓我想起,先前在幫公司的產品進行最佳化時,就是用延遲性改善效能,在我那台舊舊的開發機上,讓 App 的冷啟動時間從 10~12 秒改善到 5~6 秒,不過這方法其實需要很多設計想法上的改變,若一開始沒考慮到,之後修改的成本或是可能造成的副作用 (如文章中所說的空值檢查或是 API 變動) 就需要考慮進去,雖然說有個說法是 premature optimization is the root of all evil,但如果 Performance 是架構設計時列為重要的 factor 時 (不一定總是總要的),即使不立即實作,設計時也要想著最佳化這件事。

 
over 2 years ago

過去把 GitHub 只當成免費的 git repository 在用,省去自己架設 git repository server 的麻煩,但用過 CocoaPod 開發 iOS 專案後,發現很多 third-party library (例如:DZNPhotoPickerController) 不只把 GitHub 當成 git repository 而已,還是發布用的空間。最近撥了一點時間研究 GitHub 發現:GitHub 幾乎可以提供軟體生命週期管理所需的各種服務了,想當初修軟體生命週期這門課時,花不少時間研究 IBM® Rational Team Concert™,現在很多服務都可以在 GitHub 上使用,不用再自己架 server 了。

會開始研究是因為自己偶而會把一些簡單的工具類 (Utility) 程式放在 GitHub 上,但也只限於把程式碼丟上去而已,對於程式碼的狀態與管理就很少去處理,事實上很多事情都可以自動化,以自己的 SortDescriptor 專案為例,現在只有要 commit 都會自動觸發 Travis CI 進行建置和測試,另外還會跑 code coverage 工具產生測試涵蓋率的報表。有趣的是,還可以把所有的狀態都用成 badge 顯示在主頁上,因此只要進到專案首頁就會知道:最後一次建置是成功的,然後 code coverage 達到 100%。

回到軟體生命週期管理,若是小的團隊或是新創團隊,在開發軟體專案時,我個人覺得軟體生命週期管理中,較重要的有幾個環節,以及可以一起搭配使用的服務:

  • 專案管理 or 軟體開發流程管理 (Trello)
  • 持續整合 (Travis CI)
  • 版本控管 (GitHub)
  • 議題追蹤 (GitHub)
  • 建構管理 (GitHub 搭配 JCenterMaven CentralArtifactory,以Java專案為例)

GitHub 提供版本控管和議題追蹤這二個部分的功能,搭配 GitHub 的 Webhooks & Services 整合第三方的服務,像剛剛提到的,在 GitHub 整合 Travis CI,當有任何人 commit 上去就會觸發,根據 .travis.yml 腳本的設定,可以只跑測試,或是在進行 tag 的時候,將網站部署到 server 或是將套件部署到 JCenter,由於 .travis.yml 腳本也跟著一起版控,所以也比較不會有在 Jenkins 上可能會出現建置設定與程式碼不相容的情況,例如程式碼最新的版本需要新的參數設定與環境,但某個較舊的 branch 或 tag 必須使用舊的環境,否則會建置失敗的情況。這正好是滿足建置管理中,確保所有會影響到建置的環境都需要進行版控與追蹤的目標。

建構管理除了上述建置腳本的版控外,還有一個是第三方套件的版控,軟體通常都不是一個團隊能全部自己開發的,大多數情況下都會用到第三方套件,因此在 Java 的社群中,Maven 以及最近很紅的 Gradle 都有提供建構管理的相關功能,以 Maven 來說,pom.xml 檔指定第三方套件的版本,然後 pom.xml 檔隨著程式碼一起版控,建置時,根據 pom.xml 的設定, Maven 下載並使用指定的第三方套件健行建置,不會有誤用到不相容版本第三方套件的問題。

但當開發團隊開發的軟體越多時,通常會累積不少可重複使用的程式碼,此時,怎麼管理這些內部套件呢?假設這些內部套件是可以給外面使用的,那較單純,直接在 JCenter 註冊一個 repository,然後在 pom.xml 檔中加上發布的 plug-in 設定後,就可以將套件的 JAR 檔放到 JCenter 讓大眾使用, JCenter 在 Google 的加持下 (Android 套件的官方 Repository),讓不少人因無法證明擁有 group ID 所有權 (通常是要花錢去註冊 domain name) 的團隊,從 Maven Central 轉往 JCenter。

如果是私有的套件呢?可以選擇在團隊內用 Artifactory 建立一個 repository server 來管理這些套件,server 建置好後,一樣可以在 pom.xml 檔中加入布署的 plug-in,差別只是發布的位置是內部的 server。Artifactory 除了可以當套件管理的 repository 外,還可以充當外部 repository 的快取,在下載大量相依的第三方套件時 (一般來說第三方套件會相依其他第三方套件,導致要下載一大串第三方套件),縮減不少時間和網路用量。

此外,建構管理中還包含文件的管理,軟體還會需要一些文件,像是系統架構說明、設計說明等等,GitHub 的 Wiki 可以扮演文件庫的功能,同樣也能進行版本控管,只要有心,文件與程式是可以一起追蹤並管理的。例如在 merge 回 master 時,就需要同時審查程式碼與文件是否一致,若一致才能 merge 回 master 上,如此就可以確保 master 上的程式碼和文件是一致的,但這是 policy 的部分,要視團隊的需求決定是否這麼做,但就工具上是沒問題的。

在軟體開發流程的管理上,不管是使用 Scrum 或是 Kanban 方法,通常都會建議使用實體看板,但有時候電子看板還是有一些好處,像是和不同工作地點的人一起合作時就能夠協同使用。現在有蠻多雲端服務可以用的,像是 JIRATargetProcess 都可以與 GitHub 一起使用,不過上述的都算是需要付費的服務 (免費有些限制),若只是要個簡單的看板,其實可以使用 Trello,建立一個看板,然後建立 Product Backlog、Sprint Backlog、In Progress 和 Done 等列表 (下圖中的 Deployed 是針對網站服務的專案額外調整增加的,代表程式已經部署到 server 上了,可以用了) 就可以拿 Trello 當成 Scrum 的看板了。

雖然 Trello 沒有 user story 和 task 的概念,但每張 card 都還是可以有 to-do list,所以可以用 card 來描述 user story 然後在每張 card 中用 to-do list 的項目描述 task,就像下圖一樣,唯一可惜的是, to-do list 無法像 card 一樣在看板的 list 中移動作爲狀態切換 (另一點可惜的是 to-do list 的項目也無法出現在 PomoDone 中使用),但在看板上是可以看到該 story 的 task 完成的數量,還算可以接受。

到這邊,等於是以 GitHub 為中心,整合了許多網路上的服務,完成了軟體生命週期管理中較為重要的幾個項目。當然,當團隊或公司有一定的規模後,可以選擇整套的軟體生命週期管理解決方案,像是微軟的 Team Foundation Server 或是 Atlassian,只要花錢就能夠獲得完整且充分整合的方案,但新創時組合眾多免費服務的土砲也是一種不錯的選擇。而且因為是自己組合的,常常能組合出整套 solution 所無法提供的新玩法。

搞懂 Travis CI 的設定檔怎麼寫,怎麼串連 Travis CI 與 GitHub 與 codecov,如何註冊 JCenter repository (還發生過註冊錯的 group ID,得請管理員幫我改一些東西),以及和 Maven Central 打交道 (最後當然是失敗收場,因為我不想花錢去註冊一個沒在使用的網域),雖然都是一些麻煩瑣碎的事,但只要完成了,卻可以提高整體的開發效率,是蠻值得投資的基礎建設,即使不是用 GitHub,像是 GitLab 或是 BitBucket 等服務都有類似的組合可以搭配使用,因此,即使在公司內部,也能組合出這樣的環境 (最近就幫公司組合了 GitLab + GitLab CI + Artifactory + Trello Kanban)。

參考資料
https://theagilecoder.wordpress.com/2013/11/11/how-to-set-up-trello-board-for-scrum/
https://theagilecoder.wordpress.com/2013/06/08/using-trello-for-scrum/
http://willowtreeapps.com/blog/user-story-mapping-using-trello/

後記
本來這一篇是去年底就開始寫了,只是中間停擺了很長一段時間,本來是連怎麼設定都要寫上來,不過我其實也就只是照著網路上的文章不斷地試誤,而且不同專案、語言或是環境也不見得能照用,所以就不放在文章中了。

 
over 2 years ago

第一章 精實軟體開發

精實軟體開發是由軟體開發領導者,例如:軟體開發部經理、專案經理和技術領導者,而不一般程式程式開發人員所創設的思想工具。 p. 3 [ps. 思想工具這個詞讓人有想洗腦別人的遐想XD]

「原則」所影響的是企業的文化層面,比起單純的開發方法影響要巨大多了。 p. 4

只要是對客戶或產品沒有提升任何價值的行為,基本上就是一種浪費! p. 6

「錯誤的估算」便是一個簡單不下來的原因。千萬不要在沒有做適度的拆解問題 (工作項目) 下進行時程的預估,因為那完全是在猜猜看!猜是人類最糟糕的預估了。 ... (中略) ... 所以在減少浪費的前提下,「先拆解再簡單化」是開工之前 (或是進行工時預估前) 的必備動作,正確的拆解可以避開那些不必要的複雜性干擾。 p. 7

老實說,只有進行一段時間,有更深一層的了解後再來估算自然會準確許多。這種較精確的估算通常發生在專案進行五分之一到三分之一之間,這是一件耐人尋味的事,此時工程師對於專案的把握程度就可以大幅提升,這個時候的預估就可以接近「承諾」了。 p. 8 [我把接近二字變粗體,因為很重要XD]

判斷是否浪費十分重要,它是你避免浪費的基礎。 p. 9

「半成品」的英文是 Work-In-Process (WIP),雖然翻譯成「在製品」看起來較貼切,但我偏好採用「半成品」這個字眼。所謂「部分完成的工作」,它是一種賭博,一個隨時可能會失效的功能,因為他有可能還沒上場就被換掉了。 p. 9

盈餘時間是最適合用來進行文件的撰寫作業了,工程師要學會交接文件給自己p. 10 [這是真的!]

記得,只有在有必要的時候才新增功能,任何一段不需要的程式碼都是一種浪費,千萬要懂得抵抗自以為能夠有先知卓見這種未雨綢繆能力的誘惑。 p. 10

由程式誕生的方式到我們除錯解決缺陷的方式,藝術的成分還是佔據較大的比例,因此軟體開發是一門工藝是目前較被接受的一種說法。 p. 12

軟體開發是一種學習的過程。 p. 14

科學方法是透過觀察、建立假設、設計實驗、進行實驗、然後得到結果。有趣的是,如果你的假設越正確,你就不會學到太多東西。當失敗率達到50%時,你會得到最多的訊息,也就是學到最多。工程師撰寫程式時不也是如此? ... (中略) ... 傳統的開發方式正是要求大家透過審慎的態度一次做對,而敏捷開發則是鼓勵透過嘗試、測試、修正的短週期來開發程式,自然你會學到最多。 p. 15 [好像還蠻常聽到一次做對這句話,是錯覺嗎?]

如果測試成本越高,就多花些時間仔細思考、審慎檢查後再動手,如果實驗的成本很低,那它就是最有效的方法! p. 15 [所以我一直認為單元測試的CP值最高]

沒有比在心情好的時候更能充分吸收知識的了,保持愉悅是一種成功學習的秘訣,所以在每日站立會議時進行鼓舞士氣、提升團隊和諧的行為.對一天的工作絕對有它的提升效益,你應該嘗試看看! _p. 17_

延遲決策是為了避免在早期資訊還不夠清楚的狀況下,就迅速做出決定,或是進行評估作業,這會是一種浪費,因為可能會有很多東西之後還要修改或是重做。 p. 18

最後負責時刻是指,當你再不做出決策時,不做決策的成本就要高於作出決策的成本時,就稱之為「最後負責時刻」。 p. 19

精實理論假設:

  1. 成熟的組織關注的是整體的系統,它不會專注於優化分散的部分。
  2. 成熟的組織強調的是有效學習,它會授權予工作人員制定決策的權力p. 26

從管理學的角度來看,讓團隊自我管理可以在工作上獲得最佳的效益,所以管理者真正該做的事,是去做那些能夠讓團隊增值的措施。 p. 26

簡單的規則讓團隊顯得一致,而一致的目標讓團隊更加團結;混亂與充滿相怨的環境只會讓團隊失去內在成長的動機。 p. 28

很明顯,你的問題不在於採用哪種敏捷法則,而是在於管理,也就是該如何正確的管理團隊。 p. 29

沒錯,這正是我需要的東西。 ... (中略) ... 因為它已經滿足你真正的需求了,所以你只會注意到他做了些什麼,而不會在乎它的缺陷,也就是所謂的「情人眼裡出西施」,此時的品質就是西施。 p. 29

要建構具有高度感知完整性和概念完整性的系統,應該在客戶與開發團隊之間以及開發團隊的上下游過程之間形成出色的資訊流 (information flow),而此資訊流必須考慮到系統當前和潛在的用途。具體上怎麼做呢?

  • 增加全體開發人員在應用領域方面的知識。
  • 接受變更,並將變更看成是一件正常的過程和容納新設計及決策的能力。
  • 營造提高通訊能力的環境,以便對人員、工具和訊息進行整合。p. 31

局部優化易造成捨本逐末 ... (中略) ... 同樣也會發生在個人身上.如果人們在開發一個系統時,處處都優先考慮自己的專業興趣而忽略了整體性的考量,則產品就會出現局部優化,而共同利益就會受到損害。 p. 33

第二章 看板方法

四個基本原則 (Foundational Principles):

  • 原則 1:從既有的流程開始 (Start with existing process)
  • 原則 2:同意持續增量、漸進的變化 (Agree to pursue incremental, evolutionary change)
  • 原則 3:尊重當前的流程、角色、職責和頭銜 (Respect the current process, roles, responsibilities, and titles)
  • 原則 4:鼓勵各層級的領導行為 (Leadership at all levels)
六個實務 (Core Practices):
  • 步驟 1:視覺化
  • 步驟 2:限制半成品 (WIP) 數量
  • 步驟 3:管理工作流程
  • 步驟 4:讓規則明確
  • 步驟 5:落實回饋循環
  • 步驟 6:由協作改善,經實驗演進 p. 40

看板是一種透過漸進、演化過程來改變組織系統的方法⋯看板的本質是一個單純的想法,那就是半成品 (work-in-process, WIP) 必須被限制。—— David J. Anderson p. 40

取得協議是第一準則,最好能夠事前先進行溝通,取得團隊成員的同意。通常自我管理的團隊對變革的承受度會較高,但頻繁的變革仍然應該避免,以小幅度的增量方式漸進式地做改革,才是第二原則的訴求。 p. 46

規劃為來通常能帶給人們希望,但在變革之前要確保,不是只有一群主管充滿期待與希望,期待變革後帶來的種種效能,這是十分危險的一件事!不論是哪一種敏捷方法,都會主張由團隊做自我管理,這個主張不是說當主管們辛辛苦苦完成變革後,再把它交接下去給團隊,非也!要形成自主的團隊,當然是從變革之初就開始著手。主管們不用太擔心該如何去設計這樣的情境,其實在變革當下,『尊重』是團隊最渴望的需求了。 p. 47

以人為本,尊重人性一直是敏捷開發的最重要精神,正如敏捷宣言的第一條宣言所說的『個人與互動重於流程與工具』。 p. 48

四個基本原則的最大意義在追求一個好的開始,前三個原則在提醒你避開人為的阻力,第四個原則則告訴你,讓對現有工作有改進熱情的人浮現出來,讓團隊能審視自己現有的工作流程,並找出哪裡可以改善的地方,經過討論後改善缺點變得更好。 p. 49

看板方法能夠協助我們做到一個首要目標,七個次要目標:

  • 首要目標:優化現有的流程
  • 高質量交付
  • 提升前置時間的可預測性
  • 提升員工滿意度
  • 為改善流出盈餘時間
  • 簡化優先級排序
  • 使系統設計及運作透明化
  • 設計能夠打造高成熟度組織的流程 p. 51

看板方法從一開始的出發點便認為,被壓迫的員工不見得能有高性能的表現;反之,員工在滿意的環境下容易受到激勵而有高性能表現。 p.54

在引入新改革方法的時候,首先要做的便是為團隊找時間,要知道一個沒有時間的團隊根本很難再去學習新的東西。

雖然我們都知道,改革的動作通常都是由上而下運作才容易獲得成功,但精實的精神則告訴我們要透過持續的改善,不只是由上而下,更要由下往上才能形成自我管理的團隊,並獲得全面性的成功。 p. 58

『維護作業』一直是看板方法扁線的最好的一個領域,這也正好是其他許多敏捷開發法最受人質疑的地方。平心而論,維護作業就是接獲請求後,便去設法解決問題,然後在適當的時機做成部署,替換掉先前的問題。這種直接了當的工作,實在也沒有必要運用敏捷開發的漸進式開發方法或是切割成多個迭代來完成它,直覺地採用看板方法的流程控制方式,正是最能滿足客戶急著獲得改善的需求了。 p. 59

所以就目前的看板方法而言,稱它只是一個流程控制法則一點也不為過。因此,我們可以把它歸納為:運用在流程管理和改進的一種高性能方法p. 62

第三章 看板方法的六大核心實務

『選定範圍』是一開始最重要的步驟,選定起點與終點的工作可能會影響到實施看板方法的整體效能,經常會不容易做決策,但有一個簡單的原則可以參考,那就是試問一下:它是我們可以控制的項目嗎? p. 70

對於工作流程而言,盈餘時間是一種浪費,絕對應該消除;但對於專案開發而言,盈餘時間是一種『工程師的福祉』,可以拿來做很多的運用,其中一種能夠幫助工程師持續成長而頗有價值的便是『學習』,在這瞬息萬變的資訊世界哩,成長與學習真是太重要了。 p. 78

雖然我們都知道多工對效能一定有所損失,那看板方法又是如何處理這種現象呢?有二種方法供選擇:

  1. 將WIP的數值調大到你願意承擔的 Queue 的最大值,然後再視情況依次遞減下來。
  2. 預設每個人需要承擔的多工數目乘上人數作為WIP值,用最小值的方法做設定,然後再視情況一次遞增上去。p. 79

目的都在不斷嘗試找出產生阻塞時的WIP值,找出來之後再由現況來判斷考慮調上或調下 (找到平衡點),也就是以曲線能否達到平滑的地步來做WIP值的調整依據p. 81

知道到底你要交付甚麼、給誰、以及為什麼。 p. 86

『預測客戶的需求』這句話好像是業務人員才會用到的詞彙,怎麼會在看板方法中出現呢?原因有二:第一是因為我們經常在開發一些不是客戶真正想要的功能,另一個是我們做了一堆功能但從頭到尾沒人會去用它;而這二個問題實際上都可以透過看板方法得到改善。 p. 87 [請搭配前一句一起食用]

身分為代表客戶的 PO 可能是最常挑戰 WIP 限制的人物了,每當遇到阻塞,總是有人會後悔當初把 WIP 值設得太小了,而企圖引發一場是不是要立即修改 WIP 值的爭論。 p. 90

請注意,不設置半成品的限額是錯誤的!使用看板方法之前,千萬不要在還沒有看到改善之前就因為擔心會有預測中的混亂情況而先行放棄,這種不設限 WIP 的做法,就是放棄看板方法了。 p. 91

第四章 如何實施看板方法

在設計看板牆的時候,最好將範圍 (Scope)、工作項目粒度 (Granularity) 大小、工作項目狀態 (Status) 這三個元素一起考慮進來。 p. 102

Scrum 團隊經常不事先安排特定的工作給予特定的工程師,... (中略) ...,讓工程是自己去認領,稱之為『全功能的工程師』。 這是很正面的做法,鼓勵大家去學習新東西,它不但可以促進團隊的協作及效能,也能產生相當的激勵作用;而看板方法則是偏向直接分工,也就是讓專業更專業的作法,用來追求更好的效能。 p. 105 [還是可以中庸一點]

看板方法背後有二條基礎性原則:一條是限制 WIP 的數量,另一條是僅當前面的工作欄位有空位的時候,才可以透過拉動系統拉入新的工作項目。 —— David J. Anderson p. 109

半成品數量與前置時間有直接相關,也就是說,當半成品數量減少時,平均前置時間也隨之減少。... (中略) ...半成品數量和平均前置時間之間存有相關性,而且是線性相關,在製造業中這種關係稱為利特爾定理 (Little's Law)。 p. 114

前置時間和品質之間亦存在相關性,前置時間增加,則品質會下降,前置時間越長,品質便會顯著下降。事實上,平均前置時間增加約 6.5 倍,便會導致初始缺陷超過 30 倍的攀升 (很可怕!)。半成品數量越多,平均前置時間越長,因此,提高品質的管理槓桿點 (leverage point) 是減少半成品數量的方法。 p. 115

看板部分的最下面有一個渠道 (swim line),用來支援緊急的工作事項,它的限額是 1,表示最多只能同時處理一件緊急的工作事項。最下方是『回顧改進事項』,指的是將回顧會議的結果做成紀錄,提醒團隊避免下次再犯。 p. 119 [限額是可以更動的,只是書中的例子是 1]

盡量讓工作角色分明是十分重要的。有太多軟體公司都是老闆兼技術總監的形式,非常容易讓工作角色發生混亂,這是十分划不來的事。 p. 122

讓瓶頸能夠盡快暴露出來的數值 (WIP) 最有價值!因為這樣我們就能從這裡開始進行改善的動作,流程當然就會改進了。 p. 130

遇到阻塞時要訓練團隊成員不是去質疑塞住了,我今天就沒事做了!而是主動去詢問我可以幫上什麼嗎? p. 131

第五章 個人看板:類專案管理

因此我們要懂得透過設限自己的工作量來更進一步的了解自己的工作狀況,把它適當的反應在看板上面,能夠讓我們做進一步的動態調整,這一點可以協助我們更正確地完成工作,也可以拿來改善我們做事的效能。 p. 151

類文件也是測試案例,新加入的人員以測試人員的角色進入這個專案,然後在熟悉測試案例後就可以配合 Excel/HTML 的分析文件加入撰寫程式人員的行列了。 p. 174

我常稱它們為『交接給自己的文件』,秉持的精神是任何程式都需要有文件來陪伴它:沒有任何原始碼的程式應該獨自存在,而沒有文件的伴隨。 p. 179 [自己平時也是如此]

第六章 個人看板與生活:讓生活與工作相得益彰

傳統的時間管理概念是:只要提高工作效率,你便能掌握生活,從而內心感覺平和,而且會有成就感。但是,效能並不會為你換來滿足感的,人生也不見得會因產能的增加而變得更美好。 —— 史蒂芬.柯維 《時間有約》 p. 188

事情做到一半被中斷 —— 培養短時間集中精神的能力。 p. 189 [番茄時鐘法其實蠻不錯的]

人們花太多時間,試著找出執行平凡目標的方法。 —— Mark Murphy p. 191

所以一切應該從計畫開始,在個人看板上明確標示出你的目標 (Mark Murphy 強調內心渴望是達成目標的第一要務)。 p. 191

而即使是重複的工作,我們也能夠進行得更有效率,這就是學習的可貴,我們透過經驗的回饋,學習後進行改善的行為,減少了浪費也增加了我們的能力。 p. 193

看板方法是一個很特別的軟體管理方法,因為它很容易激起人在情緒上的變化p. 194

所以我認為個人看板的目的是:讓你透過視覺化你的生活與工作後,試著運用看板方法來找出甚麼才是你生活中最重要的事,排除其他的浪費,多花一些時間在你認為最重要的事情上面,試圖在方向上及範圍上幫助你能夠看清楚,從而改善你的生活。 p. 199

第七章 預測未來:減少變異性,增加可預測度

如果我們計劃得夠詳細了,是不是就能估算的很精確呢?答案是否定的,因為變異性是難以預測的,而你實在很難詳盡地把所有的影響因子都考慮進去,因此要準確預測一件事情的未來是十分困難的一件事,因為會造成變化的因素太多了,這種因素我們就稱它為『變異性 (Variability)』。 p. 205

結構影響行為,也就是說,我們因為沒看見結構是怎麼運作的,而只是一直認為自己不得不這麼做,所以就決定去做了,這是一種『見樹不見林』的盲目決策。 p. 208

為了解決某一問題而制定的策略,通常會使問題更加嚴重,從而形成一種惡性循環,而管理者卻不自知反而更加用力執行這些引發問題的策略。 p. 209

系統思考的基本模式之一所謂的成長上限 (Limits to Growth),意思是說即使某一過程能產生預期的效果,它也會產生某種副作用,從而抵銷所取得的成果,並最終減緩成功的到來。 p. 209

很多組織都忽略了,當問題因為採用新策略而消失之後,這個新策略反而成為今天系統的限制。上面這個現象引出了系統思考的另一個模式捨本逐末 (Shifting the Burden),也就是遇到問題並沒有對症下藥,只是忙於應付問題,看起來是解決了問題,但是實際上是讓問題更難被察覺,反而造成未來更嚴重的問題。 p. 210

David J. Anderson 曾說:『軟體開發和專案管理過程,是以組織的成熟度、團隊中成員的能力,共同決定了內部變異性的數量及變異的程度』。因此請勿將看板方法視為一種軟體開發生命週期 (ALM) 或專案管理的過程,看板方法是一種變革管理的技術。 p. 215

演化輸入展示體驗:有時候複雜性夠多是源自使用者介面而非功能性需求本身,這時候,可以考慮拆分一個故事並用最簡單的介面實現,接下來再建構比較華麗的介面。 p. 218 [個人感想是這一點有時候不見得能成立]

但是它 (User story mapping) 還是有缺點的,當專案範圍太大時,這時候的使用者故事太多太複雜,就會很難做異動及維護的工作 (做起來特別累人),當然也就失去了它的價值。 p. 220

第八章 持續改進

解決瓶頸的處理方式則必須以較全面性的方式來做考量,先經過嘗試,肯定問題的癥結所在,然後再來擬訂持續改善的方法 (這裡才是主管該專注的地方!)。 p. 231

品質是一種很有趣的東西,當你開始注意時,它就已經開始在改善了。 p. 233 [這句話怎麼聽起來很像魔法的發動咒語XD]

一個繁忙的團隊就好像已經盛滿了水的杯子一樣,它是沒有辦法再加進任何東西了!你必須先把盛滿了的杯子空出一些空間來,才可能再往裡頭加東西。所以在你開始進行改革之前,先要設法幫團隊找出空閒的時間,... (中略) ...『半成品』正是那個不會影響產能的工作,說穿了就是減少『半成品』的數量。 p. 234

採用看板方法可以改善組織的文化,並幫助組織走向成熟,形成一種持續改善的文化,這是精實精神造成的文化與組織習慣的改變,比任何一種軟體開發方法影響更為深遠。 p. 237 [這句話要搭配該節標題一起食用:沒有銀子彈]

附錄

如果你的團隊成員都不敢嘗試新的想法,因為他們害怕可能會失敗,你可能是Scrum But。

如果你將數據指標看得比表現優異更重的話,你可能是Scrum But。

如果你凡事都要通過管理階層來做決定的話,你可能是Scrum But。

這本書是在 Agile Tour 2015 聽完作者本人演講後買的,不過,還真的沒挪出什麼時間把它看完,直到最近因為一些因素終於有時間把它看完了,所以上述很多摘錄都是心有戚戚焉。看是看完了,也別因為我上面都把好處寫出來就覺得看板方法好棒棒,一定也可以適用在你的團隊,就好像基金的廣告,投資一定有風險,基金投資有賺有賠,申購前應詳閱公開說明書,想用任何敏捷方法前,也許可以參考一下拙作

 
over 2 years ago

前陣子,在 Facebook 上分享了《Agile is Dead》中文翻譯版,分享時人在公車上,不喜歡用手機打字的我,沒有下任何註解就分享出去了,但後續的發酵倒是令我有點意外。我個人沒有推崇任何方法,就如同我討厭有人跟我推銷宗教一樣。只是在進入職場前跟著老師很早就接觸 RUP 和 agile software development,在職場中也待在 scrum 團隊多年,甚至後來也擔任 scrum master,希望給團隊最大的空間能夠自我組織與成長,也以這目標在努力,因此,若真要問我對這篇文章的感想是什麼?事實上跟文章的內容大致相同,只是標題很聳動,我應該會說 Agile 是個已經被過度炒作的商業名詞,注意,我用大寫開頭,因此常被誤解是銀子彈能解決所有軟體開發中會遇到的問題,但事實上它並不是,要視 context 才能知道哪種方法 (不論是不是 agile 方法) 適合您的團隊。

在沒有內化《Manifesto for Agile Software Development》核心精神前就導入任何方法,就好像練武功只練外功沒練內功會走火入魔一樣,會有很多副作用,因此覺得這方法很 OOXX。至於為什麼導入時總是會先從方法開始而不是先從了解精神開始?我想這跟為什麼練武功都是先從練基本功與招式開始相同,因為要把一個概念內化是很難的,方法是最容易讓人上手可以開始的地方,但練武到後期,若在一個好的師父帶領下,不會讓他的徒弟只練外功不練內功,相同地,如果有好的 agile coach,是會讓團隊慢慢內化宣言背後的精神,只可惜,在商業炒作下,有太多拿著 XXX 證照的講師,說是幫助團隊導入 agile 開發方法,方法教完了就離開,團隊也就照著方法做了,短期可能有些成效,但講師一不在團隊內,團隊無法用內化的思維自己解決問題,也就停滯甚至退步。

若我們用理解 pattern 的方式來理解這些方法,也許應該先從當初這些方法推出時的 context,以及所面對的 forces 有哪些,了解這些方法如何平衡這些 forces,然後才能判斷是否適合用在您的 context 中。不過可惜的是,我沒參與過那場討論出《Manifesto for Agile Software Development》會議,我無法明確知道 context 與 forces,但我在學軟體工程與專案管理時,有個漫畫倒是常常被引用,即便到現在依然是如此:

在那個年代...這說法好像我也很老了,需求常常是軟體開發商與客戶之間永遠的痛,軟體開發者覺得客戶不知道自己在說什麼,客戶覺得軟體開發商總是誤解他們的需求,又或者是,軟體開發商受不了客戶對需求一變再變,但客戶覺得不是他們變更需求,而是軟體開發商根本就做錯了。在這樣的 context 下,有一些方法被提出來,想平衡需求無法明確與變動的 forces,像是 Rational Unified Process (RUP) 或是對我來說惡名昭彰的 CMMI,某種程度上,我認為 use cases 搭配 iterative development 的 RUP,確實是比根本不該用來開發軟體的 waterfall process 要好很多。

只是,當商業業務本身變動越來越快的情況下,對開發中的軟體需求自然也就跟著變動,若不跟著變動,即使做對了做完了,對客戶來說也沒有用處,因為當初的需求現在已經不是需求了。以下是我的猜測,軟體開發商完成了合約中的項目,卻無法幫助客戶;為了確保做對合約中的項目,即使是用 RUP,也做了大量的big up front design,因此對於變更有一定的排斥;因為 RUP 有一定的規範和文件,讓不少人覺得 RUP 很笨重;因為 RUP 通常伴隨著 UML,變成設計師或分析師與工程師之間,只有 diagram 而沒有溝通。所以在現實中需求是易變動的 context 下,為了平衡上述的 forces,宣言誕生了,以及後來的幾種方法 (就我的印象,有些方法誕生其實比宣言要早)。

《Manifesto for Agile Software Development》就這四條 (還包含一段關於如何解釋 over 的描述),看似簡單,但卻是很難達到的目標:

  • Individuals and interactions over processes and tools
  • Working software over comprehensive documentation
  • Customer collaboration over contract negotiation
  • Responding to change over following a plan

即便使用 Scrum 或 Kanban 等方法,都不保證能達到上述的目標。

當軟體開發不再只是工程師的事,需要各種角色 (像是美術與企劃) 的參與,individuals and interactions 就需要考量到不同領域做事方式的不同,取得一個多方都能快樂工作的平衡點,這就很難。

當使用者對軟體的要求不再只是一個不會出錯的 working software 時,如何在設計的過程中考慮並溝通各式各樣的 non-functional requirements,以及如何在時程與完整性之間取得平衡,這就很難。

當開發的產品,其使用者是一般消費者,但需求的決定權是握在一個不在團隊中且很難約時間與他討論需求的老闆時,customer collaboration 就很難。

在有單元測試、持續整合的協助下,responding to change 對軟體工程師來說,確實比過去要輕鬆不少,但成本依舊是有的,但無法用單元測試和持續整合協助的部分呢?像是體驗、動畫、轉場、UI 風格等非功能性需求的變動,對於軟體工程師或非軟體工程師來說,有時候一個變更對他們來說卻可能是地獄般的加班,或是 product owner 得理解需要好幾個 sprints 來處理變更,此時 scrum master 能笑著臉對他們說:Responding to change over following a plan 嗎?很難。

既然很難,那就是這方法不好啊?何需要用?我一開頭說過,我不是個 agile software development 的推崇者,所以也不會推別人入教,更不會見人就說 XXX 方法好棒棒,用了之後專案絕對不延誤,開發人員一定很開心,準時上下班,因為根本沒這種東西。我也認為要做到上述宣言確實很難,要花費很多的心力,才能將核心的精神內化到團隊中。因為專案的屬性,當初團隊組成時,認為 Scrum 方法能幫助團隊順利開發產品,所以用 Scrum 方法一路走自今日。當初在接觸 Scrum 時看了《Agile Software Development with Scrum》這本書,書中用一張圖介紹 agile 方法適合的專案,我依稀還有點印象 (手上沒這本書,感謝 Teddy 贊助圖片),下面是自己的解讀 (沒書可以抄 XD)

  • 若需求不明確且所需技術也不明確,即圖中右上角 chaos 那一塊,不論用什麼方式,風險都很高,只是 agile 方法能更早暴露風險。
  • 若專案需求明確,彼此共識高,所需技術也明確,也就是圖中屬於 simple 的那一塊,用 agile 方法、RUP、CMMI 或 waterfall 都不會差太多,大致都會成功,差在有沒有機制能讓團隊在專案進行中跟著進步。
  • 若專案需求明確,彼此共識高,所需技術不太明確,一般俗稱 R&D,屬於圖中間下方的 complicated 那一塊,waterfall 是不太合適的,因為在做計畫時,很難處理技術上的不明確,較合適的是能容納試誤的方法。
  • 若專案需求沒這麼明確,或是會變動,但所需技術明確,屬於圖中左側中間的 complicated 那一塊,那能盡早讓客戶見到產品,並隨著調整的方法比較合適。
  • 不過實際上的專案,大都會在兩個軸上遊走,也就是灰色的 complex 那一塊,因此能容納試誤與快速應變的方法比較合適。

有人看到這可能就會說:你就是在推銷 agile 方法啊,我可以很肯定說不是,事實上,只要團隊現行的制度能滿足上述的描述,就能在所屬的專案上進行的較順利 (只是比較順利,成不成功還要看很多因素),不用刻意導入什麼特定的 agile 方法,採取 agile 方法可能會對公司現有制度和文化造成衝擊,越是有阻力時硬推行一種方法其實都不太容易成功。重點是能適應所屬的專案生態,讓團隊成員開心積極地去完成專案,並讓客戶滿意,agile 方法只是眾多方法中,不少團隊使用後認為確實幫助他們解決問題的方法。上面那張圖還只是其中一個面向,要不要導入一種方法還要考慮到時程、成本和品質等面向。所以與其說,某方法已死,較合適的說法也許是視情境決定適不適合您的團隊。

幾個月前,我在團隊的 release planning 中安排了幾個影片給團隊成員看,第一個是 Spotify Engineering Culture ,我個人很喜歡這分成上下兩集的短片,裡面有許多可以引起思考的東西:


第二個是 10+ Deploys Per Day: Dev and Ops Cooperation at Flickr,因為產品已經進入準營運階段,我希望透過這影片讓團隊思考未來該如何順暢地營運產品。

本來,我還想安排第三部影片,就是 Dave Thomas (若不知道他是誰,你可以在剛剛的 Manifesto for Agile Software Development 網頁上看到他的名字) 的演講,題目就是《Agile is Dead》,但後來我決定不放進去,就自己的觀察,我們是有幾年 scrum 經驗的團隊,但在團隊成員增加與變動下,整體以『守、破、離』三個階段來看,還在等待破繭而出進入破的狀態,擔心這影片讓團隊往不預期的方向走。

看完影片後的問卷,可以發現團隊從影片中吸收了很多東西,也思考了很多東西。最近,接近 release 周期的尾聲,在我的建議下,團隊使用 Spotify 分享的《How we do large scale retrospectives》的方式,針對未來營運需求,討論團隊組成與制度的調整,會議中,我盡可能保持低調並讓參與的成員發言與討論,也很高興看到成員認真地思考問題並提出見解,幾次來回,與各自小組的成員討論 (在上個 release 週期,決定將一整個約 20 人的團隊拆成 2 個 feature team 與 1 個 OP team),並慢慢整理出一個結論,這過程中,一些《Manifesto for Agile Software Development》的核心精神真正內化到他們的心中。

像是會去思考,這樣的調整會不會只是開發 feature 的成員很開心,但營運的成員很痛苦,會不會只有寫程式的成員很開心,美術成員很辛苦?或是固定 iteration 的週期是不是真的適合不同類型成員的工作模式?一個 story 因固定週期與既有人力被切開後,在某些因素下,後續一些雖然不影響功能性卻影響體驗的小 story 在 priority 上被排到很後面或是被忽略 (我建議過 PO 試試 User Story Mapping 進行追蹤),導致產品雖是個 working software 卻不是一個團隊心目中 high quality 的產品。

可惜的是,新的團隊組成與運行機制,我不會參與到,因為一些因素我決定轉換跑道到別的團隊發展,但我覺得現在的團隊已經算是到『破』的狀態,已經是能 inspect 與 adapt (這是我認為很重要的核心精神) 的團隊,尊重彼此、思考與真心地自我檢討,雖然說還有很多地方能改進,我想這團隊在另一位 agile coach 的相互引導下,應該能順利繼續往下走下去。

最後,要給在考慮是否導入 agile 方法的人建議的話,先熟悉自己的 context 與要面對的 forces,思考後再決定是否導入,不需要為了那個名字而導入。如果想導入 agile,建議初期要找到對的 agile coach 或顧問,並在團隊內培養一位 agile coach,這位 agile coach 需要不斷地吸收內化精神並散播給團隊,讓團隊最後能自己解決問題,不然就很容易進入覺得練功無用,抱怨 XXX 已死的狀態。

後註
其實某某方法已死,這種標題很多,像是 Continuous Integration is Dead 或是 Is DevOps Dead? LogicMonitor Suggests So But I'm Not So Sure,在讀這類文章時,我都建議先釐清作者所處的 context 及想平衡的 forces 有哪些?與他們做過哪些努力和遭遇的困難,再來判斷是否可以套用在自己的 context 中。

參考閱讀

 
over 2 years ago

Translated from "Pushing Data in Both Directions with WebSockets" by Danny Coward, Java Magazine, January/February 2016, page 47-58. Copyright Oracle Corporation.

使用WebSockets雙向推播資料

用WebSockets長效性連線見練間單的聊天軟體

在本系列的首部曲中介紹了WebSockets,基本的WebSocket協定給予我們兩種原生的格式可以使用:文字與二進制資料,對於在客戶端與伺服器端交換簡單資訊的應用程式來說相當好用,例如前篇文章的時鐘應用程式,唯一透過WebSocket訊息互動機制交換的資料只有從伺服器廣播出去的時間資訊(文字格式)以及客戶端用來結束更新的stop字串。但很快地,應用程式有更複雜東西透過WebSocket連線傳送與接收,會發現需要一個結構存放這些資訊,作為Java開發者,我們習慣以物件的形式處理應用程式資料:不論是使用Java API標準的類別,或是我們自己建立的類別,這表示當您被Java WebSocket API低階的訊息功能困住,和想用物件寫程式而不是用字串或位元陣列時,您需要寫程式將字串或位元陣列轉換成您的物件,反之亦然,我們來看怎麼做吧!

幸運地,Java WebSocket API支援將物件編碼成WebSocket訊息與從WebSocket訊息解碼回物件的任務,首先,Java WebSocket API試圖將訊息轉成您請求的Java基礎型別(或等效的類別),這意味您可以宣告一個訊息處理函式像這樣

@OnMessage
public void handleCounter(int newvalue) {...}

或這樣

@OnMessage
public void handleBoolean(Boolean b) {...}

然後Java WebSocket會試圖將傳進來的訊息轉成宣告的參數型別[譯註:intBoolean]。

相同地,RemoteEndpoint.Basic的傳送函式包含一個通用的函式:

public void sendObject(Object message) throws IOException, EncodeException

讓您可以傳入任何Java基礎型別或等效的類別,Java WebSocket的實作會為您將值轉換成等效的字串。

這只能讓您到這裡,通常,您想要用更高層次、更結構化的物件來傳遞您應用程式中的訊息,要在訊息處理函式中處理自訂的物件,您須為端點提供一個WebSocket的Decoder實作,讓執行環境用來將進來的訊息轉換成自訂的物件,要送出自訂的物件,您同樣需要提供Encoder的實作,讓執行環境將自訂物件轉換成原生的WebSocket訊息,整個流程總結如Figure 1

Figure 1. Encoders and decoders

Figure 1的上方呈現端點與客戶端交換字串,下方則是使用編碼器與解碼器將Foo物件編成文字訊息與反解的過程。

Java WebSocket API有一系列的javax.websocket.Decoderjavax.websocket.Encoder介面可以選擇,根據您想製作什麼形式的轉換。例如,想實作一個Decoder將文字訊息轉換成稱作Foo的自訂物件,您可以用Foo作為泛型型別實作Decoder.Text<T>介面,然後提供這個函式的實作:

public void sendObject(Object message) throwsIOException, EncodeException

這任勞任怨的解碼器函式會在每次有新的文字訊息送進來時被呼叫,將訊息轉成Foo型別的實體,然後執行環境會將這實體送進端點的訊息處理函式中。其他Decoder類別可用來轉換二進制WebSocket訊息,訊息會以同步式(blocking) I/O串流的形式送入。

要實作Encoder將自訂的類別Foo物件轉成WebSocket文字訊息,可以用Foo作為泛型型別實作Encoder.Text<T>介面,然後提供這個函式的實作:

public String encode(Foo foo) throws EncodeException

如果您呼叫RemoteEndpointsendObject()函式(如前所述)傳送Foo實體,Java WebSocket執行環境會需要這個編碼器這會將Foo實體轉成字串。如同DecoderEncoder也有不同型態,可以轉換自訂物件成為二進制訊息,將自訂物件以同步的(blocking) I/O串流方式傳送。

如我們所見到的@ClientEndpoint@ServerEndpoint定義,如果您想用,這機制相當容易與端點連結,您可以單純在端點的decoders()encoders()參數中分別列上您想使用的解碼器與編碼器實作。如果,您為Java基礎型別設定客製的編碼器與解碼器,他們將會取代執行環境為這些型別提供的預設編碼器與解碼器,如同您所預期的。

訊息處理模式

到目前為止,我們只討論一次只傳送或接受一整個WebSocket訊息,即使多數應用程式因為他們的應用協定指定義小量的訊息,保持用這簡單的模式,但有些應用程式需要處理大量的WeSocket訊息,像是傳送圖片或大的文件,Java WebSocket API提供數種處理模式優雅並有效率地處理大量的訊息。

接收大量訊息 Java WebSocket API有二種額外的模式用來接收訊息,適用在當您知道會是大量訊息的情境。第一種模式讓端點直接處理同步I/O API,因此可以用java.io.Reader接收文字訊息或用java.io.InputStream接收二進制訊息。使用這模式,訊息處理函式參數不再是StringByteBuffer,您將使用ReaderInputStream,例如:

@OnMessage
public void handleMessageAsStream(InputStream messageStream, Session session) {
    // read from the messageStream

    // until you have consumed the

    // whole binary message

}

第二種模式提供一種基本切割API,WebSocket的訊息以小片段搭配一個boolean旗標傳給訊息處理函式,用旗標識別後續是否還有其他片段已完成整個訊息,當然,訊息片段會以既定的順序抵達,也不會混入其他訊息的片段。使用這種模式,訊息處理還是多一個boolean參數,例如:

@OnMessage
public void handleMessageInChunks(String chunk, boolean isLast) {
    // reconstitute the message

    // from the chunks as they arrive

}

在這模式,每個片段的大小取決於幾個傳訊訊息的對象與Java WebSocket執行環境設定等相關因素,只需知道您會以多個片段的方式收到完整的訊息。

傳送訊息的模式,如您可能預期的,WebSocket協定的對稱性,Java WebSocket API有相同模式適合用來傳送大量的訊息。除了如前所見一次傳送一整個訊息,您也可以用同步式(blocking)的I/O串流來傳送訊息,用java.io.Writerjava.io.OutputStream傳送文字訊息或二進制內容。當然,可以從RemoteEndpoint.Basic介面取得的Session物件獲得額外的函式:

public Writer getSendWriter() throws IOException

public OutputStream getSendStream() throws IOException

第二種模式是切割模式,但相反地,是用來傳送。同樣,一個端點能用RemoteEndpoint.Basic的以下函式以此模式傳送訊息:

public void sendText(String partialTextMessage, boolean isLast) throws IOException

public void sentBinary(ByteBuffer partialBinaryMessage, boolean isLast) throws IOException

根據您希望傳送的訊息類型。

非同步式傳送訊息,WebSocket的訊息送達通知總是非同步的,一個端點通常不會知道訊息甚麼時候送達,訊息總是在另一端選擇時出現。到目前為止,RemoteEndpoint.Basic介面中我們已經見過的所有用來傳送訊息的函式都是同步式傳送,簡單來說,這意味著send()函式的執行會等到訊息確實送達。這對小訊息很合適,但如果訊息量很大,WebSocket可做更好的事情而不是在等待訊息傳送完畢,像是傳送訊息給其他人、更新使用者介面或專注在處理更多傳送進來的訊息。針對這樣的端點,從Session物件取得RemoteEndpoint.Async,如同RemoteEndpoint.Basic,有不少種send()函式將一整個訊息作為參數(有不同型式),在訊息實際傳送前,它們會立即回傳。例如,當傳送一則大量的文字訊息您可以使用:

public void sendText(String textMessage, SendHandler handler)

這函式立即回傳,作為第二個參數的SendHandler會收到通知當訊息實際被傳送。如此,您會知道訊息被送出,但您不需要等待它確實完成。或者,您想周期性地檢查非同步傳送的進度,例如可以選擇此函式:

public Future<Void> sendText(String textMessage)

在這情況,函式在訊息傳送前立即回傳,您可以對回傳的Future查詢訊息傳送的狀態,甚至如果你改變主意,可以取消傳送。當然,如您預期,有相同的函式傳送二進制訊息。

在我們結束Java WebSocket API的主題前,有一點值得提出來:WebSocket協定本身並沒有保證送達的概念,換句話說,當您傳送一則訊息,您不知道客戶端是否確實收到,如果,您在錯誤處理函式中收到一個錯誤,它通常是一個訊息沒有被完整送達的明確信號,但如果沒有錯誤,訊息仍然可能被完整送達。您有可能需要用Java WebSocket建立互動,對重要的訊息,另一端需要送回一個確認通知,但是,不像其他傳訊協定,像是JMS,本身沒有任何送達的保證[譯註:WebSocket沒有]。

路徑對應

在時鐘的例子中,只有一個端點,對應到整個Web應用程式URI空間中單一個相對的URI,客戶端用一個URL,用應用程式的URI加上此端點的URI連到此端點,這正是一個Java WebSocket API路徑對應的例子。一般來說,一個端點可以從像這樣的URL存取

<ws or wss>://<hostname>:<port>/<web-app-context-path>/<websocket-path>?<query-string>

其中,<websocket-path>@ServerEndpoint annotation的屬性,而query-string是選擇性的查詢字串。當<websocket-path>是一個URI,如ClockServer端點般,只有用此URI發出請求才會連到此端點。

Java WebSocket API能讓伺服器端的端點對應到URI範本,URI範本是一種別緻的方式讓URI可以有幾個片段可以用變數替換,例如:

/airlines/{service-class}

是一個URI範本,有一個變數稱作service-class

Java WebSocket API允許URI的請求對應到一個URI範本,如果此請求URI合乎該URI範本,例如:

/airlines/coach
/airlines/first
/airlines/business

都合乎URI範本。

/airlines/{service-class}

變數service-class分別是coachfirstbusiness

在WebSocket應用中,URI範本相當有用,因為範本中的變數可在端點中使用,在伺服器端中任何生命週期處理函式中,都可以加任意數量的字串參數加註@PathParam取得路徑中的變數,延續這個例子,假設我們有Listing 1的伺服器端點程式:

Listing 1. 一個訂閱通知的端點
@ServerEndpoint("/air1ines/{service-class}")
public class MyBookingNotifier {

    @OnOpen
    public void initializeUpdates(Session session, @PathParam("service-class") String sClass) {
        if ("first".equals(sClass)) {
            // open champagne

        } else if ("business".equals(sC1ass)) {
            // heated nuts

        } else {
            // don't bang your head on our aircraft

        }
    }
    ...
}

根據客戶端請求的URI,能提供多種不同層級的服務。

在執行環境中取得路徑資訊,一個端點能在執行環境中完整取得自身的路徑資訊。首先,它總是能取得在WebSocket容器中發布的路徑,您能在任何能取得ServerEndpointConfig實體的地方使用ServerEndpointConfig.getPath()取得這資訊,如Listing 2所示。

Listing 2. 端點可以取得自己的路徑對應
@ServerEndpoint("/travel/hotels/{stars}")
public class HotelBookingService {

    public void handleConnection(Session s, EndpointConfig config) {
        String myPath = ((ServerEndpointConfig) config).getPath();
        // myPath is "/travel/hotels/{stars}"

        ...
    }
}

這方式同樣適用URI路徑對應的端點上[譯註:指非URI範本也能用這方式取得路徑]。

第二種資訊您也許想在執行期間知道的是客戶端是以何URI連到此端點,這資訊有幾種形式,我們接下來會看到,但此函是能取得所有資訊:

Session.getRequestURI()

此函是給您相對於伺服器跟路徑的URI,注意的是,這包含此端點的Web應用程式的環境路徑,所以,以訂飯店的例子,若布署到環境路徑是/customer/services的Web應用程式,則客戶端會以此URI連到HotelBookingService

ws://fun.org/customer/services/travel/hotels/3

則呼叫getRequestURI()會得到

/customer/services/travel/hotels/3

Session另有二個函式解析請求的URI取得更進一步的資訊當URI包含查詢字串,所以我們看一下查詢字串。

查詢字串與請求參數,如我們之前見到,查詢參數在連到一個WebSocket端點的URL是選擇性的。

<ws or wss>://<host:name>:<port:>/<web-app-context-path>/<websocket-path>?<query-string>

在URI中的查詢字串原先是從common gateway interface (CGI)應用程式開始大量使用,URI路徑中的片段指向CGI程式(通常是/cgi-bin),連接在URI之後的查詢字串提供一連串的參數讓CGI程式確認請求。查詢字串同樣常用在傳送HTML表單的資料,例如,一個網站應用程式的HTML程式:

<form
  name="input"
  action="form-processor" method="get">
  Your Username: <input type="text" name="user">
                 <input type="submit" value="Submit">
</form>

點擊Submit按鈕會送出一個HTTP請求到以下的URI:

/form-processor?user=Jared

相對於HTML程式頁面的位置並將輸入欄位的文字Jared送出。根據/form-processor路徑所在的資源特性,查詢字串user=Jared可以用來決定回傳何種結果。

例如,假設form-processor的資源是一個Java servlet,可以從HttpServletRequest呼叫getQueryString()取得查詢字串。

相同的精神,查詢字串可以用在連到使用Java WebSocket API建立的WebSocket端點的URI上,Java WebSocket API不會用URI中的查詢字串作為開啟連線時決定應該連到哪個端點,換句話說,不論URI中是否有查詢字串,都不會影響對應到哪個伺服器端端點的發佈路徑,此外,查詢字串在發佈時在路徑中是被忽略的。

就如同CGI程式或其他網頁元件,WebSocket端點可以用查詢字串進一步設定客戶端建立的連線。因為WebSocket的實作實際上忽略進來的請求中查詢字串的值,如何使用查詢字串的任何邏輯完全是在WebSocket元件中,取得查詢字串的函式主要都在Session物件中:

public String getQueryString()

此函式回傳完整的查詢字串(從?字元開始的全部字串),而此函式:

public Map<String,List<String>> getRequestParameterMap()

可以取得從查詢字串解析後的資料結構包含所有請求參數,注意到從map取得的值是一個字串的list,這是因為二個參數可能有相同的名字但不同的值,例如,您可能用此URI連到HotelBookingService端點:

ws://fun.org/customer/services/travel/hotels/4?showpics=thumbnails&description=short

在這情況,查詢字串是showpics=thumbnails&description=short,然後取得請求參數,做某些事情如Listing 3所示:

Listing 3. Accessing request parameters
@ServerEndpoint("/travel/hotels/{stars}")
public class HotelBookingService2 {

    public void handleConnection(Session session, EndpointConfig config) {
        String pictureType = session.getRequestParameterMap().get("showpics").get(0);
        String textMode = session.getRequestParameterMap().get("description").get(0);
        ...
    }
    ...
}

其中pictureTypetextMode的值分別會是thumbnailsshort

您同樣可以從請求的URI取得查詢字串,在Java WebSocket API中,Session.getRequestURI的結果總是包含URI和查詢字串。

伺服器端點的布署

布署Java WebSocket端點到Java EE容器遵循簡單的事就是簡單的規則,當您將加註@ServerEndpoint的Java類別打包成WAR檔,實作Java WebSocket的容器會掃描WAR檔,然後找到所有這樣的類別並布署它們。這意思是您除了將它們打包成WAR檔外,不需要做什麼特別的事來布署您的伺服器端點。然而,您也許希望緊緊控制布署WAR檔中那些伺服器端點,這情況下,您可以提供一個javax.websocket.ServerApplicationConfig介面的實作,讓您過濾那些端點要布署。

聊天應用程式

一個測試推送技術的好方式是建立一個有來自許多用戶端頻繁的非同步更新的應用程式,聊天應用程式正是這樣的案例,讓我們開始看一下如何應用所學的Java WebSocket API來建立一個簡單的聊天應用程式。

Figure 2呈現聊天應用程式的主視窗,當您登入時提示輸入使用者名稱。

Figure 2. 登入開始聊天

幾個人可以同時聊天,在底部的文字輸入框中輸入他們的訊息,點擊送出按鈕,您可以在右側看到目前在線的使用者,然後在中間左側看到每個人的訊息紀錄,在Figure 3中,三個人有不太愉快的對話[譯註:要看圖裡的對話才知道]。

Figure 3. 聊天全程

Figure 4中,我們可以看到其中一人突然離開,而其他人稍微較優雅地離開,只剩下一個人在聊天室。

Figure 4. 離開聊天室

在我們開始詳細檢視程式前,我們先看如何建構這個應用程式的大藍圖,網頁使用JavaScript WebSocket客戶端傳送與接收所有聊天訊息,只有一個Java WebSocket端點ChatServer在網站伺服器處理所有從多個客戶端送來的聊天訊息、追蹤那些用戶端仍在線上、維護對話紀錄、以及對所有連線的客戶端廣播更新不論是誰加入、離開或是任何人隨時送新的訊息到這群組,這應用程式使用自訂的WebSocket EncodersDecoders建立聊天訊息的模型。

我們看一下Listing 4中的ChatServer端點。[因為長度的關係,程式碼片段可從本期的下載區下載]

這程式中有許多要注意的,首先,這端點對應到相對的URI:/chat-server,並分別使用ChatEncoderChatDecoder作為編碼器與解碼器。

第一次了解Java WebSocket最好的方法是觀察生命週期函式,如您所知,就是那些加註@OnOpen@OnMessage@OnError@OnClose的函式,我們可以用這方式觀察ChatServer類別,首先,當有新的客戶端連到ChatServer端點時,會準備一個實體變數參考到對話紀錄(transcript)、session和EndpointConfig。記住,每個連線的客戶端都有一個新的實體,所以,每個聊天室中的成員都會有一個獨自的chat server實體。每個WebSocket邏輯端點總會有一個EndpointConfig,所以每個ChatServer實體的endpointConfig變數指向單一共用的EndpointConfig實體。這實體是個singleton,且它持有一個map可以放任一應用程式狀態,因此,它是個存放一個應用程式全域狀態的好地方。每個客戶端連線總會有一個獨立的session物件,所以每個ChatServer實體指向自己的Session實體代表連線的客戶端,並與Listing 5所列的Transcript類別建立關聯:

Listing 5. The Transcript class
import java.util.ArrayList;
import java.util.List;
import javax.websocket.*;

public class Transcript {

    private List<String> messages = new ArrayList<>();
    private List<String> usernames = new ArrayList<>();
    private int maxLines;
    private static String TRANSCRIPT_ATTRIBUTE_NAME = "CHAT_TRANSCRIPT_AN";

    public static Transcript getTranscript(EndpointConfig ec) {
        if (!ec.getUserProperties().containsKey(TRANSCRIPT_ATTRIBUTE_NAME)) {
            ec.getUserProperties().put(TRANSCRIPT_ATTRIBUTE_NAME, new Transcript(20));
            return (Transcript) c.getUserProperties().get(TRANSCRIPT_ATTRIBUTE_NAME);
        }
    }

    Transcript(int maxLines) {
        this.maxLines = maxLines;
    }

    public String getLastUsername() {
        return usernames.get(usernames.size() -1);
    }

    public String getLastMessage() {
        return messages.get(messages.size() -1);
    }

    public void addEntry(string username, String message) {
        if (usernames.size() > maxLines) {
            usernames.remove(0);
            messages.remove(0);
        }
        usernames.add(username);
        messages.add(message);
    }
}

我們可以看到每個EndpointConfig有一個transcript實體,換句話說,只有一個Transcript實體,與所有ChatServer實體分享,這是好的,因為我們需要這實體顯示團體的聊天訊息訊給所有的客戶端。

ChatServer最重要的函式是訊息處理函式也就是加註@OnMessage的函式,您可以看函式的宣告是處理一個ChatMessage物件,而不是文字或二進制WebSocket訊息,感謝它所使用的ChatDecoderChatDecoder將訊息轉成ChatMessage的子類別,為了簡潔,不列出所有ChatMessage子類別的程式,Table 1總結ChatMessage的子類別和各別的用途。

ChatMessage子類別 用途
ChatUpdateMessage 帶有使用者名稱與該使用者送出的聊天訊息
NewUserMessage 帶有新登入的使用者名稱
UserListUpdateMessage 帶有現在在線上的使用者名稱列表
UserSignoffMessage 帶有離開的使用者名稱

Table 1. ChatMessage子類別

現在我們可以簡單地看ChatServer的訊息處理函式,每當客戶端有新的動作發生都會呼叫handleChatMessage()函式,用來處理新使用者登入、發佈一則聊天訊息與使用者登出的情境。

ChatServer被告知一則新聊天訊息發佈,隨著程式流程,handleChatMessage()導向processChatUpdate()函式,該函式呼叫addMessage()將新的聊天訊息加到共用的transcript,然後呼叫Listing 6中的broadcastTranscriptUpdate()函式:

Listing 6. Broadcasting a new chat message
private void broadcastTranscriptUpdate() {
    for (Session nextsession : session.getOpenSessions()) {
        ChatUpdateMessage cdm = new ChatUpdateMessage(this.transcript.getLastUsername(), this.transcript.getLastMessage());
        try{
            nextsession.getBasicRemote(}.sendObject(cdm};
        } catch (IOException | EncodeException ex) {
            System.out.println("Error updating a client : " + ex.getMessage());
        }
    }
}

這函式使用非常有用的Session.getOpenSessions(),允許一個端點實體取得連到該邏輯端點的所有開啟中的連線,在這例子中,這函式用這開啟中的所有連線來廣播有新聊天訊息給所有的客戶端以更新他們的畫面顯示該聊天訊息,注意到,聊天訊息是以ChatMessage的形式送出,即ChatUpdateMessageChatEncoder會處理將ChatUpdateMessage轉成實際被送給客戶端的文字訊息,並將聊天訊息包在裡面。

由於在處理送進來的訊息時,我們沒細看ChatDecoder,我們暫停一下,看一下於Listing 7ChatEncoder類別。

Listing 7. The ChatEncoder class
import java.util.Iterator;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class ChatEncoder implements Encoder.Text<ChatMessage> {

    public static final String SEPARATOR = ":";

    @Override
    public void init(EndpointConfig config) {}

    @Override
    public void destroy() {}

    @Override
    public String encode(ChatMessage cm) throws EncodeException {
        if (cm instanceof StructuredMessage) {
            String dataString = "";
            for (Iterator itr = ((StructuredMessage) cm).getList().iterator(); itr.hasNext(); ) {
                dataString = dataString + SEPARATDR +
                itr.next();
            }
            return cm.getType() + dataString;
        } else if (cm instanceof BasicMessage) {
            return cm.getType() + ((BasicMessage) cm).getData();
        } else {
            throw new EncodeException(cm, "Cannot encode messages of type: " + cm.getC1ass());
        }
    }
}

您可以看到ChatEncoder類別被要求實作Encoder的生命週期函式:init()destroy(),雖然這編碼器在容器呼叫時沒做任何事,但別的編碼器可能選擇在這些生命週期函式中初始化與釋放昂貴的資源,encode()函式裁示此類別主要的部份,將訊息實體轉換成可以傳送給客戶端的字串。

encode()函式才是此類別主要的部份,將訊息實體轉換成可以傳送給客戶端的字串。

現在回到ChatServer類別,我們可以看到在handleChatMessage()函式中,這端點優雅地處理當客戶端的登出:再關閉連線前,送一個UserSignoffMessage,這同樣優雅地處理客戶端單方面關閉連線,例如關閉瀏覽器或瀏覽別的網頁。加註@OnCloseendChatChannel()函式廣播一個訊息給所有連線的客戶端,告知他們有人不告而別離開聊天室。回頭看截圖,我們可以看到Jess和Rob離開聊天室的不同。

結論

在這兩回的文章中,我們學會如何建立Java WebSocket端點,我們探討了WebSocket協定的基礎概念,與什麼情境伺服器能推送資料的能力,我們研究Java WebSocket端點的生命週期,試驗了Java WebSocket API的幾個主要類別,也研究了編碼與解碼的技巧,包含Java WebSocket API所支援的各式傳訊息模式。我們觀察伺服器端的端點如何被應對到網站應用程式的URI空間及客戶端得請求如何應對到端點。我們用一個聊天應用程式總結,展現Java WebSocket API的許多特點,在這些知識的幫助下,現在您可以輕易地建立有持續連線的應用程式了。

This article was adapted from the book Java EE 7: The Big Picture with kind permission from the publisher, Oracle Press. [譯註:這我就不翻譯了,應該不影響對本文的了解吧!(笑)]

LEARN MORE
Oracle’s Java WebSockets tutorial
Long polling, a WebSocket alternative (拙譯)

譯者的告白
老實說,翻這篇文章時,Java Magazine的三月/四月雙月刊已經出來了,最近事情超多,之後真的只能挑自己有興趣的文章翻了,無法整期都翻譯,還是有人想加入共筆呢?如果有,也許可以考慮改轉往GitBook?

 
over 2 years ago

唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。事實上,這也不是我第一個這樣說的,有句軟體工程諺語是這樣說的:

Why is it that some software engineers and computer scientists are able to produce clear, elegant desings and programs, while others cannot? Critical to these questions is the notion of abstraction. (為何有些軟體工程師與電腦科學家能夠產生清楚而且優美的設計與程式,但其他人卻不能?關鍵在於抽象觀念。)

Jeff Kramer CACM 50(4) 2007

釐清問題

釐清問題是讓自己能解決對的問題的第一步,當一開始什麼都不想,根據使用者或顧客一個模糊的需求或是想法就開始埋頭苦寫,即便程式寫好了也不一定解決對方真正的問題,因此有一個說法是找出問題背後的問題,使用者提出一個問題,通常是在現實生活中遇到困難,但在描述時,卻不一定能精確的描述問題(這當然不能當著顧客的面說),或是把背後的問題給描述出來,所以在理解需求的過程中,是要去幫使用者找出真正的問題。不過,我不打算在這裡說明釐清問題的方法。

拆解問題

當釐清真正的問題後,問題有時很大,有時很小,問題小也許就可以開始找尋解法,但問題很大時,會像毛線球般糾結很難好好處理,所以應該先試著將大問題拆解成小的問題,然後再根據每個小的問題去尋找解法。舉個例子,雖然現在網路就像空氣一樣,幾乎成為生活中不可或缺的一個元素,即便如此,我們還是可以問一個問題:當我們輸入一個網址後,電腦是如何呈現這個網頁?

這樣一個大問題可以被拆解成好幾個小問題:

  • 瀏覽器是怎麼知道一個網址對應到網路上哪一個伺服器?
  • 瀏覽器的請求是如何送到伺服器的?
  • 當伺服器知道某人想看某個網頁時,網頁是以什麼形式回到當初請求的電腦?
  • 當瀏覽器收到伺服器的內容又要如何呈現網頁?

而上述這些問題其實都還很大,還可以再被拆成更多小的問題,在過去許多人的努力下,定義成一個七層的OSI網路模型,每一層都提供一個功能(或換個說法解決一個特定問題),例如:

  • 屬於應用層的HTTP協定,讓伺服器能知道使用者想要看什麼文件(網頁)並回傳指定的文件
  • 屬於傳輸層的TCP協定,建立瀏覽器與伺服器之間一個虛擬連線[1],並負責確保傳輸資料的完整性
  • 屬於網路層的IP協定,為網路上每個節點提供地址,並負責將資料在眾多網路節點中繞送到正確的節點
  • 屬於實體層的WiFi協定[2],負責將電腦的數位訊息能夠在空氣中用電波傳輸,並檢查接收的數位訊號完整性

這種divide and conquer正是電腦科學解決問題的方式,但這跟抽象又有什麼關係?事實上每一層都是提供一個抽象,例如虛擬連線就是一個抽象的概念,對於使用TCP的HTTP而言,具體的細節則不需要知道,但HTTP知道,它想要送的訊息在TCP提供的虛擬連線中是保證會送到的,怎麼送達的細節則是TCP的責任(具體化)

解決問題

事實上,如何解決問題或是如何邏輯思考?與電腦無關,在沒有電腦之前,我們有數學公式、物理公式、化學公式與機械等,很多的問題其實也都能被解決,只是可能需要很大量的人,或是無法快速的得到想要的結果,因此,當要用電腦解決問題時,需要的是計算思維(computational thinking),讓解決問題的方法是可以用電腦去計算的,就像前陣子很熱門的Alpha Go圍棋大戰,先要做的是替圍棋找出一個模型讓電腦能夠計算,這其實需要的就是抽象化能力。

當問題能被計算,但可能不夠快,例如下一步棋需要一天或一個小時,這時候需要另一種演算法思維(algorithmic thinking),透過特殊設計的資料結構,以及找出能讓電腦做更少的計算就能得到結果的演算法,讓電腦能更快的解決問題,類似的例子像是影像壓縮和解壓縮是個抽象概念,可以用更少的網路頻寬傳送更高畫質的影片,而具體的演算法,H.265則可以比H.264有更好的效率與影片品質。

抽象化

說了這麼多,終於又回到抽象化這個詞,抽象化可以用在好幾個不同層面,像剛剛提到的OSI網路模型和影片壓縮都是提供抽象概念。但實際寫程式時,抽象化是讓程式容易閱讀的關鍵,畢竟大部分時間讀程式的是人而不是電腦,所以這讓我想起一句話:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (隨便找個傻瓜都能寫出電腦能懂的程式碼。好的程式設計師寫人能看得懂的程式碼。)

Martin Fowler

當軟體持續開發,維護程式碼比開發新程式碼要更傷腦筋,如何讓後續的開發者讀程式像是讀文章般容易懂,重要的就是能用問題domain中的術詞(term)來描述程式。舉個例子,假設有一個my-book-store的網路書店,提供若干REST API讓客戶端可以使用:

取得Isaac Asimov的科幻類作品
GET http://my-book-store.com/books?category=science-fiction&author=Isaac%20Asimov

在編號19333910書籍新增一筆評論
POST http://my-book-store.com/books/19333910/comments

修改編號13332144書籍的資訊
PUT http://my-book-store.com/books/13332144

刪除編號19333912的第12筆評論
DELETE http://my-book-store.com/books/19333912/comments/12

以Java來說,想要使用REST API,客戶端可以用Socket建立連線到my-book-store,準備好HTTP協定相關的標頭與內容,然後傳送給伺服器然後再取得結果,但我想很多人都知道:Socket是作業系統或JVM提供給軟體開發者操作TCP的API,對於我們要做的功能來說太低階了。事實上,Java有提供HttpURLConnection讓開發者可以直接建立HTTP連線,因此可以用Listing 1的程式呼叫REST API取得Isaac Asimov的科幻類作品(範例程式中未處理所有可能拋出的例外)。

Listing 1. 用HttpURLConnection呼叫REST API取得Isaac Asimov的科幻類作品
public List<Book> getBooks(String category, String author) {
    List<Book> books = new ArrayList<Book>;
    String urlString = String.format("https://my-book-store.com/books?category=%1$s&author=%2$s", category, author);
    URL url = new URL(URLUtils.encode(urlString));
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    ObjectMapper mapper = new ObjectMapper();
    try (InputStream stream = connection.getInputStream()) {
        String content = IOUtils.toString(stream);
        books.addAll(mapper.readValue(content, new TypeReference<List<Book>>(){}));
    }
    return books;
}

可以用Listing 2的程式呼叫RES API在編號19333910書籍新增一筆評論,但寫到這,是否發現有太多與Listing 1重複的程式?而這些程式其實還是在處理很多低階的IO處理。也許需要另一層抽象可以跟REST伺服器溝通,而不用去處理實作細節。

Listing 2. 用HttpURLConnection呼叫REST API在編號19333910書籍新增一筆評論
public boolean postComment(int bookId, String comment) {
    String urlString = String.format("https://my-book-store.com/books/%1$s/comments", String.valueOf(bookId));
    URL url = new URL(URLUtils.encode(urlString));
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("POST");
    connection.setDoOutput(true);
    try (OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
        writer.write(content);
        writer.flush();
    }
    return connection.getResponseCode() == 200;
}

所以,我們可以觀察一下上述四個API不同的地方與相同的地方,可以發現如下,我們的抽象層需要能指定伺服器的位置(host),API的路徑(path),路徑上可能有參數可以設定(path parameters),額外可以夾帶查詢參數(query parameters),最後,最重要的是可以指定呼叫的方法(HTTP Method)。

HTTP Mehotd https://host/path/[{path parameters}][?query parameters]

因此,我們可以根據剛剛的描述設計一個RestClient,直接來看例子,Listing 3與Listing 4直接以RestClient改寫getBooks(category, author)postComment(bookId, comment)函式,是否與原先的Listing 1和Listing 2讀起來,在語意上是不是有完全不同的感受?

Listing 3. 以RestClient改寫getBook函式
public List<Book> getBooks(String category, String author) {
    RestClient client = RestClient.hostOn("http://my-book-store.com");
    List<Book> books = client.get("/books")
        .query("category", category)
        .query("author", author)
        .results();
    return books;
}
Listing 4. 以RestClient改寫postComment函式
public boolean postComment(int bookId, String comment) {
    RestClient client = RestClient.hostOn("http://my-book-store.com");
    return client.post("/books/{bookId}/comments")
        .where("bookId", String.valueOf(bookId))
        .content(comment)
        .isSucceeded();
}

有了RestClient,要實作updateBook(bookId, updates)deleteComment(bookId, commentId)是不是也變得很容易,或許,有人只是覺得透過封裝,可以重複使用程式碼,但對我來說,這並不是主要的目的,Listing 5與Listing 6讀起來是否開始有感覺了呢?透過結合domain的術語與Fluent Interface,其實我們已經完成了一個為REST API設計的Domain Specific Language [3]。

Listing 5. 以RestClient實作updateBook函式
public boolean updateBook(int bookId, Map<String, String> updates) {
    RestClient client = RestClient.hostOn("http://my-book-store.com");
    return client.put("/books/{bookId}")
        .where("bookId", String.valueOf(bookId))
        .content(updates)
        .isSucceeded();
}
Listing 6. 以RestClient實作deleteComment函式
public boolean deleteComment(int bookId, int commentId) {
    RestClient client = RestClient.hostOn("http://my-book-store.com");
    return client.delete("/books/{bookId}/comments/{commentId}")
        .where("bookId", String.valueOf(bookId))
        .where("commentId", String.valueOf(commentId))
        .isSucceeded();
}

當軟體越開發越大,為軟體進行模組化是絕對必要的,而抽象化也是模組設計所需要的一項很重要的能力,我們可以將剛剛四個函式包裝成一個MyBookStoreService,因此,使用起來就像Listing 7所示,讀起來語意上又提高一個層級,對使用者來說,也已經不知道底層使用的是REST API。

Listing 7. 使用MyBookStoreService
MyBookStoreService service = new MyBookStoreService();
List<Book> books = service.getBooks("science-fiction", "Isaac Asimov");
service.postComment(19333910, "I like it!");

如同OSI網路模型一樣,軟體的開發是透過一層又一層的抽象堆疊完成,解決各種不同層次的問題(前提是問題已經先被拆解),因此有個說法:

All problems in computer science can be solved by another level of indirection.

David Wheeler

但要怎麼提升抽象化的能力呢?以自己的經驗,首先,先試著讓自己能寫出有條理的文章,因為對現在的我來說,寫程式其實是在寫文章,寫讓其他人看得懂的文章,所以透過寫文章訓練自己的如何思考與如何整理思緒,是非常有幫助的(這真要感謝我的指導教授在我寫碩博士論文時的訓練)。再來,多看別人好的程式碼,瞭解其中使用哪些design principles、design pattern和architecture pattern,以及背後使用的意圖,耳濡目染久了,就會有自己對於事物抽象化的想法(是的,抽象化沒有標準答案的,只有合不合適解決問題與否和是否容易理解的差別)。

結語

語意的抽象化可以說從過去到現在都仍然是進行式,從最早期用打卡機寫程式,到後來可以用組合語言寫程式,到能用C寫程序導向的程式,演變到可以用Java/C#等語言寫物件導向的程式,甚至最近很流行的用functional paradigm的概念寫程式,每次演進都在提高程式語言的抽象程度,到最後每個domain都會有自己的domain specific language,讓原先domain的人可以讀懂程式。

其實要開發大型的軟體,不論是在學界或在業界,真正好的軟體開發者要具備的要素還有很多,像是能與同儕溝通、引領思考、軟體工程實務(開發流程、建構管理等)的實踐,都會讓一個好的軟體開發者與一個差的軟體開發者在效率上差上好幾倍(有一說是10倍,但我找不到出處),但就以平日每天在寫程式的層級來看,抽象化能力是最不可缺少的一項能力。寫了這麼多,我想如果再有人問我:怎樣寫好程式?或是怎麼能成為一個好的軟體開發者?這文章是我目前能給的最好說法,希望對想學習軟體開發的人有幫助。

後話

最近寫程式變得很熱門,連歐巴馬總統都在學寫程式,再過幾年,從國中開始,學生也開始要學寫程式,這讓我想起一句在軟體界的幽默諺語:

如果你想毀掉一個人的一天,就給他一個程式; 如果你想毀掉一個人的一生,就教他寫程式。 (If you give someone a program, you will frustrate them for a day; if you teach them how to program, you will frustrate them for a lifetime.)

如果真是如此,那我們正在進行毀掉全國學生的計畫(笑)?

附註

[1] 實際上瀏覽器的電腦與伺服器之間不可能真的有一條連線,只是透過封包在網路上多個節點轉送達成類似的現象。
[2] 這說法不是很精確,WiFi事實上包含了實體曾與資料鏈結層,但為了方便說明,請體諒一下。
[3] 雖然有人覺得像Objective C或Swift語言寫程式時需要寫出參數名稱很多餘,但在設計DSL時,這卻常常能讓程式更像自然語言,例如:client.where("bookId", is: 19333910)
[4] 文中引用很多諺語主要來自《軟體工程諺語》部落格。

 
over 2 years ago

Translated from "Long Polling with Asynchronous Servlets" by Henry Naftulin, Java Magazine, January/February 2016, page 41-46. Copyright Oracle Corporation.

用非同步Servlets實現Long Polling

當其他方法都無法用時可靠的客戶端/伺服器端通訊方式

在不久之前,桌面應用程式與網頁應用程式之間存在一個很大的差距,如果您回到10年前,很明顯桌面應用程式有比較快的回饋,有比較好的使用者介面,整體上提供較好的使用者體驗,網頁應用程式在使用者體驗上比較落後的一個主要原因是因為無法像桌面應用程式那樣快速反應伺服器上狀態的變化,使用者必須更新整個頁面來取得畫面上的新資料。過去,為了媲美桌面應用程式,不少開發網頁應用程式的公司使用不同的策略,像是使用applets、Adobe Flash應用程式、Comet或其他當時受歡迎的框架,也有使用原始的Ajax,但即使使用這些技術,網頁應用程式還是無法在易用性上與桌面軟體媲美。

現在,網頁應用程式被期待是互動的、有漂亮的UI,且能和相似的桌面軟體做相同的事,使用豐富的前端UI框架(像是Bootstrap)及處理框架(例如jQuery和Angular JS),建立好看且能快速通知伺服器(使用者輸入造成)UI變化的應用程式變得更容易。

但如何傳遞伺服器的變化給網頁客戶端呢?畢竟,在使用現在最新的技術,我們習慣且期待幾乎即時的UI回饋以反應伺服器端資訊的變化,在本文中,我探究一個解決方案:long polling [譯註:我實在不想用長輪詢這個詞],以及簡短地看一下其他替代方案。

現在,網頁應用程式被期待是互動的、有漂亮的UI,且能和相似的桌面軟體做相同的事。

Long Polling與其他替代方案

很長一段時間,網頁應用程式以n-tiered (通常是three-tiered)架構開發,在這架構中,客戶端發起請求向伺服器要資料,若客戶端沒有請求更新,伺服器沒有其他的方法可以推送資料給客戶端,但在許多應用中,伺服器上的變化需要在合理的時間內傳遞給客戶端,使用long polling技術以達成這限制。

Long polling是用來推送資料給網頁用戶端的技術,用戶端請求新資訊後,伺服器會保留這請求直到有新資料為止,當伺服器收到新資料,將送資料給客戶端完成客戶端的回覆,此時,伺服器可以保留此連線用來傳送後需的更新給客戶端,或是立即關閉連線,若是後者,一旦客戶端收到伺服器的回覆,連線就被關閉,客戶端立即傳送另一個更新的請求,然後不斷重複整個流程。

Long polling有幾種變形,最簡單的版本是客戶端以一定的週期輪詢伺服器,當收到請求,伺服器立即回覆,可能是傳送當下最新的資料給客戶端,或是告知客戶端目前沒有新資料,這種簡單的輪詢對更新頻率不高或是顯示失時效的資料不是問題的應用程式來說是可行的。另一種版本是本文我要討論的,伺服器會保留客戶端的請求,直到有客戶端要求的資料才回。

幾個long polling主要的替代方案有WebSocket和Server-Sent Events (SSE)。WebSocket是目前廣泛使用的替代方案,它是在單一TCP連線中提供全雙工通訊通道的標準協定,WebSocket其中一個最大的優點是可以大幅減少伺服器與客戶端之間的網路流量,缺點是並非所有的瀏覽器都支援,針對HTTP協定最佳化的舊網路路由器可能會快取或關閉您的WebSocket連線,這是為什麼某些連線函式庫會在支援時升級到WebSocket協定,但若不支援時降回long polling。[編按:在本期的47頁,有一篇文章介紹使用WebSocket完成相似的專案 (拙譯)]

SSE是另一個標準的技術,瀏覽器從HTTP連線接受來自伺服器的主動更新,它是為能高效地推送資料給客戶端所設計,協定有自動重建連線與其他有用的機制,例如追蹤最後一則已收的訊息,同樣的,不是所有的瀏覽器都支援,在這情況下,程式通常降回long polling方案。

因此,long polling依然是主要且可靠的解決方案,了解它是如何運作及如何有效率地實作是很有用的。

在Servlet 3.0之前的Long Polling

在Servlet 3.0標準出來之前,有二個伺服器執行緒模型:thread per connectionthread per request,在thread-per-connection模型中,每個TCP/IP連線會關聯到一個執行緒,若請求都來自同個客戶端,伺服器每秒可以應付相當大量的請求,但是,這模型有個延展性(scalability)的問題,原因是大多數網站,使用者發起一個動作,然後連線幾乎保持閒置的狀態直到使用者讀完頁面決定接下來要做什麼,因此,關聯到此連線的執行緒也閒置。要改善延展性,網站伺服器可以使用thread-per-request模型,在這模型中,在服務完某個請求後,執行緒可以重複使用去服務不同客戶端的請求,每個請求的服務時間會小幅增加的代價下,這模型允許服務更多的使用者,這代價是因為需要做執行緒排程,目前主流的網站伺服器使用thread-per-request模型。

但是,針對long polling,thread per connection與thread per request在延展性上的差異沒這麼明顯,這是因為每個請求必須等到伺服器有資料後才能回覆,在servlet中等待是很沒效率的,因為本來可以服務其他請求的執行緒被凍結,在Servlet 3.0之前,這導致當更多使用者加入時,延展性很差。

Servlet 3.0改變了Long Polling

Servlet 3.0加入了非同步處理 (asynchronous processing),伺服器可以這方式處理請求,特別是需要較長時間的操作,例如遠端呼叫或是等待某個應用程式事件發生才能產生回覆,在Servlet 3.0標準之前,當等待回覆產生時,servlet要凍結對應的執行緒,且會緊抓著有限資源不放。當有非同步處理,我們可以用不同的執行緒去處理請求並發送回覆給使用者,這改變讓原本servlet的請求執行緒不再被凍結,可以回到servlet容器中去服務其他使用者的請求。

Servlet 3.0加入AsyncContext,一個非同步運算的執行環境,來撰寫非同步處理的程式,AsyncContext將servlet的請求與回覆封裝,讓您可以在原有的servlet處理執行緒外使用。要使用AsyncContext,您首先要跟servlet容器表明意圖,例如在servlet的annoation中加入asyncSupported=true,然後,要將一個請求以非同步模式處理,需要在原先的servlet處理函式中建立一個AsyncContext實體,像是用

AsyncContext asyncContext = request.startAsync(request, response);

此刻,非同步請求的處理可以被委派到另一個執行緒或加到某個佇列中,等晚點再處理,因為servlet的請求與回覆被封裝在AsyncContext中,任何執行緒都可以使用,且沒有與原本的servlet執行緒綁在一起,這允許原先的servlet執行緒可以不用等待非同步的回覆完成就能結束目前的呼叫,然後可以服務其他客戶端的請求。

一個非同步Long Polling簡單例子

我們來看一個簡單的網站聊天應用例子,示範servlet非同步處理的優點,在這應用中,使用者在輸入使用者名稱與一則訊息後,按下Send (見Figure 1),這訊息將出現在所有輪詢此聊天網址的瀏覽器上。

Figure 1. Chat app that broadcasts incoming messages

Listing 1是聊天應用中通訊關鍵的部分程式碼。

Listing 1
@WebServlet(urlPatterns="/chatApi", asyncSupported=true, loadOnStartup = 1)
public class AsyncChatServletApi extends HttpServlet {
    ...
    private static final int NUM_WORKER_THREADS = 10;
    Lock lock = new ReentrantLock();
    LinkedList<AsyncContext> asyncContexts = new LinkedList<>();
    private AsyncListener listener = new ChatAsyncListener();

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        AsyncContext asyncContext = request.startAsync(request, response);
        asyncContext.setTimeout(-1);
        asyncContext.addListener(listener);
        try {
            lock.lock();
            asyncContexts.addFirst(asyncContext);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        final String message = getMessage(request);
        Collection<AsyncContext> localCopy;
        try {
            lock.lock();
            localCopy= asyncContexts;
            asyncContexts = new LinkedList<>();
        } finally {
            lock.unlock();
        }
        for(AsyncContext asyncContext : localCopy) {
            Runnable runnable = new RunnableWriter(asyncContext, message);
            executor.execute(runnable);
        }
        localCopy.clear();
    }

    private String getMessage(...) { ... }

    class RunnableWriter implements Runnable {
        private final AsyncContext asyncContext;
        private final String message;
        public RunnableWriter(..) { .. }

        @Override
        public void run() {
            try(PrintWriter writer = asyncContext.getResponse().getWriter()) {
                writer.println(message);
                writer.flush();
                asyncContext.complete();
            } catch(Exception e) {
                ...
            }
        }
    }
}

這程式例子實作一個非同步的servlet處理來自/chatApi的請求,程式建立一個容器用來儲存多個非同步執行環境,它包含等待聊天訊息的servlet環境,所以不是用servlet處理執行緒去等待某人送一則訊息,而是建立AsyncContext實體,然後儲存該實體,等之後某個新訊息來時再處理。

AsyncChatServletApi類別中,二個REST API會被使用:doGet and doPostdoGet註冊某個使用者要收新訊息,同時,doPost推送新訊息給所有等待的客戶端,明確地說,當使用者開啟URL,呼叫下面程式會建立一個非同步執行環境:

AsyncContext asyncContext = request.startAsync(request, response);

到目前為止,非同步執行環境被儲存到一個容器中,這容器代表等待下則訊息的所有客戶端,此時,servlet執行緒已經完成該請求的處理,然後可以被重新利用來處理其他請求。

當使用者觸發doPost請求發送一則訊息,訊息會被接收並寫到每個非同步執行環境中,然後呼叫下列函式結束整個流程:

asyncContext.complete();

由於servlet可被多個執行緒呼叫,因此,處理執行緒必須確保是執行緒安全的,特別是,要注意以下三個情境:

  • 同時處理二個post請求
  • 一個post請求正在處理中,收到一個get請求
  • 同時處理二個get請求

當二個post請求同時發生或一個post請求與一個get請求同時發生,所有等待訊息的客戶端應該收到訊息(即不能掉訊息),這可以將執行環境容器同步複製一份,並重新建立一個容器,以累積新的請求。同步在doGet函式中將一個新元素加到容器中與在doPost中複製一份容器的動作確保上述二個情境的正確性。

當二個get請求同時處理時,雙方的環境必須被儲存等待後續處理,這可以用一個執行緒安全的容器完成,或是,使用同步的程式碼片段來加入某個元素到容器中,就如同Listing 1所示。[譯註:用lock.lock()與lock.unlock()確保容器內容的正確性]

現在,我們來測試一下逾時。如果在非同步執行環境中設定非無限長的逾時時間,當逾時時,客戶端會收到 “Server returned HTTP response code: 500 for URL: http://localhost:8080/chatApi.” 的回覆,若逾時時間沒有明確被設定,請求會直接使用伺服器預設的逾時時間設定,如果逾時時間被設為0或負數(如isting 1所示),伺服器則永遠不會逾時(雖然網頁客戶端會逾時),要在伺服器中處理逾時,可以實作AsyncListener然後在onTimeout函式中提供客製化的程式,例如,可以用Listing 2的程式改變回覆成為狀態碼408 (HTTP請求逾時的狀態碼),並帶著“Request timeout, no chat messages so far, please try again”的訊息給客戶端。

Listing 2.
public class ChatAsyncListener implements AsyncListener {

    @Override
    public void onComplete(AsyncEvent event) { }

    @Override
    public void onTimeout(AsyncEvent event) {
        AsyncContext asyncContext = event.getAsyncContext();
        HttpServletResponse response = (HttpServletResponse)asyncContext.getResponse();
        response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, "Request timeout, no chat messages so far," + " please try again.");
        asyncContext.complete();
    }

    @Override
    public void onError(AsyncEvent event) { }

    @Override
    public void onStartAsync(AsyncEvent event) { }
}

效能

在一般的聊天應用中,所有的客戶端都等待新訊息,同時只有少數客戶端在寫新訊息,因此,針對這應用,專注在當我們的客戶端數量開始增加時會發生什麼事是有意義的,為量測效能,我從64個客戶端開始,然後穩定地增加到2,048個客戶端,量測得到新訊息的時間,Table 1是測試的結果,Figure 1以線圖的方式呈現結果。

Number of Clients
64 128 256 512 1,024 2,048
Run #1 Response
STDEV
483.28
1.39
492.63
3.31
521.00
26.01
485.24
10.55
504.31
15.57
616.75
37.35
Run #2 Response
STDEV
499.80
0.98
490.04
3.15
494.45
3.87
501.02
19.84
546.78
26.85
575.85
35.03
Run #3 Response
STDEV
469.90
0.69
495.57
1.53
489.43
6.79
485.57
7.87
544.37
36.46
659.75
29.13

Table 1. 發送訊息給多個客戶端,三次測試的結果(時間單位是毫秒)

Figure 2. 當客戶端數量增加的反應時間

我們的分析不需要知道完成一個操作所需的確切時間,重要的事當客戶端數量增加時的趨勢,在到達1,024個客戶端之前,平均的反應時間沒有太大的變化,大概維持在500毫秒上下,這是我們的產生訊息的執行緒所花的時間[譯註:沒看到程式,猜測大概是每500毫秒產生一則新訊息],當客戶端數量超過1,024,我們可以看到效能些微下降,主要是因為要花時間處理將訊息推播給客戶端。

另一個有趣的點是這測試說明取回訊息的平均時間略低於訊息產生的頻率,這意味,處理訊息和重新訂閱都需要時間,因此客戶端有機會錯失新訊息。

要加強這方案在不允許錯失訊息的環境中使用,客戶端可以附帶一個標記代表最後一則收到的訊息給伺服器,如果客戶端已經收過最新的訊息,伺服器將讓該執行緒以非同步方式等待新訊息,反之,伺服器將客戶端收到的最後一則訊息之後產生的所有訊息全回給客戶端。

結論

在本文中,我已經解釋什麼是long polling,以及示範如何使用它。有廣泛的支援,當有需要客戶端與伺服器之間的持續連線,隨時可以使用long polling。

譯者的告白
沒想到2016 January/February雙月刊翻譯的第一篇會是關於自己已經很久沒用過的Servlet,本來要翻WebSocket後續的文章,不過那文章有引用此篇文章(其實有交叉引用),加上這文篇文章寫得很流暢好讀,篇幅又短,很快就翻完了,算是一個好的開始吧!

 
over 2 years ago

Translated from "Jython 2.7: Integrating Python and Java" by Jim Baker and Josh Juneau, Java Magazine, November/December 2015, page 42. Copyright Oracle Corporation.

Jython 2.7: 結合Python與Java

可以輕易建立同時使用Python與Java函式庫專案的語言

在廣大社群、強健的生態系與穩健的語言加持下,Python的開發者長久以來享有極高的開發效率,Jython是Python其中一個可在JVM上運行的實作,選擇Jython的理由多樣廣泛,您可能因為想在Python程式中使用Java套件而選擇它、透過Python的互動終端探究Java生態系、佈署使用Django的Python程式到servlet容器中、或是將您的Java專案與腳本語言(一些受歡迎的工具如Sikuli、The Grinder和IBM WebSphere)一起打包。

第一個Jython釋出版本2.0,支援Python 2.0,在2001年首次見到曙光(先前的版本稱為“JPython”),Jython從此成長,並成為Java平台上最成熟與穩定的替代語言之一,最近的版本Jython 2.7,在2015五月釋出,支援Python 2.7、加強與Java的整合、和擴充對Python生態系的支援,特別是Python套件管理。

本文提供Jython 2.7詳細的導覽,包含一個容易上手的例子:使用Apache POI操作試算表,讀完本文,您將對Jython特性有足夠的了解,可以下載最新的釋出版本,然後開始您自己的專案。

精簡入門書 — 它就只是Python

Jython就是Java平台上的Python,但是,如果您不熟悉Python語言,你需要精簡入門書。

我們將用Jython終端來看語言特性,Jython終端使用受歡迎的JLine 2實作經典的read-evaluate-print loop (REPL),終端讀取使用者輸入的述句(statements)和表示式(expressions),對述句進行計算,並印出結果,這流程(迴圈)會持續到使用者離開終端為止。類似的終端,針對JVM其它受歡迎的語言像是Clojure、Groovy、JRuby和Scala,同樣用JLine 2開發。

不帶任何參數執行Jython程式將會啟動終端,我們將會使用撰寫本文時最新釋出的版本(2.7.0),如Listing 1所示(注意,在本文中,我們假設您使用UNIX-like的系統,$是像bash的命令列提示。Jython同樣能在Windows上運作)。

Listing 1.
$ jython
Jython 2.7.0 (default:. . ., Apr 29 2015, 02:25:11)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corp.)]
Type "help", "copyright", "credits" or "license"
for more information.
>>>

終端提示您從>>>開始輸入,我們開始使用dict型別,即dictionary [譯註:我想這裡翻成字典應該沒什麼幫助],一個key到value的對應(可修改),key和value可以是Python或Java的任何物件,馬上就會看到。因它的多功能,dictionary在大多數的Python程式中大量被使用,Listing 2用幾個例子說明dictionary用法。

Listing 2.
>>> d1 = {'one':1, 'two':2, 'three': 3}
>>> d1['three']
3
>>> # Equivalent construction by using keywords
>>> # Note that '#' introduces a comment, including in the console
>>> d2 = dict(one=1, two=2, three=3)
>>> d2['one']
1
>>> d1 == d2
True
>>> len(d1) # length of d1
3
>>> # Note that there is no construction equivalent using keywords,
>>> # because keywords are limited to strings that would also be
>>> # valid Python identifiers.
>>> inverted = {1: 'one', 2: 'two', 3: 'three'}

Python 2.7同樣支援dictionary comprehension [譯註:comprehension該翻成什麼?這真的考倒我了,就下面範例我知道數學上的意思,但暫時找不到合適的詞,網路上有推導或綜合運算等翻法,但都覺得不怎麼達意,所以,暫時不翻它了],comprehension是用特定語法用一個算式產生特定型別容器的一種用法,比如,用下面的comprehension,我們可以得到將原本dictionary中每個元素的value對到key的反向對應:

>>> inverted_d1 = { v: k for k, v in d1.iteritems() }

簡潔comprehension語法的變形同樣支援用來產生list和set。

反向對應的功能常用到我們可能再用一次,因此,我們定義一個函式封裝它,當我們可以在終端裡定義Python函式,這對我們同樣是一個好機會探索終端的其他用法,建立一個basics.py檔案,並將下列文字作為該檔案初始的內容:

def inverted(d): return { v: k for k,v in d.iteritems() }

我們先詳細看一下程式碼片段。首先,inverted函式使用def關鍵字定義,注意Python使用有意義的空白,代替大括號或其他符號來描述程式的階層結構(例中的大括弧是用來建立dictionary comprehension,大括弧用於建立dictset,同樣,雖然一般是四個空白。因文章的限制,我們用二個空白縮排Python程式),Python的哲學很簡單:我們已經縮排程式,所以它相應於它的結構,用大括弧或其他符號是多餘的。但許多格式化的細節,您也許需要一點時間適應Python的作法。

讓我們再次回到Jython終端,但此次,我們用jython27 –i basics.py開啟終端並載入我們的檔案,在命令提示>>>中,我們呼叫dir函式來查詢有什麼可用,當不帶參數呼叫這函式時,這只適用目前的模組:

$ jython27 -i basics.py
>>> dir()
['__builtins__', '__doc__',
'__file__',
'__name__', '__package__',
'inverted']
>>> inverted({1: 'one', 2: 'two'})
{'one': 1, 'two': 2}

-i選項指我們在basics這模組的有效範圍中運行終端,我們有許多已定義的名字,包含我們剛定義的inverted函式,這樣使用REPL的方式非常適合探索式的程式開發:用正在進行開發的模組開啟終端、在終端中嘗試一個想法、然候用終端中可用的部分抽出來編輯模組,重複這過程。這探索式開發是使用Python開發的一項保證。

您可以用較傳統的方式開發程式,因此像PyDev (建構在Eclipse之上)或PyCharm (建構在IntelliJ之上)等IDE,提供Python程式的GUI除錯工具,包含中斷點、監看、檢視變數等,這些都是可行的,因為Jython支援Python標準除錯與追蹤機制。

Jython支援Python標準除錯與追蹤機制。

Jython 2.7支援Python 2.7為set型別提供的更多功能:

>>> {2,4,6,8,10}
set([2, 4, 6, 8, 10])
>>> # Create Empty Set

>>> set()
set([])
>>> s = {2,4,6,8,10}
>>> 3 not in s
True

Jython支援set型別,它同樣支援Python的set所有功能:

>>> s.pop()
2
>>> s
set([4, 6, 8, 10])
>>> x.add(3)
>>> x.add(5)
>>> s.symmetric_difference(x)
set([3, 4, 5, 6, 8, 10])

現在我們來看Java整合,Jython不是整合Python與Java唯一的方法,其它整合選項有JPype (透過JNI嵌入CPython)和Py4J (用遠端socket連線),但是,Jython是獨一無二的,它支援Java物件如同使用Python物件般,反之亦然。

Python語言的標準函式庫缺少能排序的set,但Jython讓使用Java能排序的set實作(像是java.util.LinkedHashSet確保插入順序及java.util.TreeSet維持內容本身的順序)變容易,Listing 3示範如何使用。

Listing 3.
>>> from java.util import TreeSet
>>> clangs = TreeSet(["c", "python", "ruby", "perl", "javascript"])

相比Java,一個些微差異是Python不用new關鍵字建立物件,取而代之,類別本身就是一個工廠,如Listing 4所示。

Listing 4.
>>> jvmlangs = TreeSet(["java", "python", "groovy", "scala", "ruby", "javascript"])
>>> clangs | jvmlangs # set union

[c, groovy, java, javascript, perl, python, ruby, scala]
>>> clangs & jvmlangs # set intersection

[javascript, python, ruby]

現在我們嘗試使用其它Java套件,Jython的開發者是Google Guava函式庫容器的愛用者,廣泛地使用,特別是MapMaker,函式庫中一個concurrent map [譯註:這個我也放棄了,似乎沒聽過這的中譯]。

首先,我們需要下載Google Guava的JAR檔,然後放在我們的CLASSPATH路徑中(在本例中,我們使用Guava release 18.0),重啟Jython終端讓CLASSPATH的變動生效。

現在是一個好時機去嘗試tab自動完成,Jython 2.7新加入的支援,有時候,在Java生態系中開發有一點不便:套件名稱很長,常常要吃力拼出來。有tab自動完成的支援,任何時候在終端上輸入文字時,您可以按TAB鍵取得可能自動完成清單(如果有多個可能)或直接自動完成填入(若只有一個可能)。

所以先匯入:

>>> import com.google

然後,您可以輸入以下文字,接著按下TAB鍵

>>> d = com.google.c

於是您會得到:

>>> d = com.google.common

您最終可以完成下列:

>>> d = com.google.common.collect.HashBiMap.create(dict(one=1, two=2, three=3))

雙向對映的好處是會維護任何更新,如下所示:

>>> d.inverse()
{3: three, 2: two, 1: one}
>>> d.inverse()[4] = "four"
>>> d
{three: 3, four: 4, two: 2, one: 1}

練習:操作試算表

我們現在用更進一步的例子展示Jython與Java的深度整合。

我們的假設是要自動化的既有商業流程都仰賴試算表來呈現營運現況,雖然以試算表為基礎的流程的彈性已被證實,但它仍需手動處理且容易出錯。現今,這流程仰賴電子郵件、分享磁碟區、聚集與一些商業工具,聽起來很熟悉?

作為軟體開發者,我們知道有許多方法可以自動化這些商業流程,我們可以重寫然後不再使用試算表,但我們想保留試算表的優點:包含廣泛的使用率、彈性與容易使用,所以我們嘗試另一種方式:我們將繼續使用試算表,但提供更好的工具管理它們,我們將使用並整合下列工具:

  • Apache POI可以以程式操作試算表(Java函式庫)。
  • GitHub為試算表進行版控,我們特別想利用其大量的REST API來儲存與取得試算表(REST服務),因此,GitHub作為我們可能用來管理(包含我們可能建立的)試算表的通用REST服務。
  • Requests簡化HTTP以及特別是RESTful服務的使用,以利於使用GitHub的REST API去取得文件(Python函式庫)。
  • Nosetests支援單元測試(Python函式庫)。
  • 客製化的Python程式黏合上述的東西,包含審核與公式計算

首先,下載Apache POI,撰寫本文時,最新的版本是3.12,您需要從POI下載poi、poi-ooxml、poi-ooxml-schemas及xmlbeans等JAR擋到CLASSPATH路徑中。

下一步,您需要安裝需要的Python套件,安裝的支援是Jython 2.7的亮點,在過去,對Python開發者而言,Jython先前的版本惱人的事情之一,Jython從未完整支援Python的生態系,從2.7的版本開始,您可以欣然的獲得Python生態系帶來的優點,特別是PyPI (Python Package Index)有大量的Python套件,在Jython 2.7做這件事相當容易,因為廣受歡迎的pip工具已經包含在此版本中,這支援讓在應用程式中容易併入Python生態系的函式庫與API更。

下述的指令將使用pip模組安裝Nosetests與Requests模組,-m MODULE意指以命令列模式模式執行指定的模組,並處理後續的參數:

$ jython27 -m pip install nose requests

Java與Python的相依性都已經處理好,那從哪裡開始?Python語言能如此優雅是因為我們可以漸進式地在終端中探索問題與可能的解決方案。

Python語言能如此優雅是因為我們可以漸進式地在終端中探索問題與可能的解決方案。

假設我們有一個名為hours.xslx的試算表位於GitHub空間:https://github.com/jimbaker/poi 的最上層,我們可以用https://github.com/jimbaker/poi/raw/master/hours.xlsx 取得試算表,我們可以在終端中嘗試(url設成想要的試算表):

>>> import requests
>>> response = requests.get(url, stream=True)

我們將reponse寫到一個二進制檔案中(因此檔案模式是"wb"),我們每次512 bytes逐次寫入檔案減少記憶體的使用量,writelines函式接受一個iterator:

>>> f = open("hours.xlsx", "wb")
>>> f.writelines(respone.iter_content(512))
>>> f.close()

現在,我們用POI讀取儲存的試算表,注意Jython暗地將Python物件橋接成FileInputStreamFileOutputStream,因此可以使用需要的Java函式或建構子:

>>> from org.apache.poi.xssf.usermodel import XSSFWorkbook
>>> workbook = XSSFWorkbook(open("hours.xlsx", "rb"))

我們可以對試算表做什麼?探索看看:

>>> dir(workbook)

最終,在終端中探索與POI API文件研究後,我們可能得到像Listing 5所示的程式來處理試算表:

Listing 5.
# traverses cells in a workbook,
# calling a callback function on each cell

from contexlib import closing

def process_workbook(path, callback=None):
  if callback is None:
    def callback(cell):
      print cell,

with open(path, "rb") as file:
  with closing(XSSFWorkbook(file)) as workbook:
    for sheet in workbook:
      for row in sheet:
        for cell in row:
          callback(cell)

process_workbook函式可接受二個參數:pathcallback,注意,callback是非必要參數,因為我們給予他一個預設值None,然而,沒有像Java那樣指定靜態型別或Scala那樣推論型別,當我們提到程式碼的靜態分析(或語法分析),是指檢驗程式碼文字,而不是執行它,我們(編譯器或IDE等工具)可以決定程式的某些屬性,像是變數的有效範圍、變數的型別、是否一致地使用型別,換句話說,程式是否有檢查型別,是否有部分程式碼不會被執行,因此是否可以移除?我們是否可以使用常數推導(constant folding)或內含(inling)等等。剛提及的部分,Jython只支援靜態分析變數有效範圍的靜態(CPython能進行部份的常數推導與無用程式碼碼移除,Python 3.5將有標準靜態型別註釋,作為混合動態型別與靜態型別的漸近型別系統的一部分)。

我們仍定義一個函式如果沒提供callback,稱作callback,這或許會搞糊塗,但其有效範圍在process_workbook函式內部,它不只是語法規範其有效範圍,事實上它是一個closure,callback是有條件地被定義[譯註:若外部沒提供就用內部的定義],因此這和我們在Java裡使用的方式相當不一樣。再一次,Python展現出它確實是一個動態語言,任何對process_workbook的靜態分析只可能斷定callback可能是這個函式或不是,但是,注意Jython已經將程式碼編譯成Java bytecode,所以看到callback的名稱是否設定到對應某個編譯過的函式主體,因此,條件式定義的開銷不過就是指定變數的開銷,這展現出Jython讓您在Java與Python之間來回選用合適的方式完成事情。

Jython讓您在Java與Python之間來回選用合適的方式完成事情。

我們接著走訪整個試算表中每個表格的每一列與每個cell,試算表集、表格與列物件都實作java.lang.Iterable,讓Jython依序處理,也許不令人意外,Jython的整合也能讓Java程式能用for-each迴圈的方式走訪Python的iterables (或iterators)。

callback(cell)的方式呼叫callback時,cell被傳入,Jython執行環境進行動態的型別檢查:確認callback是否是一個可以被呼叫的物件?Python有個簡單的規則:可被呼叫的物件都實作特別的函式__call__。所有的方法[譯註:這沒法也翻成函式,會變成無法區分]都實作這特別的函式,但任意類別也可以實作,Python總結這型別方式為duck typing,這名稱來自若他看起來像鴨子、游泳像鴨子、像鴨子那樣呱呱叫,那它也許就是鴨子,Python假設您知道你在做什麼,讓您作主。

然而,如果某個物件沒提供__call__函式,當程式執行時,Python會拋出TypeError例外,當然,__call__本身也可能拋出例外。

現在我們定義一個callback可以審核試算表中的公式,非常類似Excel的公式,如果某個cell有個公式,這公式字串可以用getCellFormula()函式取得,注意POI的公式與Excel不同,因為沒有以=符號開頭。

因為Python除了函式外還支援屬性,Jython進一步加強如何使用Java物件,您可以把getter和setter當屬性使用,忽略getset,所以可以寫審核callback如下:

def print_if_hardcoded(cell):
  try:
    float(cell.cellFormula)
      ref = CellReference(cell)
      print ref.formatAsString(), cell
  except:
    pass

這裡我們看到一個動態語言與在Python中常用的pattern:我們嘗試做某件事,然後捕捉所有例外,(這pattern稱做“it is easier to ask forgiveness than permission” [譯註:這裡就視作專有名詞不翻譯了]),我們串起二個評估,取得公式字串(如果不是,POI會拋出IllegalStateException例外)且試圖取得該字串的浮點數值(若無法取直,Python拋出ValueError例外),如果嘗試失敗,表示這不是一個公式(沒有您想找的公式)。

假設這程式存成poi.py,我們可以用jython -i poi.py如Listing 6所示。

Listing 6.
>>> process_workbook("example.xlsx", print_if_hardcoded)
A1 42
A2 47

我們可以快速地建立一個腳本,用Requests下載試算表,套用剛寫的審核並儲存結果,然後可能作為一個REST API。

合併試算表

我們來看一個使用更多POI的例子,我們需要合併試算多個試算表成為一個,我們可以更進一步擴充,建立合併公式、提供格式化功能等,且可以更複雜如Excel能做的。

Listing 7的程式(可從download area下載),展示一種方法完成上述的事情,它善用Python 2.7新增的argparse函式庫開啟任意數量的試算表,然後將合併的結果寫到輸出的試算表,定義一個像這樣的主函式是符合Python語言習慣的。

當我們想對試算表每個cell做同樣的事情時,像process_workbook這樣的函式就很有用,其他情況,我們可能想處理特定的某些cell,因此我們定義一個新的函式:get_cells能取得指定範圍的cell,像A1:G8,或是參考的聯集,例如:A1,B1,C1,D1。

def get_cells(sheet, ref):
  for row_idx, col_idx in referred(ref):
    row = sheet.getRow(row_idx)
    if row is not None:
      cell = row.getCell(col_idx)
      if cell is not None:
        yield cell

get_cells函式是個generator函式,呼叫此函式會得到一個iterator,每個迭代依序產出結果(如yield關鍵字所標註),所以這是建構java.lang.Iterator相當方便的方式,但不需要明確捕捉每次呼叫與下個函式呼叫之間的狀態,generator是Python程式相當常見的用法,特別是這樣簡化資料(特別是大資料)逐步地處理,Listing 8 (可從download area下載)展示generator的使用。

get_cells,我們惡以很快地計算查詢的結果,馬上來試試,A1:G8範圍的總和是多少?換句話說,等於計算試算表中=SUM (A1:G8)的結果。

定義下列的輔助函式get_nums,然後使用內建的函式sum

NUMERIC_CELLS = { Cell.CELL_TYPE_FORMULA, Cell.CELL_TYPE_NUMERIC }
def get_nums(cells):
  for cell in cells:
    if cell.cellType in NUMERIC_CELLS:
      yield cell.numericCellValue

使用內建的函式sum,答案很簡單是sum(get_nums(get_cells(spreadsheet, "A1:G8")))

在A1:G8的範圍中是否有ell是有公式的呢?定義一個審核的函式,是先前我們已有的變形,然後使用內建的函式any

def hardcoded_cells(cells):
  for cell in cells:
    try:
      float(cell.cellFormula)
      yield True
    except:
      yield False

答案就會是any(hardcoded_cells(get_cells (spreadsheet, "A1:G8")))

我們目前完成的有定義初期高階的Python API操作試算表,組合一些可能可以用在試算表的函式,然而,我們能保留所有Java POI函式庫所提供的功能。

這引領我們到最後一個主題:我們有辦法確認我們的試算表通過承諾的測試嗎?複雜一點,假設我們建立一個持續整合的服務,像是Jenkins,對試算表執行測試,也許作為GitHub pull request的一部分,我們該如何定義與執行我們的測試?Python生態系在測試框架上有許多好的選擇,像是標準函式庫本身以及unittest (xUnit測試風格的實作),但建築在unittest之上的Nose測試框架更廣為大家使用,因為它很容易使用。

例如。我們可能想確認cross-tabulations的正確性:某一列的小計應該等於某一行的小計,並考慮到數值精準度的問題,如Listing 9所示。

Listing 9.
from nose.tools import assert_almost_equals

def assert_crosstab(spreadsheet, range1, range2):
  assert_almost_equals(sum(get_nums(spreadsheet, range1)), sum(get_nums(spreadsheet, range2)))

當這定義好,我們可以寫一個如Listing 10簡單的測試。

Listing 10.
finance_wb = XSSFWorkbook("financials.xlsx")
main_sheet = finance_wb.sheetAt(0)

def test_financials():
  assert_crosstab(main_sheet, "A5:G5", "H1:H5")

然後可以執行Nose去尋找並執行您的測試。Nose遵循convention-over-configuration (慣例優於設定)的方式,意指它很容易使用,結果如下:

$jython -m nose
.
----------------------------
Ran 1 test in 0.033s
OK

第二列的每一個點代表所收集到的所有測試中的一個測試,直接加更多測試吧!

莫再回首

可預見地,隨著時間流逝,技術與語言特性都會演進,Jython 2.7確實有些重要的捨棄,也許最需要注意的是,Jython 2.7最低需求是Java 7,另一個重要的是安裝程式不再支援使用其他的JRE產生Jython啟動器(launcher),只使用JAVA_HOME中的JRE。

Jython 3.5

Python語言仍持續地發展,參考實作亦是,在您讀此文章時,CPython將會釋出,某個時間點,規劃釋出Jython 3.5,與CPython 3.5的釋出一起,值得提的一點是,Jython 2.7基本上有與Python 3.2相同內在執行環境及stdlib的支援,但重要的成果將會在Jython 3.5上,Python 3.5一個熱切期盼的特性是選擇性的靜態型別,讓Jython有更好的Java整合。

但沒這麼快,Jython 2.7.x將持續好一陣子,只要Python 2.7仍廣為使用中,Jython開發團隊就會規劃維護2.7.x,轉移並採用Python 3的進程相當緩慢,部分原因是2.7與3.0之間的差異過大。因為Python 2.7仍廣為使用,Jython將在Jython 2.7.x的開發軸上規劃time-based的釋出,Jython 2.7.x未來的釋出將專注在效能與整合等,Java 9的釋出有許多動態語言上優化將帶來效能的改善。

即使不會這麼快到Jython 3.5,但它已在進行。事實上,已經有個專屬Jython 3.5的開發分支,即便它仍在非常初期的階段。目前的目標是希望在未來二年釋出Jython 3.5。

結論

Jython 2.7提供豐富的工具,讓開發者在相同的codebase中結合二個廣受歡迎的生態系:Python與Java,在本文中,我們看到一些Jython新的功能,但還有其他相當棒的功能可以去探索,到jython.org下載它,然後關注後續的更新,每六個月將更新一次。

編輯:本文是持續探索JVM語言的系列文章的一部分,上一期,我們報導Kotlin,下一期,我們介紹Gosu,在工業界,一個同時用於前端與後端系統的JVM語言。

譯者的告別
雖然這次的翻譯我自己不是很滿意,一個原因是我對Python本身的熟悉度沒那麼高,因此無法完全理解作者某些地方的原意,另外是句子翻得不是很通順,不過,Java Magazine的2015年11-12雙月刊的所有文章總算是翻譯完畢,算是很有毅力地完成一件事了。

 
over 2 years ago

Translated from "Contexts and Dependency Injection: The New Java EE Toolbox - Integration with Java EE" by Antonio Goncalves, Java Magazine, November/December 2015, page 34. Copyright Oracle Corporation.

環境與關係注入:新的Java EE工具箱

與Java EE整合

這系列文章試圖解開環境與關係注入(Contexts and Dependency Injection, CDI)的秘密,在前三期的文章中[譯註:這我恐怕就沒空翻譯了],我探討何為強型的關係注入、如何用CDI整合第三方框架、及如何使用攔截器(interceptor)、裝飾器(decorator)與事件(event)建立弱耦合,這最終篇將涵蓋CDI與Java EE的整合。

Java EE是Java執行環境的擴充,他提供一個受控制的環境,在這環境中,容器供應為數不少的服務元件,這些服務包含生命週期管理(lifecycle management)、安全性(security)、驗證(validation)、物件延續性(persistence) [譯註:這東西有點難直翻成儲存,用資料庫儲存物件只是讓物件延續的眾多方法中的一種],當然,還有注入(injection)。物件延續性與交易(transaction)常常一起用來開發應用程式的後台(back end)。

在網頁層,Java EE有servlet、WebSockets [編按:參考本期文章]和JavaServer Faces (JSF)與使用者介面相關的技術,CDI,如果在前三篇文章所述,可以將網頁層與服務層結合在一起,建立一個同質[譯註:指都使用Java技術]且整合的應用程式。

結合網頁層與服務層

Java EE有許多技術讓我們建立任何架構,包含網站應用程式、REST介面、批次處理、非同步傳訊、物件延續等等。如Figure 1所示,這些應用都可以組織成數層(tiers):呈現、商業邏輯、商業模型或與外部服務互動。根據需求,任何架構都可能從無狀態到有狀態的、從flat layered到multitiered [譯註:layer和tier都是分層的概念,所以這就不翻譯了,免得無法分出之間的差別]。一個問題是,網頁層或服務層個別有自己的典範和語言,因此,CDI便是結合它們很重要的資源。

Figure 1. 一個應用的標準分層

Java應用在服務層,除了網頁客戶端(使用HTML)和資料庫(使用資料庫定義語言)外,大多數Java EE使用Java為主要語言,因此,我們可以在大多數的應用層(Java Persistence API存取商業模型實體或在商業邏輯層中一個簡單bean)看到Java,我們甚至可以在部分的呈現層中使用Java:使用Java寫JSF backing beans。

EL應用在呈現層,我會說Java為主要語言,是因為JSF頁面主要使用Facelets或Expression Language (EL),EL提供一個重要的機制,讓呈現層能與應用邏輯溝通,這在JavaServer Faces與JavaServer Pages中都可以使用,透過#符號,如Figure 2所示,EL使用簡單的表達式動態地從元件存取資料,例如,顯示訂單的小記在頁面上,或是當按鈕按下時執行compute函式。

CDI結合服務層與呈現層,使用@Named,CDI結合Java與Expression Language,就如同您可以在Figure 2看到的,基本上,給予CDI bean一個名字,讓它連結在EL中,因此當PurchaseOrderBean被加註@Named("po")時,它以po的名字連結在EL中。

Figure 2. 使用Expression Language

CDI應用在管理狀態,CDI更進一步替我們使用有效範圍(scope)管理bean的狀態,假設在網站的右上角,我們需要顯示登入的使用者,希望這資訊可以保持到session結束,針對這情況,只需替bean加註@SessionScoped,CDI會管理狀態,當session結束時銷毀bean;另一方面,每次更新頁面時,應該計算與顯示訂單的小計,因為PurchaseOrderBean的範圍比session要短,所以我們可以為它加註@RequestScoped,CDI只為每次請求保留bean的狀態,這意味請求是無狀態的。透過一些annotations,CDI結合網頁層與服務層,減少為膠合而寫的程式碼,讓開發者專注在商業問題上。CDI為分層架構定義一個一致的模型,在使用者多次請求的互動中提供明確定義的環境。

連結 (Binding)

連結是將網頁層與服務層結合在一起的基本服務,如果我們想在非Java但支援EL的程式碼(例如JSF頁面)中參考一個bean,我們必須為bean指定一個EL名稱,用@Named內建的修飾字指定,然後我們可以輕易地在任何JSF頁面中透過EL表示是使用bean。原本,EL受到ECMAScript和XPath表示語言的啟發,被引入到Java EE中,讓網頁開發者可以存取與操作後端的Java程式而不需透過JavaScript。

Expression Language EL的語法相當簡單,使用井字符號(#)與大括號標示一個需要被解析的表示式,這些表示式可以很複雜或很簡單(見Listing 1),也可以使用數學運算、lambda表示法等等。

Listing 1.
// Value Expressions

#{purchaseOrderBean.subtotal}
#{purchaseOrderBean.customer.name}

// Array Expressions

#{purchaseOrderBean.orders[2]}

// Method Expressions

#{purchaseOrderBean.compute}

// Parameterized Method Calls

#{purchaseOrderBean.compute('5')}

value expression最常用因為可以讀取與寫入資料,因此,我們的頁面可以取得PurchaseOrderBeansubtotal屬性或是客戶的name屬性。語法同樣允許存取陣列或list的元素:使用角括弧指定索引,如此,表示式回傳bean當中第二筆消費交易。另一有用的EL功能是method expression,可以執行bean可回傳結果的公開函式,所以,表示式執行PurchaseOrderBeancompute函式。參數化的函示可以接受參數,例中,5被當成參數傳入。

JSF pages 回到我們的呈現層,EL以另一種形式出現在JSF頁面中,以Listing 2為例,value expression用來顯示小記與訂單的附加稅(VAT)稅率,這連結是雙向的,意思是表示式可以修改屬性的值,當網頁被傳送給伺服器。當我們需要在按鈕被按下時執行某個動作時,method expression是很有用的,在這案例中,點擊compute按鈕將會執行PurchaseOrderBeancompute函式。

Listing 2.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:body>
    <h:form>
        <h:outputLabel value="Subtotal:"/>
        <h:inputText value="#{purchaseOrderBean.subtotal}"/>
        <h:outputLabel value="VAT rate:"/>
        <h:inputText value="#{purchaseOrderBean.vatRate}"/>
        <h:commandLink value="Compute" action='#{purchaseOrderBean.compute}'/>
    </h:form>
</h:body>
</html>

CDI beans Listing 3中的PurchaseOrderBeansubtotalvatRate屬性與對應的getter及setter,還有一個compute函式,負責計算訂單在指定稅率下的總價,這程式除了加註@Named外沒什麼特別的,但沒有它,這個bean將無法有EL名稱,無法被連結到頁面中。

Listing 3.
@Named
public class PurchaseOrderBean {

    private Float subtotal = 0F;
    private Float vatRate = 5.5F;
    // …

    
    public String compute() {
        Float vat = subtotal * (vatRate / 100);
        Float discount = subtotal * (discountRate / 100);
        total = subtotal + vat  discount;
        return null;
    }

    // ...

}

@Named @Named讓EL能參考到bean的屬性與函式,我們可以在使用@Named時不指定名稱,讓CDI為我們取名,預設名稱是類別名稱將第一個字母變小寫,就如此例中,以小寫p開頭的purchaseOrderBean。但是我們可以在使用@Named時指定非預設的名稱,當使用@Named("order"),前面的表示式也必須對應的改名[譯註:Listing 2中的purchaseOrderBean要改成order]。

連結Producers與Alternatives

如我們剛所見,@Named可以將表示式與bean連結起來,搭配producer,EL可以參考任何東西,例如,我們產生一個整數,給予名稱,然後就可以在表示中餐考到它。除了Java,EL同樣能用alternative來切換實作。

為producer命名 為了解釋有名字的producer與alternative,我們看一下NumberProducer類別,角色是用來產生數值(見Listing 4),它有vatRatediscountRate屬性,兩者的型別都是Float,計畫是產生這些屬性讓CDI管理並可以被注入到某處,就您現在所知,這程式可能會含糊不清,因兩個屬性的型別都是Float。為區別它們,我們為一個屬性加註@VAT,另一個加註@Discount,現在,想在JSF頁面中取得附加稅率,只需在產生的屬性加註@Named,預設,EL名稱是vatRate,如此一來,JSF頁面可直接參考vatRate,不需將NumberProducer類別名稱放在前面(<h:inputText value="#{vatRate}"/>)。記住,@Named使用的預設名稱可以被覆寫,例如,我們可以用vat取代vatRate,然後用(<h:inputText value="#{vat}"/>)表示式參考到它。

Listing 4.
public class NumberProducer {

    @Produces
    @VAT
    @Named("vat")
    private Float vatRate = 5.5F;

    @Produces
    @Discount
    @Named("discount")
    private Float discountRate = 2.25f;
}

Alternative producer 現在,假設我們有另一個使用案例,附加稅和折扣會根據外部的設定而改變,例如,附加稅率在某些國家是5.5%,但在其他國家是19.6%,或是在平日折扣是2.25%,但在聖誕節期間是4.75%,這是alternatives常見的使用案例。首先,我們仍需要產生、修飾與命名附加稅率與折扣屬性(見Listing 5),然後我們加註@Alternative,如您所見,CDI是非常具表達性的,每個annotation都有自己的意義,讓讀程式時相當容易理解,然後,要做的只是在benas.xml中指定開啟或關閉alternatives。

Listing 5.
public class NumberProducer {

    @Produces @VAT @Named("vat")
    private Float vatRate = 5.5F;

    @Produces @VAT @Named("vat") @Alternative
    private Float vatRateAlt = 19.6F;

    @Produces @Discount @Named("discount")
    private Float discountRate = 2.25f;

    @Produces @Discount @Named("discount") @Alternative
    private Float discountRateAlt = 4.75f;
}

狀態管理

我們都習慣HTTP session與HTTP請求的概念,有兩個日常問題的例子是關於管理狀態,與特定context有關,當該context不再需要時,須確保所有必要的清理工作被執行,例如,HTTP session結束時,session需要被清除。傳統上,透過取得與修改servlet session及request屬性,這狀態管理已被用手動的方式實作。CDI讓這狀態管理的概念更進一步,適用到整個應用程式,不限於HTTP。此外,CDI以描述性的方式做到:使用annotation,bean的狀態交由容器管理。不再有因應用程式無法清理session屬性造成的記憶體洩漏,CDI自動完成這些清理工作。CDI將Servlet規範中定義的context模型:application、session、request擴充成另一種context:conversation,然後,將這環境套用到整個商業邏輯層,不只是網頁層。

CDI讓這狀態管理的概念更進一步,適用到整個應用程式,不限於HTTP。此外,CDI以描述性的方式做到,使用單一個annotation,bean的狀態交由容器管理。

內建的有效範圍 在開始看一些程式碼前,我們先說明四種內建的CDI有效範圍(如Figure 3所示)。假設我們有一個應用程式,其生命週期有數個月,我們開啟伺服器然後在關機前讓它執行數個月,在這例子中,application scope (應用程式層級的有效範圍)非常長,一個使用者登入,且保持登入狀態數分鐘,session scope (session層級的有效範圍)從他登入瞬間開始持續到他登出瞬間,第二個使用者登入但她持續較長的session。每個session都是彼此獨立專屬於單一使用者,生命週期也完全不同。在這期間,使用者都點擊他們專屬的空間,每個點擊都建立一個請求,由伺服器處理。最後一個有效範圍是conversation,它相當特異,因為它可以視需求維持生命週期,只要開啟一個conversation就行,它可以跨越多個請求,然後結束,每個使用者將會有他/她專屬的conversation。每個有效範圍都用annotation表達。

Figure 3. 四種CDI內建的有效範圍

Application scope 例如,假設應用程式需要一個全域的快取,由一個key-value的map物件、幾個新增物件到快取、用key取值和移除物件的函式組成,我們希望所有與應用程式互動的使用者都可以使用這個快取。為此,我們為此bean加註@ApplicationScoped (見Listing 6),當需要使用這快取時,CDI容器會自動建立它,當建立它的環境結束時(即伺服器關機),會自動被消滅。如果想在JSF頁面中直接參考到這快取,只要加註@Named

Listing 6.
@Named
@ApplicationScoped
public class Cache implements Serializable {

    private Map<Object,Object> cache = new HashMap<>();

    public void addToCache(Object key, Object value) {
        // ...

    }

    public Object getFromCache (Object key) {
        // ...

    }

    public void removeFromCache (Object key) {
        // ...

    }
}

Session scope 應用程式層級有效範圍的bean在整個應用程式生命週期間存活,且分享給所有使用者。而Session-scoped的bean則只在HTTP session的生命週期間存活,且只屬於當前的使用者,這有效範圍十分有用,例如,設計一個購物車模型(見Listing 7),每個使用者有自己的購物清單,當他登入,可以加物品到購物車,然後在結束時結帳離開。當session建立時,這購物車實體會自動被建立,然後在session結束時被消滅,這時體會與使用者的session連結,然後分享於session的所有請求中。同樣,加註@Named,如果想在EL中使用。

Listing 7.
@Named
@SessionScoped
public class ShoppingCart implements Serializable {

    private List<Item> cartItems = new ArrayList<>();

    public String addItemToCart() {
        // ...

    }

    public String checkout() {
        // ...

    }
}

Request scope. 到目前為止,我們描述的所有有效範圍都在處理狀態,對於無狀態的應用程式,我們可以使用HTTP請求與request scope的bean,這些bean通常是沒有狀態的services (見Listing 8)和controller,例如,建立一本書、取得所有書的封面圖片、取得某分類的書籍清單。通常,他們都會加註@Named,因為可以在頁面上的按鈕點擊時被執行。一個被定義成@RequestScoped的物件在每次請求時被建立,而且不需是可被序列化的(serializable)。

Listing 8.
@RequestScoped
public class BookService {

    public Book persist(Book book) {
        // ...

    }

    public List<String> findAllImages() {
        // ...

    }

    public List<Book> findByCategory(long categoryId) {
        // ...

    }
}

Conversation scope 最後一個內建的有效範圍是conversation scope,和session scope有點像,可以保持某個使用者的狀態且可以跨越多個請求,但是,和session scope不同的是,conversation scope是由應用程式明確劃分的。假設我們使用好幾個網頁組成一個精靈,讓顧客建立一個profile (見Listing 9),為了管理conversation的生命週期[譯註:透過好幾個網頁的推進,就好像伺服器與顧客在對談],CDI給我們一個Conversation API,用注入的方式取得。所以,當使用者開始建立profile,呼叫begin函式開啟一個conversation,使用者可以走訪頁面,回到前個頁面或到下個頁面,直到conversation結束,如您所見,conversation scope是唯一需要明確劃分的。其他有效範圍的bean都由CDI容器自動清理,而conversations需要明確的啟動與結束,或等到超過時限。

Listing 9.
@Named
@ConversationScoped
public class CustomerWizard implements Serializable {

    @Inject
    private Conversation conversation;

    private Customer customer = new Customer();

    public void initProfile () {
        conversation.begin();
        // ...

    }

    public void endProfile () {
        // ...

        conversation.end();
    }
}

Dependent scope 所有我們剛才看的有效範圍都與情境相關,這意味他們的生命周期都由容器管理,注入的bean都與情境相關,CDI容器確保在正確的時間建立物件與注入物件,時間點由物件所指定有效範圍所決定。dependent scope與情境無關,實際上是一個虛擬的有效範圍,dependent scope是CDI bean的預設有效範圍,如果一個CDI bean沒有指定上述任何一個有效範圍,則會被注入成一個dependent-scoped bean,這指的是它的有效範圍與它所屬物件的有效範圍相同。例如,在Listing 10中,一個request-scoped的服務(BookService)注入一個dependent的IsbnGenerator物件,則被注入的IsbnGenerator物件的有效範圍也是request scope。一個dependent scope的bean實體的緊緊相依於另一個物件,IsbnGeneratorBookService建立時被實體化,在BookService被消滅時一併被消滅。我們可以總是使用@Dependent,但大可不必,因為它是預設的有效範圍。

Listing 10.
@Dependent
public class IsbnGenerator {

    public String generateNumber() {
        return "13-84356-" + Math.abs(new Random().nextInt());
    }
}

@RequestScoped
public class BookService {

    @Inject
    private IsbnGenerator generator;
    // ...

}

結論

在本文中,我們示範如何用@Named與有範圍的狀態管理將網頁層與服務層連結在一起,當使用CDI,呈現層的元件與商業邏輯層的元件並沒有差異,都可以被限定範圍、注入、或是在EL中使用。我們可以將應用程式根據我們所需的任意架構分層,不用擔心應用程式邏輯屈從技術的分層。如果架構的分層太扁平,沒有甚麼可以阻擋我們使用CDI建立一個等效的分層架構。撰寫一個所有物件都是CDI bean的Java EE應用程式是有可能的。 [譯註:那我應該會瘋掉...]

LEARN MORE
CDI specification
Beginning Java EE 7
PluralSight course on CDI 1.1
Weld CDI reference implementation

譯者的告白
其實每次看到container based的技術時,心裡總是有些矛盾,它確實很好用,加速開發,但它同時汙染了domain model (好吧~大概只有我這麼龜毛,我認為domain model應該與任何framework保持距離),也許改天應該來寫一篇文章關於這內心的糾結。不過有沒有時間就不知道了,Java Magazine的2015年11-12雙月刊還剩下一篇,接下來將邁入2016年1-2雙月刊的翻譯了。