Omni 集團(tuán)是一家屬于員工的公司,在這里,人們可以帶他們的狗來上班。
換句話說:當(dāng)你考慮如何管理大型項(xiàng)目時(shí),首先得考慮文化 。“我們?nèi)绾谓M織項(xiàng)目?用什么樣的源代碼控制管理?” 文化并不糾結(jié)于這些細(xì)節(jié),一個(gè)優(yōu)秀的文化會(huì)形成一個(gè)快樂的團(tuán)隊(duì),他們將弄清楚如何一起工作。而 Omni 在文化建設(shè)上的成果可謂豐碩。
Omni 的文化可以另撰一篇長文,但在這里不打算涉及太多。相反,這里是一次圍繞著技術(shù)的游歷,著重于討論我們?nèi)绾喂芾碜约旱?App。
所有的工程師向 Tim Wood 進(jìn)行匯報(bào),他是 Omni 的 CTO 和創(chuàng)始人。每個(gè)產(chǎn)品都有一個(gè)項(xiàng)目經(jīng)理。
各個(gè) App 的項(xiàng)目組是流動(dòng)的。項(xiàng)目組本身不會(huì)經(jīng)?;螂S意地變動(dòng),不過它們終究還是會(huì)變化。
因?yàn)榇蠹叶荚谵k公室工作,所以我們會(huì)面對(duì)面的溝通。有時(shí)會(huì)召開會(huì)議,其中一些是提前安排好的,而另一些則是臨時(shí)決定的。每個(gè)人都會(huì)參加一周一次的全體員工大會(huì),時(shí)間大約是 20 分鐘。主要部門主管和項(xiàng)目經(jīng)理說明接下來的工作方向。在之后還有每周的開發(fā)會(huì)議,這也持續(xù)約 20 分鐘。討論一些大家普遍關(guān)心的開發(fā)事項(xiàng) (用于規(guī)避逐個(gè)找人溝通,雖然有時(shí)還是不得不這樣做)。
面對(duì)面的交流得益于 Omni 的核心工作時(shí)間:人們希望每天早上11點(diǎn)到下午4點(diǎn)的時(shí)間都在辦公室內(nèi)度過,所以你知道你可以在那個(gè)時(shí)間里找到想找的人。(不過,你也可以來的或早或晚,隨你心意,只要你在工作上投入了一整周的時(shí)間。)
其它途徑的話,包括 E-mail,包括一些郵件列表,和我們內(nèi)部的聊天室加上私信。(我們最近將 Message 和 Jabber 替換為了一套可以保存歷史記錄的系統(tǒng),它還可以播放 GIF。)
每一個(gè)開發(fā)組織都有至少一個(gè)人負(fù)責(zé)解決 Bug 追蹤系統(tǒng)中反饋的問題。Omni 使用了一個(gè)內(nèi)部的 Mac 應(yīng)用 OmniBugZapper (OBZ) ,它具有那些你期望擁有的功能。它不像一些公開應(yīng)用那樣矚目,但它卻是一個(gè)很有用的工具。
(在OmniBugZapper中的) 一個(gè)典型的工作流是:你在當(dāng)前階段發(fā)現(xiàn)了一個(gè) Bug,將它的優(yōu)先級(jí)調(diào)為高,然后打開它,使其他人可以看見你正在解決這個(gè) Bug。
一旦問題解決,你在這個(gè) Bug 上添加一條筆記說明你做了什么來搞定它,或許也會(huì)說明如何去測(cè)試,然后你寫上了版本管理系統(tǒng)中的版本號(hào)。
你將 Bug 的狀態(tài)切換為“驗(yàn)證”,然后一位測(cè)試接收并確定 Bug 是否真的被修復(fù)了。如果沒有被解決,它可能會(huì)重新被打開。如果修復(fù)衍生出了一個(gè)新的 Bug,一個(gè)新的 Bug 會(huì)被添加至 OmniBugZapper。
一旦處于“驗(yàn)證”狀態(tài)的 Bug 通過了測(cè)試,它會(huì)被標(biāo)記為“已解決”。
(前方高能:開發(fā)者與測(cè)試者之間的關(guān)系并非敵對(duì),雖然我已經(jīng)在幾家公司聽說過類似的事情。有些時(shí)候當(dāng)你覺得測(cè)試的工作就是折磨你,但這才該是常態(tài)。這意味著他們做了了不起的工作,我們都有著同樣的目標(biāo),那就是做一個(gè)棒棒的應(yīng)用出來。)
有一些 Bug 的修復(fù)需要工程師來驗(yàn)證,但這比較少見。大部分 Bug 是通過測(cè)試來驗(yàn)證的。(驗(yàn)證的工程師與修復(fù)的工程師不可以是同一個(gè)人。)
有些 Bug 是永遠(yuǎn)不會(huì)被修復(fù)或者驗(yàn)證的。它們包括討論型 Bug 和參考型 Bug:我們通過在的前者基礎(chǔ)上進(jìn)行討論來決定某個(gè)功能的改變或者增加,而后者可以用來對(duì)某個(gè)特性的行為或者出現(xiàn)做一些注解。
OmniBugZapper 有階段管理 (milestones) 的概念,自然,它有一個(gè)階段監(jiān)視器窗口,我們可以看到當(dāng)前階段的進(jìn)度和狀態(tài)。
每一個(gè)階段都劃分為分析、計(jì)劃和驗(yàn)證。Bug 會(huì)經(jīng)過分析變?yōu)橛?jì)劃,或者留至以后解決。
決定一個(gè) Bug 或者功能的開發(fā)階段與發(fā)布版本是合作式的,每一個(gè)想要參與的人都可以參加。通常,項(xiàng)目經(jīng)理會(huì)做大部分的決定。在比較大的問題上,通常會(huì)進(jìn)行更多的討論,我們一般會(huì)達(dá)成共識(shí),不過我們不會(huì)通過投票決定設(shè)計(jì)問題,我們的 CEO,Ken Case,有最終決定權(quán)。Ken 負(fù)責(zé)規(guī)劃未來。
譯者注:Milestones 可被翻譯為里程碑圖,是項(xiàng)目管理中的一種工具。為明確其功能,此處翻譯為階段管理,而每一個(gè) Milestone 則被翻譯為階段。
我們使用 SVN。所有的應(yīng)用與網(wǎng)站都在一個(gè)巨型的代碼倉庫中。我絲毫不驚訝于每個(gè)人的開發(fā)副本只是其中的一部分,而非全部。
你可能會(huì)覺得 SVN 是一個(gè)還未封印的史前工程師產(chǎn)物,并好奇人們關(guān)于切換工具的考慮。不過 SVN 干的還不錯(cuò),如何簡(jiǎn)化對(duì)一個(gè)大型倉庫的管理,總有些值得討論的事情。
我們有很多個(gè)腳本幫助我們做這些工作。比如,當(dāng)我想得到 OmniFocus 的最后一次更改,我輸入./Update OmniFocus
,然后它會(huì)更新我的開發(fā)副本 (通常我每天第一件事就是做這個(gè))。我的開發(fā)副本中沒有 OmniGraffle,只因?yàn)槲也恍枰P(guān)注它。但我也可以使用 ./Update OmniGraffle
。
SVN 創(chuàng)建分支可能不像 Git 與 Mercurial 那樣方便,但也沒有那么困難。每當(dāng)我們的應(yīng)用接近發(fā)布時(shí),我們會(huì)建立一個(gè)分支,用于隔離其它的變動(dòng)。人們可以出于各種目的隨時(shí)創(chuàng)建私人分支與目錄。
提交信息通過 E-mail 發(fā)送給工程師和其它關(guān)注項(xiàng)目的人。
由于應(yīng)用被置于大量自身也有 Bug 的系統(tǒng)框架上面,而應(yīng)用也運(yùn)行在實(shí)際的用戶的電腦上而非理想運(yùn)行環(huán)境中,所以無法保證一個(gè)應(yīng)用從來不會(huì)崩潰。
不過確保我們自己的代碼不會(huì)崩潰是我們的職責(zé),如果我們發(fā)現(xiàn)系統(tǒng)框架中有一個(gè)會(huì)導(dǎo)致崩潰的 Bug,我們需要找到方法繞過它,以讓代碼正常工作。
我們使用了一些圖形化的統(tǒng)計(jì)來展示一個(gè)應(yīng)用在崩潰之前的平均運(yùn)行時(shí)間。我們還有一個(gè)內(nèi)部應(yīng)用叫做 OmniCrashSorter,我們可以看到一些被標(biāo)記出來的崩潰日志,包括異常的追蹤調(diào)用棧,以及一些用戶崩潰場(chǎng)景的記錄。
關(guān)于崩潰有這么一個(gè)掃興的問題:崩潰永遠(yuǎn)不會(huì)在開發(fā)時(shí)出現(xiàn) (這似乎是一個(gè)軟件開發(fā)中的鐵律)。 這使得用戶的崩潰報(bào)告以及重現(xiàn)步驟非常重要。所以我們收集這些報(bào)告,并讓它們易于被查看。
有時(shí),我們會(huì)刻意使崩潰發(fā)生在異常中。因?yàn)槲覀兊膽?yīng)用使用自動(dòng)保存,崩潰會(huì)比可能出現(xiàn)的臟數(shù)據(jù)覆寫更為安全。
我們?cè)趦?nèi)部的 Wiki 上有一個(gè)簡(jiǎn)單的代碼風(fēng)格規(guī)范,而我因?yàn)樵缫咽炀毜膽?yīng)用,反而忘了它說的是什么。
除了一個(gè)事情之外,方法應(yīng)當(dāng)像這樣開頭:
- (void)someMethod;
{
或許很多人并不知道,在 Objective-C 中允許這樣使用分號(hào)。沒錯(cuò),是允許的。
這種方式比較好的地方在于,它可以使我們很容易的將類的聲明拷貝到頭文件或者類的擴(kuò)展當(dāng)中。而且,你可以選擇一整行,Command+E,然后查找包含它的頭文件 (或者從頭文件查找回你實(shí)現(xiàn)它的 .m 文件)。
我不喜歡這個(gè)方式。對(duì)我這樣一個(gè)極簡(jiǎn)主義強(qiáng)迫癥來說,分號(hào)太多余了,而所有多余的事情都應(yīng)該被去掉。(想象我的 X-ACTO 刻刀慢慢的繞著一個(gè)分號(hào)刻上一圈,伴隨著一陣東北風(fēng),把它刮到空氣中,離開我的桌子,然后散落進(jìn)垃圾桶里,真是說不出的暢快。)
不過這只是我沒事兒時(shí)的抱怨。我想說的重點(diǎn)在于,我們需要一個(gè)代碼規(guī)范,而且每個(gè)人都使用它。然后我們?cè)陂喿x彼此的代碼時(shí),就不會(huì)被引誘著去爭(zhēng)論關(guān)于分號(hào)的問題。我們不應(yīng)該只為了比拼彼此的口味,把時(shí)間浪費(fèi)在重新格式化已經(jīng)寫好的代碼上。
Omni 所有的應(yīng)用在某種程度上是同一個(gè)大型應(yīng)用,因?yàn)樗鼈円蕾囉诤芏鄠€(gè)共享的框架。這些框架一部分是開源的,你可以閱讀相關(guān)的信息,并在GitHub上獲取源碼。還有一些額外的內(nèi)部框架,一部分用在每一個(gè)應(yīng)用中,還有一些則只用在部分應(yīng)用里。
共享的框架使開發(fā)一些不同的應(yīng)用變得更容易,而且開發(fā)組的替換也變的方便,畢竟大部分框架都是相同的。
當(dāng)然,有一點(diǎn)不好的地方,是某個(gè)框架發(fā)生了變化可能一次性影響多個(gè)應(yīng)用。不過解決這個(gè)問題的唯一方法就是解決它,Bug 就是 Bug。
(當(dāng)項(xiàng)目快要發(fā)布時(shí),我們會(huì)建立一個(gè)新分支,用來防止在接下來的幾周中框架開發(fā)中產(chǎn)生的變化。)
新的代碼往往是基于 ARC 的。雖然有大量的舊代碼還未被轉(zhuǎn)換,但這也沒什么問題。畢竟隨著運(yùn)行情況的變化,被調(diào)校的代碼只應(yīng)該是那些需要被校正的。不過有些時(shí)候它們依舊應(yīng)該被轉(zhuǎn)換。(我已經(jīng)做過一部分關(guān)于轉(zhuǎn)換的工作,以后還會(huì)做更多。我覺得使用 ARC 寫出不會(huì)發(fā)生崩潰的代碼會(huì)更容易一些。)
雖然一批工程師已經(jīng)開始編寫 Swift 的代碼,Swift 目前尚未出現(xiàn)在我們的任何應(yīng)用和框架中。
不過這也說不準(zhǔn),或許在明天,或許在一兩年之后,又或許就在你閱讀本篇文章的當(dāng)下,改變就會(huì)發(fā)生。
OmniFocus 中有覆蓋模型類的單元測(cè)試;其他應(yīng)用也有差不太多的測(cè)試覆蓋率。我們與其他 OS X 和 iOS 開發(fā)者面對(duì)面臨的問題是相同的,每個(gè)應(yīng)用的 UI 元素都很多,而做自動(dòng)化的 UI 測(cè)試卻很困難。我們的 Mac 應(yīng)用的解決方案是基于 AppleScript 的測(cè)試。(這是確保應(yīng)用支持 AppleScript 的主要原因之一,而為了確保該支持的功能狀態(tài)正常,編寫測(cè)試是一種很好的辦法)
對(duì)于 Cocoa 的開發(fā)者來說,測(cè)試并不像在 Ruby,JavaScrpit 以及 Python 的開發(fā)中那么重要,這主要是因?yàn)榫幾g器和靜態(tài)分析可以捕獲到很多腳本語言捕獲不到的問題。
不過它們依舊很重要。
你可以在我們的代碼中看到我們正在使用的一些斷言:OBASSERT_NOT_REACHED, OBPRECONDITION, OBASSERT,等等。
我們使用這些斷言來表示一些推測(cè)和意圖。它們是為了我們自己的以及其它工程師開發(fā)的后續(xù)版本中而編寫的,我們大量的使用它們。
斷言太多的不好的地方在于你獲取到失敗的判定時(shí),你不得不找到原因。為什么代碼沒有按照期望運(yùn)行?是因?yàn)閿嘌允褂缅e(cuò)誤么,是不是斷言代碼需要被拓展或者修改?
我花了一段時(shí)間查看了許多的控制臺(tái)輸出記錄來確定這是不是個(gè)好方法,結(jié)論是,它是。
每一個(gè) App 都有一個(gè) workspace 文件,里面包括了 OS X 和 iOS 的項(xiàng)目,并嵌入了許多項(xiàng)目中使用的框架。
我們大量的使用 .xconfig 文件。你可以在我們的代碼中看到一大堆。
在 Omni 中,這是我之前沒有使用過的東西,甚至好幾個(gè)月都不曾看過,只知道它們可以正常被使用就行了。
OmniFocus 使用獨(dú)立的數(shù)據(jù)庫和一些偏好設(shè)置進(jìn)行調(diào)試版本的構(gòu)建,所以開發(fā)者不需要擔(dān)心真實(shí)數(shù)據(jù)被調(diào)試數(shù)據(jù)污染。
(我們其它的應(yīng)用是基于文檔的應(yīng)用,所以并不完全適合上述方法,不過 OmniFocus 之外的應(yīng)用也會(huì)使用獨(dú)立的 App ID 來構(gòu)建調(diào)試版本。)(譯者注:防止和線上版本沖突或者 iCloud Drive 污染)
靜態(tài)分析被設(shè)置為深度配置,包括調(diào)試版本的構(gòu)建。本應(yīng)如此。
我們有一個(gè)用于構(gòu)建的服務(wù)器,當(dāng)然,我們會(huì)在構(gòu)建失敗的時(shí)候得到提醒。OmniAutoBuild 是另一個(gè)內(nèi)部使用的 Mac App,我們可以在軟件中查看是什么導(dǎo)致了構(gòu)建的失敗。
構(gòu)建完整的、可發(fā)布的程序是由腳本完成的。我們會(huì)設(shè)置標(biāo)記將構(gòu)建好的版本放在演示服務(wù)器上,所以外部的試用版測(cè)試者可以下載最新的測(cè)試版本。
iOS 的測(cè)試版則使用 TestFlight。
真希望我可以說自己有一些秘密咒語,這樣我就可以把它們告訴你。
不過,實(shí)際上,在 Omni 管理大型項(xiàng)目與你所想像的方式?jīng)]什么差別。詳細(xì)的溝通與定義 - 交流到人,聊天,使用 OmniBugZapper,使用斷言,做代碼審查,遵守編碼規(guī)范 - 這些都很重要。
接下來的事情是自動(dòng)化:讓電腦做它們最擅長的事情。
不過,回到最初,有一些事情是在選擇 Bug 追蹤系統(tǒng)、 代碼控制管理系統(tǒng)或者其它什么事情之前的,那就是公司文化。與優(yōu)秀的人一起建立一個(gè)基于信任、尊重的環(huán)境,他們會(huì)做出更好的決定,并從壞決定中吸取教訓(xùn)。
好消息是這些事情都在有條不紊地進(jìn)行中。
還有午餐,工作與午餐。我們都在一起吃飯,這會(huì)產(chǎn)生一些區(qū)別。
P.S. Many thanks to the folks at Omni who read drafts of this article and provided feedback: Rachael Worthington, Curt Clifton, Jim Correia, Tim Ekl, Tim Wood, and Ken Case. Anything weird or wrong is my fault, not theirs.
另外,非常感謝 Omni 中閱讀過這篇文章草稿并提出反饋的人們,Rachael Worthington,Curt Clifton,Jim Correia,Tim Ekl,Tim Wood 和 Ken Case。如果有什么奇怪的錯(cuò)誤,一定是我犯了錯(cuò),不是他們。