網頁

搜尋此網誌

2021年11月25日 星期四

CloudMosa 面試心得

轉貼 https://t5318019.home.blog/2019/12/27/cloudmosa-job-interview/ (因為 Google 搜尋不到)

這篇文章分享 CloudMosa (美商雲端科技) 的面試心得。會投這間公司主要是他們的 Puffin Browser 與創辦人,所以就試著應徵 SDET 看看,以自己的能力來說是沒機會的,但還是硬著頭皮去試試,感謝 CloudMosa 面試官的寶貴時間,還讓我問了一堆問題 (感覺反而是採訪)。

CloudMosa 面試據說有二階段,我只有去第一階段,第二階段說是用 Skype 跟創辦人面試。第一階段面試大約要 4 小時,我 13:00 去到 19:00 才離開,總共有 4 位面試官輪流面試,各自獨立進行,所以可能要自我介紹 4 次。中間沒有特別休息,只去了一次洗手間,面試結束感覺非常疲累,20:00 回到家吃飯洗澡弄一弄就睡了。

面試流程大該分成 3 個部分。第一是自我介紹,說說過去你做了什麼偉大的事情,第二是面試官提問,第三是自己的提問。面試官的面試時間不一定,每位大概是 1 至 2 小時。 4 位面試官據說是從資歷淺到深來進行,但因為我當天面試官有兩位住在桃園和新竹,導致順序有些調整。

面試過程感覺是一關比一關困難,大概在第三關的時候就覺得沒機會了,最後第四關反而比較輕鬆自在,像是在跟同事討論事情的感覺。

過程

由於自己是軟體開發人員 (SDE),第一關和第四關的面試官是 SDET,專業領域比較沒有辦法深入的討論,面試過程比較像是討論產品測試的一些想法與問題。

第二關的面試官是 SDE ,這部分的面試就硬了。有個簡短的 pseudocode 程式撰寫,考題是 LRU 快取演算法,寫完和面試官討論了程式邏輯和複雜度。事後回想覺得當時太緊張了,面試官的訊息有些沒有抓到,可能回答錯誤。另外,也從我過去的工作經驗問了 SQL 和 NoSQL 的差異與應用情境,這題也沒有回答的很好,覺得「知道是一回事,能夠妥善回答又是另一回事」。

當天第三關的面試官是 VP ,主要是問人格特質的部分,技術的問題偏少。這邊我覺得被過去的工作思維給限制住了,像是面試官問了一個要如何規劃設計 Puffin Browser 的測試,當時回答是看看手邊有多少資源才能做多少事,但事後回想這樣應該不正確,必須「以終為始」來規劃設計。要做大事情,要想著可以找到和創造那些資源,而不是被資源限制住。

VP 最後問了兩個問題,(1) 如果有一百萬美金從事資訊領域創業,不求任何營收,在兩至三年的時間內,你要做什麼? (2) 如果有一百萬美金從事資訊領域創業,在兩至三年的時間內需要回收賺錢,你要做什麼?

因為第一個問題我沒有答案, VP 才又提出第二個問題,當時覺得第二個問題比較難。但現在我反而認為第一個問題才難,問題的困難其實在於「做什麼去創業」的想法,有營收限制條件的第二題反而比較容易一些。

心得總結

  • 感謝 CloudMosa 的面試機會。 年紀稍大,越覺得時間寶貴,別人願意給面試機會就是對能力的一種肯定。
  • CloudMosa 的面試是專業的,過程中面試者是受到尊重的。據說面試官結束要寫報告,不是隨隨便便的。
  • 面試時的自我介紹要練習,因為可能要說 4 遍,不應該太長。
  • 第一階段面試很久,大約 4 小時,要吃飽才去啊!中間可以去洗手間,面試官還會問說要不要喝飲料呢。
  • 公司距離不是問題,新竹以北的高手可以試試看。
  • 根據面試官表示
    • 他們要找高手中的高手,厲害的人做事可以減少一堆問題 。
    • 自主管理,工作目標自己找 。
    • 彈性上下班,一週有一次和創辦人一起開會。
    • 6 個月至 1 年的試用期,不適任資遣 。
  • 面試紀念品: Puffin Browser 馬克杯。

2016年8月9日 星期二

Windows File API

「Windows 有關檔案操作的 API 到底有多少個?」因為這個問題查了 MSDN 而撰寫這篇作為紀錄。

Windows 有關檔案 (file) 操作的 API 目前總共有 130 個,依序字母開頭排序如下:
  1. AddUsersToEncryptedFile
  2. AreFileApisANSI
  3. CancelIo
  4. CancelIoEx
  5. CancelSynchronousIo
  6. CheckNameLegalDOS8Dot3
  7. CloseEncryptedFileRaw
  8. CopyFile
  9. CopyFile2
  10. CopyFile2ProgressRoutine
  11. CopyFileEx
  12. CopyFileTransacted
  13. CopyProgressRoutine
  14. CreateFile
  15. CreateFile2
  16. CreateFileTransacted
  17. CreateHardLink
  18. CreateHardLinkTransacted
  19. CreateIoCompletionPort
  20. CreateSymbolicLink
  21. CreateSymbolicLinkTransacted
  22. DecryptFile
  23. DeleteFile
  24. DeleteFileTransacted
  25. DuplicateEncryptionInfoFile
  26. EncryptFile
  27. EncryptionDisable
  28. ExportCallback
  29. FileEncryptionStatus
  30. FileIOCompletionRoutine
  31. FindClose
  32. FindFirstFile
  33. FindFirstFileEx
  34. FindFirstFileNameTransactedW
  35. FindFirstFileNameW
  36. FindFirstFileTransacted
  37. FindFirstStreamTransactedW
  38. FindFirstStreamW
  39. FindNextFile
  40. FindNextFileNameW
  41. FindNextStreamW
  42. FlushFileBuffers
  43. FreeEncryptionCertificateHashList
  44. GetBinaryType
  45. GetCompressedFileSize
  46. GetCompressedFileSizeTransacted
  47. GetExpandedName
  48. GetFileAttributes
  49. GetFileAttributesEx
  50. GetFileAttributesTransacted
  51. GetFileBandwidthReservation
  52. GetFileInformationByHandle
  53. GetFileInformationByHandleEx
  54. GetFileSize
  55. GetFileSizeEx
  56. GetFileType
  57. GetFinalPathNameByHandle
  58. GetFullPathName
  59. GetFullPathNameTransacted
  60. GetLongPathName
  61. GetLongPathNameTransacted
  62. GetQueuedCompletionStatus
  63. GetQueuedCompletionStatusEx
  64. GetShortPathName
  65. GetTempFileName
  66. GetTempPath
  67. ImportCallback
  68. LockFile
  69. LockFileEx
  70. LZClose
  71. LZCopy
  72. LZInit
  73. LZOpenFile
  74. LZRead
  75. LZSeek
  76. MoveFile
  77. MoveFileEx
  78. MoveFileTransacted
  79. MoveFileWithProgress
  80. OpenEncryptedFileRaw
  81. OpenFile
  82. OpenFileById
  83. PostQueuedCompletionStatus
  84. QueryRecoveryAgentsOnEncryptedFile
  85. QueryUsersOnEncryptedFile
  86. ReadEncryptedFileRaw
  87. ReadFile
  88. ReadFileEx
  89. ReadFileScatter
  90. RemoveUsersFromEncryptedFile
  91. ReOpenFile
  92. ReplaceFile
  93. SearchPath
  94. SetEndOfFile
  95. SetFileApisToANSI
  96. SetFileApisToOEM
  97. SetFileAttributes
  98. SetFileAttributesTransacted
  99. SetFileBandwidthReservation
  100. SetFileCompletionNotificationModes
  101. SetFileInformationByHandle
  102. SetFileIoOverlappedRange
  103. SetFilePointer
  104. SetFilePointerEx
  105. SetFileShortName
  106. SetFileValidData
  107. SetSearchPathMode
  108. SetUserFileEncryptionKey
  109. UnlockFile
  110. UnlockFileEx
  111. WofEnumEntries
  112. WofEnumEntryProc
  113. WofEnumFilesProc
  114. WofFileEnumFiles
  115. WofGetDriverVersion
  116. WofIsExternalFile
  117. WofSetFileDataLocation
  118. WofShouldCompressBinaries
  119. WofWimAddEntry
  120. WofWimEnumFiles
  121. WofWimRemoveEntry
  122. WofWimSuspendEntry
  123. WofWimUpdateEntry
  124. Wow64DisableWow64FsRedirection
  125. Wow64EnableWow64FsRedirection
  126. Wow64RevertWow64FsRedirection
  127. WriteEncryptedFileRaw
  128. WriteFile
  129. WriteFileEx
  130. WriteFileGather
將檔案操作的 API 進行分類整理,可以分成下列幾類:
說到檔案 API 也不能不談到目錄 (directory) 操作的 API ,目前總共有 11 個,依序字母開頭排序如下:
  1. CreateDirectory
  2. CreateDirectoryEx
  3. CreateDirectoryTransacted
  4. FindCloseChangeNotification
  5. FindFirstChangeNotification
  6. FindNextChangeNotification
  7. GetCurrentDirectory
  8. ReadDirectoryChangesW
  9. RemoveDirectory
  10. RemoveDirectoryTransacted
  11. SetCurrentDirectory
將目錄操作的 API 進行分類整理,我們可以分成下列 4 類
參考資料:
###

2016年7月11日 星期一

WiX toolset

WiX 是 Windows Installer XML 的縮寫,WiX 顧名思義是「Windows 安裝程式的 XML 語言」,除此之外, WiX 提供一系列命令列工具程式,因此稱為「WiX 工具集 (toolset)」。我們使用 WiX toolset 可以做出專業的 Windows 安裝程式來部署我們的產品,重點是 WiX toolset 是免費和開源的軟體,另外 WiX toolset 和 Visual Studio 與 MSBuild 高度整合,支援 IntelliSense 和專案方式的操作,讓開發上更是方便。現在 WiX toolset 的版本是 3.10,大約是每年會發佈一次,目前這個專案已經是 .NET Foundation 的其中一個專案了。

在開始使用 WiX toolset  之前,必須先瞭解 Microsoft Windows Installer 相關的基礎知識,因為 WiX toolset 是基於 Windows Installer 技術所開發出來的工具,XML 標籤的組織方式也是依據 Windows Installer 的概念。Windows Installer 是 Windows 作業系統提供的安裝 (installation) 和配置 (configuration) 服務,目前最新的版本是 Windows Installer 5.0。

我們透過安裝包 (installation package) 部署產品 (product) 或應用程式 (application),安裝上最重要的概念是「元件 (Components)」和「功能 (Features)」。一個產品包含一至多個功能,功能之間可以是階層關係 (父功能和子功能 ),而一個功能則由一至多個元件所組成 ,注意到一個元件可能屬於多個功能之中。功能是從使用者的觀點來看,而元件則是從程式設計的觀點來看,使用者通常不會察覺到元件,當使用者選擇安裝的功能後,Installer 會自動安裝相關的元件。因此在打造一個安裝包的過程中,我們必須決定如何組織元件和功能之間的關係。

WiX toolset 是編譯式的工具,需要將 XML 的原始碼透過編譯和連結才能產生 Windows 的安裝包,以下介紹相關的檔案類型和工具。

WiX 工具

  • candle.exe
    前處理器 (preprocessor) 和編譯器 (compiler),將 XML 的原始碼檔案 (.wxs) 編譯成物件檔 (.wixobj)。
  • light.exe
    連結器 (linker),將一至多個物件檔連結成 Windows Installer 資料庫 (.msi 或 .msm)。
  • lit.exe
    用來將多個物件檔變成程式庫 (.wixlib) 以提供 light 連結器使用。
  • dark.exe
    將 Windows Installer 資料庫轉換成原始碼檔案 (.wxs)。
  • heat.exe
    將目錄、檔案或專案「收割 (harvest) 」成原始碼檔案 (.wxs)。
  • insignia.exe
    數位簽章的工具
  • melt.exe
    將 .msm 換成原始碼檔案 (.wxs) 的元件群組。
  • torch.exe
    執行 diff 差異產生.wixmst 或 .mst 檔案。
  • smoke.exe
    執行 .msi 或 .msm 的確認驗證。
  • pyro.exe
    將 .wixmsp 和 wixmst 產生補丁 (patch) 安裝檔。
  • WixCop.exe
    原始碼檔案的 schema 升級與格式化。
  • WixUnit.exe
    驗證 XML 檔案和輸出檔案。
  • lux.exe
    Wix 的擴充套件,用來做資料驅動的單元測試。
  • nit.exe
    執行 lux 的單元測試。

WiX 檔案類型

  • WiX Include File (.wxi)
    類似 C++  語言的標頭檔,這是一個 XML 檔案,根元素是 <Include>。這個檔案是用來被被引入到其他原始碼檔案中。
  • WiX Localization File (.wxl)
    語言本地化的檔案,根元素是 <WixLocalization>,這個檔案是用來翻譯顯示的文字。
  • WiX Source File (.wxs)
    WiX toolset 的原始碼檔案,最重要的一個檔案,根元素是 <Wix>,用來定義安裝包的行為。
  • WiX Object File (.wixobj)
    candle.exe 編譯原始碼檔案產生的物件檔。
  • WiX XML Output File (.wixout)
    light.exe 連結各物件檔產生的輸出檔案。
  • WiX Library File (.wixlib)
    程式庫檔案,在多個 WiX 專案中共用程式碼。
  • WiX Debug File (.wixpdb)
    由連結器產生的檔案,用來除錯。
  • WiX XML Patch File (.wixmsp)
    在補丁專案中連結各物件檔產生的輸出檔案。
  • WiX Transform File (.wixmst)
    用來表示輸出檔之間的差異
  • Windows Installer Installation Package (.msi)
    安裝包檔案,Windows Installer 最基本的安裝單位。
  • Windows Installer Merge Module (.msm)
    模組合併檔,用來在多個.msi 檔案之間共用安裝邏輯。
  • Windows Installer Transform (.mst)
    用來在 .msi 檔案上套用變更。
  • Windows Installer Patch Creation Process (.pcp)
    使用 Windows Installer SDK 用來建置補丁的輸入檔。
上述介紹這些可能還不是很清楚,最快認識的方式是自己用 WiX toolset 建置一個 Windows 安裝程式,如果你已經有現成的 Visual Studio 專案,建議先參考「Creating a Simple Setup 」這篇文章將專案做成一個安裝程式。

參考資料:
###

2016年6月18日 星期六

A Curious Mind 好奇心

「我沒有特殊才能,只有強烈的好奇心。」艾伯特‧愛因斯坦 (Alvert Einstein)

這篇是記錄「好奇心:生命不在於找答案,而是問問題」的重點整理。

布萊恩‧葛瑟 (Brian Grazer), 查爾斯‧費希曼 (Charles Fishman) 著,李宜懃,好奇心:生命不在於找答案,而是問問題,台北市:商周出版:城邦文化發行,2015.08。
譯自:A Curious Mind: The Secret to a Bigger Life

作者葛瑟先生寫這本書有三個目的 (p.22):
  1. 喚起你重視好奇心的價值和力量。
  2. 分享自己運用好奇心的方式,希望這些經驗能激勵你嘗試在日常生活中身體力行。
  3. 拋磚引玉提醒所有人,為何如此重要的特質在今天仍不受重視,沒有得到適當的教導和啟發。
以下記錄書上的一些重點整理:

1 無藥可解的好奇心
  • 好奇心若要有效,至少必須搭配另外兩個關鍵特質:第一,要有能力把焦點放在問題的答案上。第二,願意採取行動。
  • 所謂有能力提出任何問題,可表現在兩件事上面:有探索答案的自由,以及挑戰權威的能力。好奇心本身就是權力的一種形式,也是勇氣的一種形式。
  • 人們熱於與人交談,特別是談論自己以及工作內容;找個小藉口有助於與他們直接交談。
  • 在管理時,提出問題往往比發號施令還有效。
  • 葛瑟先生運用好奇心最重要的方式:用好奇心來說故事,其實這也是他的職業。
  • 故事是否新奇,端看說故事的人所切入的角度。
2 警察局長、電影大亨和氫彈之父:從其他人的角度思考
  • 我們都受限於自己的思維方式,被困在自己與其他人打交道的方式中,我們習慣於自己看到的世界,往往因此認為世界就是我們看到的樣子。
  • 對許多行業而言,有能力去想像其他人的觀點也是一項重要的策略工具。
  • 如果不承認某個特質的存在,就無法理解、欣賞和加以欣賞。
  • 好奇心與創意和創新之間的關係其實相當密切:好奇心是激發創意的工具,也是提供逛新的技巧。問問題會創造出一種創新和創意的思維模式。
  • 出於好奇心而提出問題會鼓勵對方參與,運用好奇心顛覆自己的觀點往往相當值得,縱使結果不如預期。
3 好奇心是好故事的關鍵要素
  • 堅持下去的唯一方法是沉著冷靜,能把自己與發生在自己身上的是分開。
  • 若要說出偉大的故事,好奇心是其中重要的一環,讓故事能吸引你的注意力,打造出無法抗拒的魅力,讓人忍不住要問那個簡單的問題:接下來會發生什麼事?
  • 好奇心有助於創造出好故事,而說故事必然能激發好奇心。
  • 故事的起源:分享而得的知識。
  • 我們透過故事學習這個世界,了解其他人和他們腦中的想法,以及這些想法與我們之間的差異。
  • 禮貌 (manners) 是你想要表現出來的方式、想要給人的感受。禮儀 (etiquette) 則是利用一些細節,以優雅和窩心的方式讓人感受到溫暖。
4 好奇心給你超級英雄般的力量
  • 好奇心比勇氣更能戰勝恐懼
  • 用好奇心打敗「不行」,用好奇心來想清楚如何讓對方說「可以」,只是與你想像的方式可能不太一樣。
  • 堅持是驅策你前進的動力,好奇心為你指引方向。
  • 提出問題是關鍵,可以幫助自己、修正點子,說服他人。
  • 提出問題並不會讓你偏離軌道或分心,反而能讓你步上正軌。
  • 「一天中最難打的電話要先打」。它強調的是一種勇氣,一種個性,就算你百般不願意,也要確實面對必須做的任務,然後動手處理。
  • 好奇心比勇氣更能戰勝恐懼;事實上,單憑生理上的勇氣原本可以避開危險,卻因好奇心而讓許多人置身險境。飢餓、愛和好奇是生命中偉大的動力。
5 每次交談都是好奇心對話
  • 人與人的連結賦予我們生活的意義,人際關係也是我們之所以在這裡的原因。
  • 不採取行動本身反而是最有力的行動。
  • 人際關係是人活著最重要的一部分,它是維持幸福感和生活滿足感的關鍵。而好奇心是與人產生連結和維持良好關係的關鍵。
  • 提出問題當然會得到資訊。
  • 提出問題最重要的意義是,人們必須為自己的決定如何執行提出合情合理的解釋。提出合情合理的解釋也代表要回答關於細節的問題。
  • 人性有個簡單的特質,就是比較喜歡選擇去做某些事情,而不是接到命令才做這些事情。
  • 請求別人協助而不是發號施令,幾乎總是比較明智的做事方法。
  • 如果你希望別人採取某個立場,疑問句比直述句或勸誘更能有效傳達出價值觀。
  • 如果是人們自己先提出問題,就比較願意接納對方給的建議或單純的指示。
  • 問問題的重點必須是得到答案。問題和答案都必須讓一項計劃或決定能有進展。
  • 如果我們對於親近的人失去了好奇心,從那時起我們的關係就開始搖搖欲墜,以幾乎看不見的方式,悄悄地損害這段關係。要是我們不再對身旁的人提出發自內心真正關切的問題,甚至也不再認真傾聽答案,則這段關係會開始漸行漸遠。
  • 在家裡,即使你與伴侶或孩子共處一室,但除非你問他們問題、聆聽答案,否則就無法與他們產生連結。
  • 好奇心有助於維持親密感,與目標無關,而是關於快樂。
6 好品味,以及適時收起好奇心的力量
  • 如果我們不能抱持懷疑的態度提出問題,質問那些告訴我們某件事為真的人,則很容易落入下一個政治或宗教騙子設下的圈套。
  • 品味是在一個框架下,根據你的判斷結果而產生的想法,品味會為你的判斷結果提供信心,品味會讓你更有自信,因為你對喜歡的東西更加了解,你知道什麼是好的,什麼是不好的。
  • 養成不斷提出問題的習慣、提醒自己問問題帶來的價值,以及我們問問題的權利。
7 好奇心的黃金時代
  • 好奇心並沒有激發出太多大家對其本身的好奇,我們對各式各樣的事情感到好奇,唯獨漏了好奇心這個概念。
  • 十七、十八世紀的科學家之所以不同凡響,其中一個主因是他們提出了一些從沒有人問過的問題。
  • 因好奇心而提出問題不只是理解世界的一種方式,也是改變世界的一種方式。掌握權力的人一直都知道這個道理,從舊約聖經、希臘羅馬神話可見一斑。
  • 每個人都必須小心,網路會麻醉我們,而非激勵我們。下面兩件事你無法在網路上找到:一、如果這些問題從沒有人問過,你就搜尋不到答案。二、你無法透過 Google 搜尋新點子。網路只能告訴我們已知的事情。
  • 你知道的愈多,可以做的就愈多。
  • 好奇心是一種心態。更具體地說,是心胸要開闊,好奇心是一種接收能力。
###

2016年5月16日 星期一

Clean Code 無瑕的程式碼

這篇是記錄「無瑕的程式碼」第 1 章至第 5 章的重點整理。

序 (Introduction)

  • 學習工藝典範 (craftsmanship) 可分成兩部分:知識 (knowledge) 和實作 (work)
  • 你必須獲得程式工藝師所知道的原則 (principles)、模式 (patterns)、實踐 (practices) 與啟發 (heuristics),並且必須研磨這些知識,透過努力實作和練習,將這些知識融入你的手指、眼睛和身體裡。

無瑕的程式碼 (Clean Code)

  • 撰寫機器能執行的細節需求稱為「撰寫程式 (programming)」,而這些明確描述的文字稱為「程式碼 (code)」。
  • 程式碼是最終被我們用來闡述需求的語言。
  • 急於讓產品上市,導致程式碼變得一團糟,當開始加入越來越多的產品功能時,程式碼就變得越來越糟,一直到再也無法管理這團混亂。劣質的程式碼可能導致一家公司的倒閉。
  • 勒布朗克法則 (LeBlanc's Law):待會兒等於永不。
  • 花時間保持程式碼的整潔,並不只關係到成本的效益,還關乎到專業職場的生存之道。
  • 錯並不在這些事物上,而是在我們本身,我們不夠專業。
  • 你製造了爛的程式並不會因此趕上截止期限,事實上,爛程式只會馬上讓你的開發速度變得更慢,並導致你錯過截止期限。在截止期限前完成工作的唯一方法,也是讓開發速度變快的唯一方法,就是隨時隨地,都確保程式碼盡可能的整齊潔淨。
  • 能夠分辨程式碼的好壞,不代表知道如何寫出 Clean Code。「程式感 (code-sense)」是能寫出 Clean Code 的關鍵因素。
  • 沒有辦法先不讀程式就先去寫程式,所以讓程式碼更容易閱讀,也會讓程式碼變得更容易撰寫。
  • 光是把程式碼寫好還不夠,程式碼還必須持續地保持整潔。

有意義的命名 (Meaningful Names)

  • 選一個好的名稱是相當花時間,但省下來的時間比花掉的時間還多。
  • 問題不在於程式碼的簡易度,而在於程式碼的隱含性 (implicity):即程式的上下文資訊未能由程式本身明確地展現出來的程度。
  • 拼字與意圖相似的字詞就是恰當資訊,而使用與意圖不一致的拼寫就是誤導了。
  • 增加數字的序列或無意義的字詞或許可以滿足編譯器的規定,但這並不是一個好的程式設計。假使名稱必須有所不同,那麼它們也應該代表著不同的意義才對。
  • 讓你的命名能夠唸得出來。如果你唸不出你取的名稱,那就只能像白癡般用拼音來討論它。
  • 長命名勝過短命名。如果一個變數或常數在程式裡不少地方都可能使用,那最好給它們一個容易被搜尋到的名稱。
  • 現代程式語言有更豐富的型別,而且編譯器或編輯器會替我們記住型別,並要求這些型別一致,因此命名進行編碼、加上額外的字首或字尾顯得沒有必要。
  • 專業的程式設計師知道「清楚明白才是王道」,專業人士運用本身的好能力,寫出讓別人可以瞭解的程式碼。
  • 類別和物件應該使用名詞或名詞片語來命名,類別的名稱也不應該是動詞,方法應該使用動詞或動詞片語。
  • 替單一抽象概念挑選一個字詞,並堅持持續地使用它。
  • 避免使用同一個字詞代表兩種不同的目的。使用同一個字詞代表兩種不一樣的想法,基本上就是雙關語,這必須避免。
  • 優先使用電腦領域的技術性名稱命名變數,其次才是使用問題領域的詞彙來命名。
  • 較短的命名若能清楚地表達涵義,通常好過於較長的名稱。盡量減少在名稱上加入不必要的上下文資訊 (context)。

函式 (Functions)

  • 關於函式的首要準則,就是要簡短。第二項準則,就是要比第一項的簡短函式還要簡短。
  • 每個函式都一清二楚,透露出本身的意圖。每個函式帶領著你至下個函式,這就是函式該有的簡短。
  • 函式應該做一件事情。它們應該把這件事做好。而且它們應該只做這件事。
  • 如果函式只做了函式名稱下「同一層抽象概念」的幾個步驟,那麼,這個函式就算是只做了一件事。
  • 我們希望程式的閱讀就像是由上而下的敘事。我們希望每個函式後面都緊接著「下一層次的抽象概念」。這是降層準則。
  • 從本質來看,switch 敘述縱是在做 N 件事情。我們無法永遠避開使用 switch 敘述,但我們能確保讓每個 switch 敘述都被深埋到較低抽象層次的類別裡,而且他永遠不會被重複使用。
  • 當每個你看到的程式,執行結果都與你想的差不多,你會察覺到你正工作在 Clean Code 之上。
  • 只要函式越簡短和越集中在做該做的事情上,就越容易替函式取個具有描述性質的名稱。
  • 函式的參數數量,最理想的是零個 (零參數函式;niladic),其次是一個 (單參數函式;monadic),再不然就是兩個 (雙參數函式;dyadic)。可以的話,盡量避免使用三個參數 (三參數函式;triadic)。如果要使用超過三個參數 (多參數函式;polyadic),必須有非常特殊的理由,否則無論無論如何都不應該如此做。
  • 參數和函式處於不同的抽象層次,而且參數強迫你去瞭解目前並不那麼重要的細節。
  • 輸出型的參數比輸入型的參數更難以理解。
  • 當一個函式看起來需要超過兩個或三個的參數時,很可能需要將當中的一些參數包裝在一個類別裡。
  • 在單一參數的形式中,函式與參數要形成一個動詞/名詞的良好配對。另外可以使用關鍵字形式的函式命名,將參數的名稱編碼加入到函式名稱裡。
  • 副作用 (side effects) 就像是個謊言。你的函式保證只做一件事,卻暗地裡偷偷做了七他事情。函式必須要無副作用!
  • 函式應該要能做某件事,或能回答某個問題,但兩者不該同時發生。
  • 使用例外處理取代回傳錯誤碼。
  • 函式應該只做一件事,而錯誤處理就是一件事。所以,一個處理錯誤的函式,應該不能再做其他的事。
  • 不要重複自己 (Don't repeat yourself),重複程式碼也許是軟體裡所有邪惡的根源。從副程式發明以來,軟體發展領域的所有創新,都是為了消除原始碼中的重複。
  • 每個系統都是由某個特定領域的語言設計而成的,而這種特定領域語言則是「程式設計師為了描述系統所設計的」。函式是這個語言裡的動詞,類別則是這個語言裡的名詞。
  • 程式設計大師在撰寫程式時,並不認為自己是在寫程式,而是在說故事。

註解 (Comments)

  • 事實上,在最好的情況下,註解也只不過是一種必要之惡。
  • 一個註解存在的時間越久,事實就越來越偏離當初的程式碼解釋,甚至可能完全就是個誤導。原因很簡單,因為程式設計師並沒有如實地維護它們。
  • 真相永遠只存在於一個地方:程式碼。只有程式碼能忠實地告訴你它的作用,它是唯一準確的事實來源。
  • 整潔具有表達力又極少使用註解的程式碼,遠優於雜亂複雜又滿是註解的程式碼。與其花時間寫註解來解釋你所造成的混亂,不如花時間去整理那堆混亂的程式碼。
  • 真正有益的註解,是你想辦法不寫它的註解。
  • 在每個原始碼檔案的開頭寫入著作權聲明及作者資訊,就是必須且合理的註解。
  • 如果你決定要寫下註解,你就應該花上必要的時間,確保你寫出來的是最好的註解。
  • 如果有一條規則是這麼說的,每個函式都必須有一個 Javadoc ,或每個變數都應該要有註解來說明,那麼這樣的規定就有夠傻的了。
  • 想儲存修改程式碼的日誌、程式碼的出處及署名、不使用的程式碼,原始碼管控系統會是一個比較好的選擇,不要使用註解來儲存這些資訊。
  • 被註解掉的程式碼,就像是一瓶壞掉的葡萄酒,在瓶底所沈澱的殘渣,直接刪掉這些程式碼吧。
  • 註解的目的在於說明無法表達本意的程式碼。如果連註解本身都需要額外的解釋,那真是一件遺憾的事。

編排 (Formatting)

  • 程式的編排是一種溝通方式,而溝通是專業開發者的首要之務。
  • 程式碼的風格和可讀性,會在程式中立下先例,持續地影響程式的可維護性及可擴充性,就算程式碼已經被修改到無法認出原本的面貌,或者程式碼已經消失,你的程式風格和紀律依然會存在。
  • 一個原始碼檔案應該多大才好?簡短的程式碼往往比大型的程式碼更容易讓人理解。
  • 每一行程式碼都代表一個表達式或某個程式子句,還有每一段程式碼都代表一個完整的思緒。應該用空白行來分隔這些思緒。
  • 除非你有一個很好的理由,否則相近的概念不該被分散在不同的檔案裡。
  • 變數的宣告應該盡可能靠近變數被使用的地方。
  • 實體變數應該被宣告在一個大家都熟悉的地方:類別的最下方,或是類別的最上方。
  • 呼叫敘述應該要在被呼叫函式的上方,這能使得程式本身有自然的順序。
  • 程式裡的某些程式碼,希望能和其他程式碼盡可能地相近,因為它們在概念上有著相似的性質。當這個相似性越高時,它們之間的垂直距離就應該越短越好。
  • 我們希望函式呼叫呈現一種向下的相依性。也就是說,一個被呼叫的函式,應該要出現在「執行呼叫的函式」的下方。這也產生了一個良好的項下流程,由上往下查看原始碼時,可以依序發現高層模組,再接著找到低層模組。
  • 一行的程式碼應該有多寬呢?不需要使用捲軸捲到右方才是適當的寬度。(作者個人的寬度上限是 120 個字元)
  • 一個原始檔是個階層結構,而非大綱結構。在這個階層結構下的各個階層都是一個視野 (scope)。
  • 為了要讓視野的層次結構更顯而易見,我們根據各行在階層結構下的層級,對原始碼的各行進行了縮排。
  • 如果沒有了縮排,程式在視覺上將無法被人類輕易地閱讀。
  • 一個團隊的開發者,應該要認同某一種編排風格,而且所有團隊的成員都該使用這種編排風格。我們維持一致的軟體編排風格。我們並不想讓它看起來是由一群意見不合的個體所寫成的。
  • 記住,一個好的軟體系統,是由一套具良好可讀性的文件所組成。它們需要有一致且順暢的風格,讀者要能確信,他/她在某個原始檔看到的編排所代表的意義,在別的原始檔也是相同的。

參考書籍

Robert C. Martin 著,戴于晉、博碩文化編譯,無瑕的程式碼:敏捷軟體開發技巧守則,新北市:博碩文化,2013。
譯自:Clean Code: a handbook of agile software craftsmanship

###

2016年3月27日 星期日

Upstart

Upstart 是一種 init 服務 (daemon),init 是 Linux 上第一個執行的行程 (process)。init 系統除了 Upstart 之外,還有過去的 System V init (唸作 System Five, 縮寫為 SysV),與後來發展的 systemd 。Upstart 是 Ubuntu 為了取代 SysV init 所開發出來的系統,採用事件為基礎的設計 (event-based),利用事件驅動的方式可以平行執行多個程式,提高開機的速度與效能。

Upstart 有兩個重要的概念:「事件 (Events) 」與「工作 (Jobs)」

Job

Job 是一個工作的單位 (unit of work),Upstart 有三種 Job 類型:
  • Task Job:程式執行一段時間就會結束, Process 有確定壽命和結束狀態,簡稱「Task」。
  • Service Job:長時間執行的程式,通常是背景服務,簡稱「Service」。
  • Abstract Job:不會有子行程產生,純粹僅有工作狀態。
每個 Job 會存在於不同的狀態,共有 11 種狀態 (State):
  • waiting:初始狀態
  • starting:工作即將開始
  • security:工作正在載入 AppArmor 安全政策
  • pre-start:執行 pre-start 當中
  • spawned:即將執行 script 或 exec
  • post-start:執行 post-start 當中
  • running:在 post-start 之後,表示 Job 正在執行的過渡期間狀態
  • pre-stop:執行 pre-stop 當中
  • stopping:在 pre-stop 之後的過渡期間狀態
  • killed:工作即將停止
  • post-stop:執行 post-stop 當中
Job 在 Upstart 系統上是定義在副檔名為 .conf 的組態檔 (Job Configuration File) 裡,檔名應該要能夠表示你所執行的應用服務。至於 Job 要如何撰寫,可以參考 Upstart 文件的第6章

依據組態檔放置的位置不同,Job 可以區分為:
  • System Job:組態檔位於 /etc/init/ 目錄中。
  • User Job:組態檔位於使用者加目錄下的 .init/ 目錄中,Upstart v1.7以後已經不支援。
  • Session Job:Upstart v1.7之後才有的功能,Job 是被使用者的 Session 所啟動,組態檔有多個位置,請參考 cookbook 說明

Event

Event 在 Upstart 裡是用來通知其他 Job 或 Event,Event 被發出 (emit) 後會廣播至整個 Upstart 系統,只有感興趣 (也就是有註冊對應的 Event) 的  Job 或 Event 才會收到。

注意到, Event 的名稱允許和 Job 的名稱是相同的。另外, Event 也可能和 Job State 的名稱相同,但是相同名稱的 Event 和 Job State 不是指同一個事情,雖然運作起來很相似 (變動狀態很像發出事件)。

Upstart 有三種 Event 類型:
  • Signal Event:信號事件是非阻塞 (non-blocking)、非同步 (asynchronous) 的事件,信號事件發出後會立即回傳,允許呼叫者繼續執行其他工作。
  • Method Event:方法事件是阻塞 (blocking)、同步 (synchronous) 的事件,方法事件如同程式設計的 function 功能,呼叫者發出後必須等待方法事件執行完畢,若有問題發生將回傳錯誤資訊。
  • Hook Event:掛勾事件與方法事件類似,是阻塞、同步的事件類型,只是用途不太相同,當作旗標 (flag) 的功能通知其他  Job 或 Event,確定所有接收者都執行完畢才繼續。

操作

在 Ubuntu 上和 Upstart 溝通與互動,有下列幾個程式可使用,詳細指令請參考連結的內容:

參考資料:

###

2016年3月7日 星期一

Data Storage 資料儲存

資料儲存由下至上分別是磁碟 (Disk)、磁碟區 (Volume) 與檔案系統 (File System),從作業系統的角度來看都是存放裝置 (Storage) 組成。以下分別說明相關詞彙。

Disk: 磁碟

  • 磁盤 (Platter):一個至多個磁盤組成一個磁碟。
  • 磁頭 (Head):一個磁盤有兩面,每一面有一個磁頭,也就是一個磁盤有2個磁頭。
  • 磁軌 (Track):磁盤上同心圓定義為磁軌,一面磁盤上有多個磁軌。
  • 磁柱 (Cylinder):各磁盤上相同位置的磁軌。
  • 磁區 (Sector):磁碟上可定位的最小單位,也就是資料最小的儲存單位。大小通常是 512 bytes,現在改為 4096 bytes (4KiB) 的大小 (稱為 4K 磁區)。另外,CD-ROM 的磁區通常是 2048 bytes。
  • 磁碟分割(Partition):磁碟在邏輯上可以分割成不同的區域,讓一個磁碟「看起來」好像有多個磁碟。一個磁碟至少有一至多個磁碟分割。
  • 主要開機紀錄 (Master Boot Record, MBR) :用來記錄磁碟分割的狀態,位於 Disk 上的第一個磁區,包含開機程式碼 (boot code) 與磁碟分割表 (Partition Table)。
  • GUID 磁碟分割表  (GUID Partition Table):另一種用來記錄磁碟分割狀態的格式。
  • 開機磁區 (Boot Sector):可開機 Partition 上的第一個磁區。

Volume: 磁碟區

在邏輯上 (注意是邏輯上而不是實體上),可建立檔案系統的最小單位。Partition 不等於 Volume,透過操作可以將多個 Partition 組成一個 Volume,例如 Logical Volume Management (LVM) 、Redundant Array of Independent Disks (RAID) 技術。

File System: 檔案系統

檔案系統的基本功能是決定檔案如何命名、儲存和組織。
  • 區塊 (Block),Windows 系統上稱為叢集 (Cluster),或是磁叢:磁碟上處理檔案的最小單位。一個叢集由一至多個磁區組成,在檔案系統格式化時決定。
  • FAT:檔案配置表 (File Allocation Table) 檔案系統,有多種版本,包括 FAT12、FAT16和FAT32,FAT 後面的數字表示叢集的位元數大小。
    • FAT16:每個 FAT 實體 (entry) 使用 16 位元紀錄,也就是說最大可定址到216 (=65,536) 個叢集。支援最大檔案大小為 4GB,支援最大磁碟區大小為 4GB。根目錄的檔案與資料夾總數最多為 512 個。
    • FAT32:每個 FAT 實體 (entry) 使用 32 位元紀錄,但保留最高4位元,因此最大可定址到228 (= 268,435,456) 個叢集。支援最大檔案大小為 4GB,支援最大磁碟區大小為 32GB。每個目錄的檔案與資料夾總數最多為 65534 個。
  • NTFS:新技術檔案系統 (New Technology File System)。

參考資料:

###

2016年1月22日 星期五

Driver Development 驅動程式開發

什麼是驅動程式?

驅動程式 (Driver) 是一種軟體元件,介於作業系統 (Operating System) 和硬體裝置 (Device),用途是讓作業系統和裝置彼此能夠溝通。從系統層面上看,由上而下分別是應用程式 (Application)、作業系統、驅動程式與裝置,如下圖所示。

驅動程式在架構上是分層堆疊 (layered in a stack) 而成,因此驅動程式不一定只和硬體裝置溝通,可能和其他驅動程式溝通。驅動程式依照用途不同可以分為下列幾類:
  • 功能驅動程式 (Function Driver)
    直接與硬體裝置直接溝通的驅動程式,稱為 Function Driver。
  • 篩選器驅動程式 (Filter Driver)
    輔助處理 I/O 請求的驅動程式,與其他驅動程式相互溝通,稱為 Filter Driver。
  • 軟體驅動程式 (Software Driver)
    與硬體裝置無關,是為了處理作業系統的核心資料結構,稱為 Software Driver。
  • 匯流排驅動程式 (Bus Driver)
    驅動匯流排以建立裝置樹的驅動程式,稱為 Bus Driver。

驅動程式由誰開發?

驅動程式是由裝置的設計者或製造者所開發,讓作業系統知道如何操作此裝置,進行資料的讀寫。另一方面,依照硬體標準所設計和製造的裝置,這些驅動程式可能就由作業系統廠商開發提供。

驅動程式模型 (Driver Model)

開發硬體裝置的驅動程式有3種模型:
  • VxD (Virtual Device Driver):概念是將硬體裝置虛擬化,讓多個應用程式可以共用實際的硬體,所以稱為「虛擬裝置驅動程式」。是 MS-DOS、Windows 3.x 和 Windows 95 使用的技術。
  • Windows Driver Model (WDM):非 NT-based 的 Windows 98 與 Windows Me,以及 NT-based 的 Windows 2000 與 Windows XP 使用的技術。
  • Windows Driver Frameworks (WDF) ,以前稱為 Windows Driver Foundation (WDF),這是 Windows Vista 以後使用的技術,WDF 包含兩種框架:

硬體裝置的作業系統觀點

在 Windows 系統中,硬體裝置表示為隨插即用裝置樹 (Plug and Play Device Tree,簡稱 Device Tree) 中的裝置節點 (Device Node),裝置節點之間具有父子 (parent/child) 關係,裝置節點由多個裝置物件 (Device Object) 所堆疊組成,稱為裝置節點的裝置堆疊 (Device Stack)

裝置物件是一個DEVICE_OBJECT結構,每個裝置物件會有它對應的驅動程式,在系統上表示為驅動程式物件 (Driver Object) ,是一種DRIVER_OBJECT結構,從另一個觀點來看,裝置堆疊可以看成是「裝置物件—驅動程式」組合所堆疊而成。

裝置物件依據建立的驅動程式不同,區分為:
  • 由 Bus Driver 建立的裝置物件,稱為  Physical Device Object (PDO)。
  • 由 Function Driver 建立的裝置物件,稱為  Functional Device Object (FDO)。
  • 由 Filter Driver 建立的裝置物件,稱為  Filter Device Object (Filter DO)。
注意到 Bus Driver 建立的裝置物件,如果這個裝置物件本身就是匯流排的話,那 Bus Driver 所建立的是 FDO 而不是 PDO。換句話說,一個驅動程式可能同時兼具 Bus Driver 和 Function Driver 的角色功能。另外,一個 Driver Object 可以建立多個 Device Object,這也符合直覺上一個 Driver 可以驅動很多 Device 的想法。

參考資料:
###

2015年9月24日 星期四

Cryptosystem 密碼系統

這篇記錄密碼系統 (Cryptosystem) 的分類

依安全性程度

  • 無條件安全 (Unconditionally Secure)
  • 計算安全 (Computationally Secure)

依加解密資料的處理方式

  • 串流 (Stream) 密碼系統:每次加解密一個 bit 或一個 byte。
  • 區塊 (Block) 密碼系統:以數個 bits 或數個 bytes 為一個單位進行加解密。

依加解密資料的格式

  • 文字 (Text)
  • 多媒體 (Multimedia)

依加解密金鑰的異同

並列出著名的加解密方法 (演算法),與其加密模式、加密方式。
  • 秘密金鑰密碼系統 (Secret-Key Cryptosystems),也稱對稱式密碼系統 (Symmetric Cryptosystems)、單金鑰密碼系統 (One-Key Cryptosystems)。
    • 資料加密標準 (Data Encryption Standard, DES):56 位元金鑰,以64位元為區塊加解密。若明文超過區塊大小則切割成數個區塊處理,各個區塊加解密的處理方式有下列加密模式:
      • ECB (Electronic Code Book) 加密模式:每個區塊各自獨立加解密處理。
      • CBC (Cipher Block Chaining) 加密模式:利用初始化向量 (IV) 與第一個明文區塊進行XOR運算後加密,第一個密文與第二個明文XOR運算後加密。
      • CFB (Cipher Feed Back) 加密模式:初始化向量加密後與第一個明文區塊進行XOR運算,第一個密文加密後與第二個明文區塊進行XOR運算。
      • OFB (Output Feed Back) 加密模式:初始化向量加密後與第一個明文區塊進行XOR運算,初始化向量加密二次後與第二個明文區塊進行XOR運算。
    • 三重資料加密標準(Triple Data Encryption Standard, Triple-DES 或 3DES):串接 3 個 DES 使用,金鑰為168 位元或112位元,一樣以64位元為區塊加解密。。
    • 先進加密標準 (Advanced Encryption Standard, AES),金鑰長度為 128、192 或 256 位元,128 位元區塊加解密,使用 Rijndael 演算法 (讀作“Rain Doll“)。
  • 公開金鑰密碼系統 (Public-Key Cryptosystems),也稱非對稱式密碼系統 (Asymmetric Cryptosystems)、雙金鑰密碼系統 (Two-Key Cryptosystems)。
    • RSA:第一個公開金鑰密碼系統,由 MIT 的 Ron Rivest, Adi Shamir, 與 Leonard Adleman 三位教授所發明,取其性是第一個字母而稱 RSA。目前標準為 PKCS#1或  RFC 3447。基於質因數分解,相同的明文會對應到相同的秘文。
    • ElGamal:基於解離散對數 (Discrete logarithm),相同的明文會對應到不同的秘文,密文長度為明文的 2 倍。
    • 橢圓曲線 (Elliptic curve):基於橢圓曲線的有限域 (finite field),其加解密的運算量較少 (與 RSA 和 ElGamal 相比之下)。
根據參考資料描述:「一般來說,對稱式密碼系統的加解密速度大約比非對稱式密碼系統快1000倍。」因此,為了解決金鑰管理和運算量的問題,通常會使用對稱式與非對稱式所組合成的混合式加密機制。另外,在非對稱式密碼系統的標準中,沒有明確規範金鑰長度。

參考資料: 黃明祥、林詠章著,資訊與網路安全概論(第二版):建構安全的電子商務系統,台北市:麥格羅希爾,2007。

###

2015年8月7日 星期五

Agile, Lean, Kanban 敏捷, 精實, 看板

這篇記錄軟體開發方法的一些原則

敏捷軟體開發 (Agile Software Development)

敏捷軟體開發的 4 大價值觀 (values)
  • 個人與互動 重於 流程與工具
    Individuals and interactions over processes and tools
  • 可用的軟體 重於 詳盡的文件
    Working software over comprehensive documentation
  • 與客戶合作 重於 合約協商
    Customer collaboration over contract negotiation
  • 回應變化 重於 遵循計劃
    Responding to change over following a plan
敏捷軟體的 12 個原則 (principles)
  1. 我們最優先的任務,是透過及早並持績地交付有價值的軟體來滿足客戶需求。
    Our highest priority is to satisfy the customer through early and continuous delivery of valuable software.
  2. 竭誠歡迎改變需求,甚至已處開發後期亦然。敏捷流程掌控變更,以維護客戶的競爭優勢。
    Welcome changing requirements, even late in development. Agile processes harness change for  the customer's competitive advantage.
  3. 經常交付可用的軟體,頻率可以從數週到數個月,以較短時間間隔為佳。
    Deliver working software frequently, from a couple of weeks to a couple of months, with a preference to the shorter timescale.
  4. 業務人員與開發者必須在專案全程中天天一起工作。
    Business people and developers must work together daily throughout the project.
  5. 以積極的個人來建構專案,給予他們所需的環境與支援,並信任他們可以完成工作。
    Build projects around motivated individuals. Give them the environment and support they need, and trust them to get the job done.
  6. 面對面的溝通是傳遞資訊給開發團隊及團隊成員之間效率最高且效果最佳的方法。
    The most efficient and effective method of conveying information to and within a development team is face-to-face conversation.
  7. 可用的軟體是最主要的進度量測方法。
    Working software is the primary measure of progress.
  8. 敏捷程序提倡可持續的開發。贊助者、開發者及使用者應當能不斷地維持穩定的步調。
    Agile processes promote sustainable development. The sponsors, developers, and users should be able to maintain a constant pace indefinitely.
  9. 持續追求優越的技術與優良的設計,以強化敏捷性。
    Continuous attention to technical excellence and good design enhances agility.
  10. 精簡──或最大化未完成工作量之技藝──是不可或缺的。
    Simplicity--the art of maximizing the amount of work not done--is essential.
  11. 最佳的架構、需求與設計皆來自於能自我組織的團隊。
    The best architectures, requirements, and designs emerge from self-organizing teams.
  12. 團隊定期自省如何更有效率,並據之適當地調整與修正自己的行為。
    At regular intervals, the team reflects on how to become more effective, then tunes and adjusts its behavior accordingly.

精實軟體開發 (Lean Software Development)

精實軟體開發的 7 大原則,源自於 Mary Poppendieck & Tom Poppendieck 在 2003 年出版的著作:Lean Software Development: An Agile Toolkit
  1. 消除浪費 (Eliminate waste)
  2. 增強學習 (Amplify learning)
  3. 盡量延遲決策 (Decide as late as possible)
  4. 盡快交付 (Deliver as fast as possible)
  5. 授權團隊 (Empower the team)
  6. 嵌入完整性 (Build integrity in)
  7. 著眼整體 (See the whole)

看板方法 (Kanban Method)

看板方法是由 David J. Anderson 在 2005 年所創造的流程控制方法。

看板方法的 4 個基本原則 (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
看板方法的 6 個實務 (core practices)
  1. 步驟1:視覺化
    Visualize
  2. 步驟2:限制半成品 (WIP) 數量
    Limit Work-In-Progress
  3. 步驟3:管理工作流程
    Manage flow
  4. 策略4:讓規則明確
    Make policies explicit
  5. 策略5:落實回饋循環
    Implement feedback loops
  6. 策略6:由協作改善,經實驗演進
    Improve collaboratively, evolve experimentally using models and the scientific method

參考資料
###

熱門文章