閒談軟體設計:設計抉擇的因素

更新於 發佈於 閱讀時間約 16 分鐘
圖片來源:www.freepik.com

前言

會有這一篇文章,是在 DZone 看到《Yet Another Evil Suffix for Object Names: Client》討論 AWS S3 Client 的設計不夠 OO,這讓我想起之前曾替公司設計過 Java SDK 給第三方使用,當初與團隊成員討論時,也有針對這一點進行討論,不過我們最後的決定其實是跟 AWS 一樣。這是程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。

背景說明

在開始探討當初我們做出的設計決策之前,先說明當初 SDK 的使用情境 (不能透漏之前工作的細節,因此是修改過的版本),SDK 的主要使用者是下圖中橘色的第三方服務的 server 端應用程式,透過 SDK 與我們的服務溝通 (下圖中紫色的 host server),類似 OAuth,第三方服務的 Web client 端可取得代表我們服務的使用者的 token,第三方服務的 server 端用此 token 換發真正與我們服務溝通的憑證 ,我們的服務可以用憑證得知哪個第三方服務代表誰發出請求。
Figure 1 SDK 的目標使用情境
SDK 有個功能是存取服務提供的某種容器,就暫稱為 Box 吧,每個 Box 都有一個唯一的 ID,第三方服務可以用 ID 透過 SDK 操作 Box,像是修改名稱、說明、圖示,上傳檔案到這容器中,並設定容器的管理員以及那些人可以看到這個容器內的東西。
當時團隊討論時,我曾問要不要把管理者設定的函式放在 Box 物件裡?就如同《Yet Another Evil Suffix for Object Names: Client》所說的,將 Box 設計成一個 proxy,依實作可以分成在 Box 建立時,就將遠端資訊取回的 remote proxy,或是 Box 建立時不取回遠端資訊,等到真正要取得資訊時再請求資訊的 virtual proxy。也就是說 Box 有三種設計方式:AWS 的 client、remote proxy 和 virtual proxy。

Request 數量

說明完背景後,回到設計抉擇,由於 SDK 主要的使用情境在 third-party App server 上,從 response time 的考量上會希望減少 third-party App server 發向Host server 的 request 數量。所以比較在不同情境下,三種方式所需要發出的 request 數量。

情境一:取得既有 Box 的基本資訊

從下方程式可以看出,在取得基本資訊的情境下,三種設計都可用 1 次 request 就完成任務,在 virtual proxy 的情況下,boxManager.getBox("box-id") 並不會真正發出 request,會等到 info() 被呼叫時才真正發出 request。

情境二:加入一個使用者作為成員

從範例可以看到,remote proxy 較其他二種設計多了一次 request,因為不管有沒有使用到,boxManager.getBox("box-id") 馬上發出 request 取回 Box 的基本資訊,然後 addMember("user-id") 再次發出 request 真正完成任務。
到目前為止,virtual proxy 似乎是個不錯的選擇,request 數量和 client 方式一樣,設計也比較 OO,remote proxy 雖然好像會多一次 request,但若將第一次取得的 Box 實體放在記憶體當中,後續的操作也不會再發出 request,似乎也是不錯的方式。

憑證的處理

接著看憑證的處理,如果 SDK 當初設計的使用情境是像 Figure 2 一樣,直接讓使用者的 mobile client 直接存取 Host server 的話,我應該會選擇 remote proxy 的設計,因為不用考慮到多憑證的問題,每個 mobile client 只會有一個憑證代表這個 mobile client 和正在使用的使用者。remote proxy 物件放在記憶體中也沒什麼問題,由 BoxManager 管理所有曾經取得的 proxy 物件,proxy 物件的生命週期和 mobile App 的生命週期一致,可大幅減少需要再次發出 request 的情況。
Figure 2 SDK 的另一種可能使用情境
但在 server 端的程式就不是這樣了,要將 Box 實體放在記憶體中,需要把憑證相關的資訊封裝在 Box 實體中,若想要在發出 request 時使用正確的憑證,還需建立一個使用者與 Box 實體的對照表,每當 third-party App server 要利用 Box 發出 request 時,從對照表拿出該使用者的 Box 實體,然後呼叫對應的函式才會使用到正確的憑證。
這個對照表很難由 SDK 來維護,使用上反而因為對照表的關係變得複雜。而且若考量記憶體的使用量,已經很久沒有存取的 Box 實體,並沒有必要一直放在記憶體當中,只是這還要 SDK 的使用者維護這類似快取管理的機制,並沒有得到太多好處。當然,若不儲存 remote proxy 在記憶體中,憑證的處理就沒有什麼問題,只是會有 request 數量的問題。
相較之下,每當 third-party App server 需要對遠端的 Box 操作時,用使用者的憑證建立 client 物件 (BoxManager) 然後呼叫函式,省事許多。virtual proxy 因為建立成本很低,所以也不需要放在記憶體當中,因此和 client 一樣,要使用的時候重新取得一個 proxy 物件,然後呼叫函式,多憑證沒有太大的問題。
到目前為止,client 和 virtual proxy 都還是不錯的選擇。

Exception

接著思考一下 exception,這其實是最容易被遺忘的一個部分,如果事情不會出錯,自然不用考慮 exception,但 third-party App server 和 Host server 之間透過網路溝通,很難保證不出錯的。問題是 class diagram 通常不會標示 method 會拋出什麼樣的 exception,設計時就不會想到這件事,但這是一個 Java SDK,而 Java 就是一個要把 (checked) exception 宣告在 method 上的一個語言,這時候會發現 exception 讓抽象滲漏法則更為明顯:
所有重大的抽象機制在某種程式上都是有漏洞的。
是否有人注意到上面 virtual proxy 在取得 Box 基本資訊是透過呼叫 info() 的函式,為什麼是這樣的設計呢?先看原本的 info() 版本:
再看看沒有 info() 的版本:
很明顯,第二個版本比較噁心一點,連 getName() 都可能拋出 exception,這裡有將 request 回傳的錯誤碼中具有意義的轉成 domain layer exception,像是 BoxNotFoundException 及 UserNotFoundException,其他低階的錯誤則以 IOException 拋出。那這和抽象滲漏有什麼關係呢?Proxy 設計的目的就是想提高抽象程度,讓使用的開發者覺得好像 Box 這個物件就在他們的環境中,但 IOException 暴露出其實取得 Box 或更動 Box 都是需要網路溝通的,BoxNotFoundException 則透露出 Box 實體是一個可能不存在的 proxy。
那單純的 remote proxy 呢?就 exception 宣告的位置來看,remote proxy 比 virtual proxy 稍微好一些,至少 getName() 和 getDescription() 不會拋出BoxNotFoundException,畢竟已經取得 Box 實體時這些資訊已經一併取回了,但還是有一個函式 addMember(userId) 會拋出,這等等再談為什麼。
最後是 client 的版本,可以看出來 exception 都集中在 BoxManager 身上,而 Box 實體則沒有任何會拋出 exception 的函式。
其實不管哪一種方式,都免不了抽象滲漏,因為不論怎樣設計,都還是會拋出 IOException、BoxNotFoundException 或是 UserNotFoundException,只是看哪種方式讓使用者覺得比較自然,exception handling 比較方便,以這樣的角度來看,我個人覺得 client 的方式優於 remote proxy,remote proxy 優於 virtual proxy。

Asynchronous

開始開發 SDK 之前,團隊先訂下幾個共識,像是 (1) 最低相容的 Java 版本 (Java 6)、(2) 全部都是 synchronous API、(3) 盡可能減少相依的第三方函式庫並提供整合的單一 JAR 檔 (JAR with all dependencies),以及 (4) 至少所有 public API 都有 JavaDoc 說明並提供使用文件。既然全是 synchronous API,那標題為什麼是 Asynchronous 呢?
當初考量到 Servlet 雖然從 3.0 開始便支援 asynchronous requests,但使用既有框架 (像是 Struts 2、J2EE、Spring framework 等) 寫的應用程式,大多是以 synchronous 的方式撰寫,要在 synchronous 的程式中取得 asynchronous API 的回傳值,或是捕捉錯誤 (基本上要自己重新拋出,不然無法用 try-catch 處理),那是一件很麻煩的事情。反之,asynchronous 程式 (例如用 future 方式撰寫) 中要把 synchronous API 串在一起相對比較容易 ,Java 有提供工具類別能簡單封裝成 future 串起來。
但這跟上述三個設計方式有什麼關係?主要是哪種設計方式能讓 SDK 的使用者更容易知道那些 method 的呼叫是會發出 request 的,有必要的話,可以用包裝成 future 的作法以 asynchronous 的方式呼叫,讓 server 端的處理效率更高。事實上,SDK 的主要使用情境是在 server 端,但這 SDK 要在 Android 上使用也是可以的,我相信應該有不少多開發者,在呼叫某些沒有標示會發出 request 的 method 時遇到 NetworkOnMainThreadException,然後在心中暗念三字經的經驗吧!
在開發 SDK 前,有稍微研究一下 Google、Amazon 和 Microsoft 的幾個 SDK 的原始碼 (開源就是方便研究),有注意到 @WebMethod 這類的 annotation,標註在會發出 request 的 method 上,這似乎是個解法 (我離開前也替公司的 SDK 加入類似的 annotation),但我個人覺得即使沒有 annotation,所有發出 request 的 API 都在 client 身上,比較容易讓 SDK 使用者意會到 BoxManager 的所有函式都是會發出 request 的,反之,remote proxy 和 virtual proxy 則沒那麼明顯。

Synchronization

這裡的 synchronization 不是指會 blocking 的 method,而是指資料的同步,回顧整個使用情境,Box 物件都是一個代表遠端的容器,而這個物件可能由有授權的使用者透過 host server 提供的其他服務進行修改或刪除,在這情況下,不管是哪一種方式設計,一個 Box 物件都可能指向一個已被刪除的容器,因此,剛剛在 exception 一節討論 virtual proxy 時,對一個代表已刪除容器的 Box 物件呼叫 addMember(userId) 時,host server 會發現該 request 想對一個不存在的容器進行操作,最後會拋出 BoxNotFoundException,原因就是資料的不同步。
先不考慮零時差同步,要做到一定時間內,資料完全同步也是需要不小的 effort,像是每個 SDK 實體維持一個固定連線,讓 host server 能推播變更給 SDK 實體,能更新所有物件的狀態。一般來說,proxy 除了可能代表不存在的資源,其內部的狀態也可能過期,例如:remote proxy 的 Box 實體中會有 name 與 description 的資訊,隨時都可能會過期;virtual proxy 的 Box 實體中,若 info() 函式若會保存已取得的資訊,也會有過期的可能;client 的方式,Box 是一個 DTO (Data Transfer Object) 也有會過期的可能。
就過期這一點,不是三種方式都一樣嗎?一般來說,比較會把 client 的 Box 物件當成用過即丟的物件,額外的操作也不依賴在 Box 物件上,而 proxy 則會是持有一陣子的物件,所以才會有明明有一個 Box 物件,呼叫其物件卻拋出 BoxNotFoundException 的詭異現象。

Serialization

文章已經有點長了,最後再說一個因素,考慮到 third-party App server 的節點可能不只一個,例如 Figure 3 有二個節點,前面透過 load balancer 將 Web client 的 request 平均分散到不同的節點,節點之間也可能互相溝通。如果 Box 物件都是用完即丟的情況,那也許不用考慮到要將一個 Box 物件從一個節點轉移到另一個節點的問題,但如果有需要轉移,Box 物件必須支援序列化 (serialization) 和反序列化 (deserialization),使其能在透過網路傳遞,
由於封裝的關係,不管用什麼方式,序列化和反序列化的實作肯定是由 SDK 負責,這也是一個 effort。Client 的 Box 是個 POJO,序列化上很容易,但 proxy,不管是 remote proxy 或是 virtual proxy 都要在另一個節點將與 host server 溝通的物件也全部還原,相對麻煩很多。
Figure 3 多節點情境

結語

說了這麼多,似乎 client 就是唯一解了?其實不然,在寫這篇文章時,內心也在 virtual proxy 和 client 二者中猶豫,只是最後選了 client,可能是因為它相對簡單吧!但如果有更多其他因素加入思考,也許又有不同的結果,我相信還有更多可以考慮的因素,不知道大家在做設計決策時,還有考慮那些因素呢?或是,看完這分析後,有不一樣的決定呢?甚至是上述三者以外不一樣的設計呢?歡迎一起討論。

方格子整體來說還不錯,在引言的排版上是我喜歡的,但比較可惜的是 Gist 區塊的排版略顯壅擠一點,而且無法加上 caption。另一個可惜的點是沒有支援內文程式碼字型的支援,這在軟體開發圈裡可是一件大事,但軟體開發圈在這裡應該是小圈圈 (笑)。
即將進入廣告,捲動後可繼續閱讀
為什麼會看到廣告
avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
你可能也想看
Google News 追蹤
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
※ 原本狀態:伺服器渲染 這是 MVC 架構下的 request / response 示意圖,在這張圖呈現的架構裡,畫面和資料都由同一個架構處理。 伺服器渲染流程: 瀏覽器針對特定網址送出請求。 路由器解析請求後,轉接給對應的 controller。 controller 按照要求,透過
Thumbnail
設計師在臺灣面臨著許多挑戰,這篇文章從我個人經驗出發,討論了獨立接案對設計師的影響,包括時間管理、薪水不穩定、自我探索、生存的挑戰等。同時,也探討了獨立接案後自我的提升和成長,以及對客戶需求的更深入瞭解。文章提供了設計師在獨立接案中所面臨的挑戰,並就如何應對這些挑戰的心得建議。
※ 生產者和消費者模式 定義: 生產者和消費者在同一時間內共同存取某一個資料空間。生產者負責生成數據並將其放入共享空間,消費者負責從共享空間中取走數據進行處理。兩者之間互不相干,也不須互相知道對方的存在。 共同存取資料空間:生產者和消費者共享同一個資料空間。這個空間通常是緩衝區或隊列,用於在它
Thumbnail
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
RPC(Remote Procedure Call)是一種不需要理解底層網路技術就可以透過網路請求服務。主要用於分散式系統中的服務相互呼叫。 架構 Registry:負責將服務發佈成遠端服務,管理遠端服務,提供服務。 RPC Server:負責提供操作介面。 RPC Client:負責透
※ 什麼是 RESTful API? 這種運用 HTTP 來表達語義的路由設計風格稱為 RESTful API,它描述了如何實現 Web API 的架構。所謂的 API 是應用程式介面 (application programming interface),網址也是一種應用程式的「介面」,故稱為
Thumbnail
當我們在撰寫一套系統的時候, 總是會提供一個介面讓使用者來觸發功能模組並回傳使用者所需的請求, 而傳統的安裝包模式總是太侷限, 需要個別主機獨立安裝, 相當繁瑣, 但隨著時代的演進與互聯網的崛起, 大部分的工作都可以藉由網頁端、裝置端來觸發, 而伺服端則是負責接收指令、運算與回傳結果, 雲端
Thumbnail
在開發前後端分離架構時,使用兩個不同網域所遇到跨域請求問題。特別是在POST請求時行為差異大,揭示了「簡單請求」與「預檢請求」的關鍵差異。簡單請求不需預檢,但application/json會觸發預檢請求,需透過特定設定解決。分享這篇文章希望幫助開發者有效處理跨域問題。
Thumbnail
Service Worker 是用於客戶端的攔截器,可以使用 Cache Storage 和 IndexDB,並有自己的生命週期。Web Worker 用於處理可能會使用到大量運算且不希望影響到使用者體驗的任務。Shared Worker 可以在相同 Domain 的不同頁面上共享訊息。
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
※ 原本狀態:伺服器渲染 這是 MVC 架構下的 request / response 示意圖,在這張圖呈現的架構裡,畫面和資料都由同一個架構處理。 伺服器渲染流程: 瀏覽器針對特定網址送出請求。 路由器解析請求後,轉接給對應的 controller。 controller 按照要求,透過
Thumbnail
設計師在臺灣面臨著許多挑戰,這篇文章從我個人經驗出發,討論了獨立接案對設計師的影響,包括時間管理、薪水不穩定、自我探索、生存的挑戰等。同時,也探討了獨立接案後自我的提升和成長,以及對客戶需求的更深入瞭解。文章提供了設計師在獨立接案中所面臨的挑戰,並就如何應對這些挑戰的心得建議。
※ 生產者和消費者模式 定義: 生產者和消費者在同一時間內共同存取某一個資料空間。生產者負責生成數據並將其放入共享空間,消費者負責從共享空間中取走數據進行處理。兩者之間互不相干,也不須互相知道對方的存在。 共同存取資料空間:生產者和消費者共享同一個資料空間。這個空間通常是緩衝區或隊列,用於在它
Thumbnail
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
RPC(Remote Procedure Call)是一種不需要理解底層網路技術就可以透過網路請求服務。主要用於分散式系統中的服務相互呼叫。 架構 Registry:負責將服務發佈成遠端服務,管理遠端服務,提供服務。 RPC Server:負責提供操作介面。 RPC Client:負責透
※ 什麼是 RESTful API? 這種運用 HTTP 來表達語義的路由設計風格稱為 RESTful API,它描述了如何實現 Web API 的架構。所謂的 API 是應用程式介面 (application programming interface),網址也是一種應用程式的「介面」,故稱為
Thumbnail
當我們在撰寫一套系統的時候, 總是會提供一個介面讓使用者來觸發功能模組並回傳使用者所需的請求, 而傳統的安裝包模式總是太侷限, 需要個別主機獨立安裝, 相當繁瑣, 但隨著時代的演進與互聯網的崛起, 大部分的工作都可以藉由網頁端、裝置端來觸發, 而伺服端則是負責接收指令、運算與回傳結果, 雲端
Thumbnail
在開發前後端分離架構時,使用兩個不同網域所遇到跨域請求問題。特別是在POST請求時行為差異大,揭示了「簡單請求」與「預檢請求」的關鍵差異。簡單請求不需預檢,但application/json會觸發預檢請求,需透過特定設定解決。分享這篇文章希望幫助開發者有效處理跨域問題。
Thumbnail
Service Worker 是用於客戶端的攔截器,可以使用 Cache Storage 和 IndexDB,並有自己的生命週期。Web Worker 用於處理可能會使用到大量運算且不希望影響到使用者體驗的任務。Shared Worker 可以在相同 Domain 的不同頁面上共享訊息。