項目延期半年,我被軟件外包坑慘了!

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"},{"type":"strong"}],"text":"本文最初發佈於Rajiv Prabhakar的個人博客,經原作者授權由InfoQ中文站翻譯並分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多年前,年輕且天真的我決定與他人一起創業,但同時還要兼顧我們的全職工作。我負責技術開發,另一個創始人負責業務。我們的"},{"type":"link","attrs":{"href":"https:\/\/zh.wikipedia.org\/wiki\/%E6%9C%80%E7%B0%A1%E5%8F%AF%E8%A1%8C%E7%94%A2%E5%93%81","title":"","type":null},"content":[{"type":"text","text":"MVP計劃"}]},{"type":"text","text":"是發佈iOS和Android App。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我在"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.cn\/article\/XUMp-7refE8T9Lwmdeld","title":"","type":null},"content":[{"type":"text","text":"後端"}]},{"type":"text","text":"上有開發經驗,但從未開發過App。爲此,我沒有選擇從頭開始學習,而是決定僱傭外部軟件開發人員來構建App,而我則負責所有服務器端開發、P\/SaaS集成和基礎設施。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"合作始末"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這不是我第一次創業。想起來,因爲有過這種經歷,所以我過度自信了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前的創業中,我曾經僱傭過一位年輕的兼職自由職業者,他來自另一個國家。他是熟人推薦的。他不僅很好完成了工作,而且每小時收費不到10美元。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當時,我並沒有意識到這一點,但考慮到他的才華和責任心,他的報價非常便宜。現在,他在舊金山掙六位數的工資。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有點遺憾的是,他現在無法參與進來。我的聯合創始人也想請一個更知名的組織來管理我們的前端開發。因此,我們決定尋找"},{"type":"link","attrs":{"href":"https:\/\/flexiple.com\/blog\/dev-shops\/","title":"","type":null},"content":[{"type":"text","text":"開發工作室"}]},{"type":"text","text":",而非自由職業者。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,先要做一番調查。我們看了獨立的第三方評審報告,詢問了評審依據,並與開發工作室之前的客戶進行了交談,最終從多個工作室中選擇了讓我們最有信心的一家。該工作室收費標準約爲每小時25美元——明顯高於同類的自由職業者。不過,我們認爲,僱傭的是一個有經驗的專業組織,而非隨便一個人,這對我們來說更合適。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我的聯合創始人是一名律師,與他們簽訂合同時,務求詳盡。衆所周知,軟件項目非常容易超支,所以我們協商簽訂了一份固定價格的合同,並對所有出現的bug都“保修”。花了很長一段時間後,我們才敲定合同細節,並在合同裏詳細描述他們應該構建的每個功能。然後,我們支付了第一筆款項並啓動項目。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,我們犯下了致命錯誤!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據合同協議,這個項目分爲三個部分。"},{"type":"text","marks":[{"type":"strong"}],"text":"在完成任何工作之前,我們就要預付40%的費用"},{"type":"text","text":"。然後每一部分開發完成時分別再付30%,"},{"type":"text","marks":[{"type":"strong"}],"text":"但是在我們收到剛完成部分的交付成果之前"},{"type":"text","text":"。因爲我們是個初創公司,並提前支付了5位數的費用,而且在完成下一筆付款之前沒有獲得任何可交付的內容,所以我們在整個項目期間都被鎖定了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然知道會發生這種事,但我們覺得沒問題。因爲他們有來自獨立第三方機構的良好評價,優秀的客戶口碑,而我們沒有發現什麼危險信號。此外,我們知道,在一個由別人創建的項目中增加一名新的開發人員並不不容易——所以我們計劃在整個項目中與他們並肩作戰。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事後看來,那是我們做出的最糟糕的技術決定,給了我們的初創公司一個沉重的打擊。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"技術挑戰"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照我們的想法,這款App需要具備的一個關鍵功能是實時聊天。在合同談判時,他們提出一些SaaS方面的建議來簡化實時聊天功能的構建——其中之一是Twilio Chat。在研究了他們提出的各種不同建議後,我們覺得Twilio似乎是最好的選擇,於是,我倆就同意將其應用於我們的聊天功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遺憾的是,在開始構建時,他們遇到難題。他們不知道如何在React Native中使用Twilio Chat,儘管是他們最先推薦使用Twilio Chat和"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.cn\/article\/8zSlQG9IyS5WyreZgRXR","title":"","type":null},"content":[{"type":"text","text":"React Native"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更糟糕的是,他們並沒有坦白地告訴我們,他們陷入了困境,而只是簡單地告訴我們“Twilio Chat不適用於React Native”。現在,他們想讓我們切換到一個完全不同的聊天服務提供商(由一個我們從未聽說過的公司提供),然後重新開始,而"},{"type":"text","marks":[{"type":"strong"}],"text":"我們需要爲此支付額外的費用"},{"type":"text","text":"(即使這本是一個固定價格的項目)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最糟糕的是,他們從開始說的話就不是真的。Twilio Chat用在React Native中完全沒有問題——他們只是不知道怎麼做。最終,作爲一名沒有任何React Native開發經驗的開發者,我花了很多時間去研究解決方案,並教他們應該怎麼做。即使在我向他們做了演示之後,他們仍然需要我給他們提供文檔鏈接,並向他們解釋如何使用Twilio API。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我沒有和他們在一起,或者沒有替他們想出辦法完成這項工作,那麼我們可能就會採納他們的建議。我們可能會完全拋棄Twilio,轉向一個完全不同的、低標準的服務。這個決定可能會讓項目推遲好幾個月,並多花一大筆錢。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"在安全上馬馬虎虎"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我希望關於Twilio的問題就此結束,但這還沒完。所有Twilio聊天信息都屬於一個通道,而通道可以標記爲“私有”或“公共”。顧名思義,私有通道屬於通道中的特定用戶,而公共通道可以“"},{"type":"link","attrs":{"href":"https:\/\/support.twilio.com\/hc\/en-us\/articles\/115005970527-Public-vs-Private-Channel-Use-Cases-with-Twilio-Programmable-Chat","title":"","type":null},"content":[{"type":"text","text":"被非會員看到和加入。此外,公共通道及其成員和消息對於給定服務中的每個客戶端端點都是可見的。"}]},{"type":"text","text":"”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯而易見,所有的非公開消息都應該使用私有通道來實現。但令人驚訝的是,他們都是用的公共通道——這是我在瀏覽Twilio控制檯時看到的。如果我們已經上線了他們的實現,只要是有一點點開發經驗的人,就能夠竊聽每一個App用戶的私人談話。如果我自己沒有發現這個問題,開發公司肯定不會安排任何滲透測試人員來發現這些安全問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的錯誤令人無法容忍。更令人震驚的是,他們非但沒有爲自己的嚴重疏忽而道歉,還不願意更改。顯然,使用公共通道實現聊天功能更簡單,因此,他們更願意保持這種方式。只有在我們多次抱怨後,他們才最終同意改變實現方式。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Bug無處不在"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們之所以願意僱傭開發工作室,而不是個人自由職業者,是因爲他們承諾給我們的其他支持。特別是QA團隊,他們會在向我們展示應用前進行詳盡的測試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何軟件項目都會遇到Bug,這是不可避免的,所以我們理解他們不能做出任何承諾。但我們相信了他們的話,他們說我們應該只會發現一些極端情況下的Bug。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後來我們發現,這完全是一派胡言。我們從他們那裏收到的所有交付滿是Bug。甚至最基本的功能都不能工作——我甚至懷疑,即使他們測試過,他們也不是用真正的手機測試的。"},{"type":"text","marks":[{"type":"strong"}],"text":"在整整一週的時間裏,我和我的聯合創始人每天都要花上幾個小時"},{"type":"text","text":",煞費苦心地測試,並記錄所有出現的Bug。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"程序只求可運行"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉例來說,我們發現的一個Bug是,如果用戶的聯繫人超過50個,就只有前50個會在App中顯示,其他的都無法訪問。事實是,我們的一個SaaS集成被分頁了,開發人員只實現了獲取第一頁結果的代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲這個Bug只有在一個用戶有51個聯繫人時纔會被觸發,而且我們尚處於私人測試階段,所以我們過了一段時間才發現這個Bug。之後,我們向他們做了反饋,問題很快就得到了修復。我們測試了他們的修復結果,似乎一切正常。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但在審查他們的代碼變更時,我發現,他們的修復方式是多麼的旁門左道。他們沒有用一個while循環來獲取所有的結果頁,而只是簡單地添加了一個if條件來獲取第二頁的內容。"},{"type":"text","marks":[{"type":"strong"}],"text":"一旦用戶的聯繫人數量超過100,我們就會再次遇到完全相同的錯誤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我可以原諒第一個Bug,把它看成是無意的。但第二個Bug就是故意失職了。他們一定知道,我們要過很長時間才能觸發第二頁查詢結果,要過更長的時間才能觸發第三頁。他們清楚地知道自己在做什麼,知道“修復”的侷限性,但他們還是那樣做了。如果沒有人仔細檢查他們的代碼,這個Bug就會進入生產環境。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"沒有版本歷史"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲一名開發人員,我親身體會到版本控制歷史是多麼有用。它可以幫助未來的開發人員瞭解爲什麼要做出某些設計決策,特定的功能是如何構建的,以及如何構建其他類似的特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出於這個原因,在合同談判中,我特別堅持最後的交付物應該是一個Git存儲庫。他們欣然同意,並說他們內部也普遍使用Git。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遺憾的是,在交付源代碼的時候,他們只給我們發送了一個壓縮文件,其中包含所有源代碼和生成的文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我提醒他們,根據合同,他們應該給我們一個Git存儲庫。事實上,在他們發送的壓縮文件中,我甚至看到了一個“.git”目錄——表明他們在開發時確實在用Git。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二天,他們很快就給我們發送了一個Git存儲庫,"},{"type":"text","marks":[{"type":"strong"}],"text":"其中只有一次提交,而裏面的文件與前一天發送給我們的zip文件完全相同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我抑制着自己的挫敗感告訴他們,我們想要整個版本歷史,而不只是一次提交,而且還是提交的同一個zip文件。他們回答說,他們的Git存儲庫中有一些“敏感信息”,不方便向外人提供。因此,他們不能分享給我們。“合同只規定交付Git存儲庫。但並沒有說存儲庫中應該包含所有的開發提交和歷史“。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"隨意改變規則"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在談判過程中,我們多次提到服務器端API還沒有完全實現,我們希望後端開發和前端開發同時進行。在項目開始時,我會把所有API端點提供給他們,其中一些會完全實現。這樣,他們就可以使用這幾個端點立即開始開發比較簡單的特性。當他們完成這些功能時,用於下一批特性的API也就完成了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的目標是避免延期,同時開展這兩項工作,可以更快地推出我們的App。這是我們預先明確並反覆申明的內容。我們總是被告知,沒有問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遺憾的是,付完錢之後,我們開始聽到一些完全不同的聲音。"},{"type":"text","marks":[{"type":"strong"}],"text":"他們直截了當地拒絕開始任何工作,直到整個項目中每一個特性用到的後端都100%完成開發並最終確定。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所幸,我們在合同談判和設計工作上花費了大量時間,我幾乎已經完成了後端開發。所以這並沒有成爲一個問題。但令人震驚和痛心的是,他們沒有履行銷售人員早些時候做出的承諾。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"唯我獨尊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們把他們當作潛在客戶來交談的時候,他們爲我們鋪開了紅地毯。但一談到實質問題,他們就堅持要按他們的方式來做。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,在研究了各種選項之後,我決定用"},{"type":"link","attrs":{"href":"https:\/\/swagger.io\/resources\/open-api\/","title":"","type":null},"content":[{"type":"text","text":"Swagger"}]},{"type":"text","text":"來記錄所有API端點、它們的輸入、模式、描述和行爲。這樣,文檔就嵌入到了代碼中,能夠自動生成,並保持更新。"},{"type":"link","attrs":{"href":"https:\/\/petstore.swagger.io\/#\/","title":"","type":null},"content":[{"type":"text","text":"Swagger GUI"}]},{"type":"text","text":"還提供了一種非常友好的方式讓我們可以瀏覽所有API文檔,甚至可以直接從GUI進行API調用來做測試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遺憾的是,這不是他們的做事方式。因此,他們拒絕使用Swagger作爲文檔源。取而代之,他們堅持讓我們用電子郵件給他們發送一個Word文檔,包含所有在Swagger中能找到的內容,但要按照他們指定的格式填寫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們花了好幾天討論這個問題,最後他們讓步了。但在整個開發過程中,他們的態度一直沒有改變。我們僱傭他們,是爲了讓他們使用我們的後端API來創建移動應用。但他們卻對API的實現方式提出要求。每當在API設計上出現意見分歧時,我們就不得不花好幾天討論,還要忍受他們的抱怨。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種爭論可能是源於他們對API最佳實踐的熱情,但我懷疑,他們主要是想讓自己的工作儘可能簡單。而且,他們經常弄不清楚如何利用現有的API實現所需的功能。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"缺少直接溝通"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"項目開始後還有一個很大的意外,就是缺乏溝通。在我以前所有的工程項目中,在跨團隊合作時,爲了更好地瞭解和解決出現的問題,我們都會直接與工程師交談。令我驚訝的是,這是他們明確禁止的事情。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照他們的規定,我們只能與一個非技術的項目經理單點聯繫。儘管我們提了要求,但他們拒絕讓我們與實際從事項目開發工作的開發人員聯繫。此外,他們的項目經理也拒絕通過實時聊天工具交流。他們堅持一切都通過電子郵件進行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着時間的推移,這帶來了很大的溝通問題。每當開發人員遇到問題,或者有什麼想不明白,他們就會把問題發給項目經理。然後,她會把所有的問題彙總起來,在一天工作結束時給我發一封大郵件。即使我很快回復了郵件,他們還是要到下一個工作日才能看到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,"},{"type":"text","marks":[{"type":"strong"}],"text":"即使是一個簡單的問答也需要24個小時的時間"},{"type":"text","text":",比較複雜的討論則需要好幾天,而不是聊30分鐘,然後問題就解決了。值得慶幸的是,在項目後期,他們終於意識到這個過程是多麼低效,並允許我們與他們的開發人員直接聯繫。遺憾的是,到那時候,一切都太晚了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在項目剛開始時,我們就知道這會成爲一個大問題。但他們向我們保證,這不成問題。可以肯定的是,我們的擔憂變成了現實。事實證明,當你每天只能通過一封電子郵件進行溝通時,很難做到敏捷。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"嚴重延期"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很遺憾,上述所有問題體現到了項目時間表上。原本應該是一個爲期2個月的項目,最後卻用了7個月。對我們來說,這是一個重大挫折,因爲我們錯過了許多潛在的用戶,他們決定不再等我們的App發佈。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在回想起來,這些延誤一點也不奇怪,因爲他們缺少技術專家,堅持採用瀑布式方法,並拒絕通過聊天或電話直接溝通。但我懷疑,這還不是問題的全部。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我懷疑,在不同時段,他們有其他覺得更有利可圖的項目,並因此降低了我們項目的開發優先級。這也是其開發團隊在項目中途出現重大人事變動的原因。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"推卸責任"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在他們所有的失敗中,要說有什麼東西不變的話,那就是他們完全拒絕爲任何事情負責。在執行任何任務之前,他們都會對自己的能力表現出百分之百的信心,並承諾結果不會有任何差錯。而當他們沒能兌現自己的承諾時,總是把責任推給其他人。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你們搞不清楚如何使用twilio SDK?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"在React Native中無法使用Twilio聊天軟件"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(事實是可以)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你們的聊天實現會暴露所有的私人對話?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"替代方案太複雜了"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(這是我們僱傭你們的原因)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼某個屏幕要花30秒來加載?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"我們必須進行5次API調用,這使它變慢了。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(這5個API調用加起來不到1秒就完成了)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"項目比承諾的時間長3倍?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"服務器端 API太差,Bug太多。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(我確信,這與你們設定的項目優先級、能力和溝通方法不無關係)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"令人沮喪的是,這種方法很有效!如果我自己不是一個開發人員,我就會相信他們告訴我的一切。他們阻止我們與開發人員直接溝通是有原因的——他們的項目經理是討好人、傳遞信心以及轉嫁責任的專家。願上帝幫助那些僱傭了他們但卻沒有技術能力與他們辯論的可憐人。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"經驗教訓"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"面對上面出現的所有問題,我很想說:\"離岸開發者很糟糕。\"但是,這樣的結論既狹隘也過於侷限。我與來自其他國家的優秀工程師合作過,認爲優秀的開發人員只存在於美國,是非常愚蠢的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我也很想說,永遠不要把開發工作外包。如果你的公司像谷歌一樣成熟,或者是由風險投資公司資助的初創公司,那麼一切都要自己構建,並且使用工資六位數的開發人員。但是,對於一個創始團隊規模不大的自給自足的初創公司來說,使用一些便宜的僱傭兵來幫助你完成MVP是有意義的。這是一個可以成功應用於其他場合的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們不禁會想,既然看到了上面出現的所有問題,那麼應該可以通過談判達成具體的合同條款來預防。這種做法註定要失敗。有太多的未知因素和太多的主觀性,不可能把所有東西都囊括在一個法律文件中。更不用說通過訴訟依法執行合同,這本身就是一個巨大的工程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"歸根結底,當你僱用自由職業者或開發工作室時,重要事的只有一件,那就是:如果他們做得不好,你有能力離開他們。我們遇到的所有問題,都是因爲我們缺少制衡手段。因爲我們預付了很多錢,所以我們沒有能力離開並僱傭其他人,即使事情變得非常糟糕。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一種更好的方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"合同一結束,我們就與他們斷了聯繫,並大大地鬆了一口氣。我真得感到卸下了一個大包袱。從此之後,我們從根本上改變了與外部開發人員的合作方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對我們想要構建的功能,擬定一個順序列表。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"找幾名開發人員,最好是獨立的自由職業者,但如果同意以下流程,開發工作室也沒問題。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於每名開發人員,挑選列表中最重要的功能,與他們討論功能需求、預算和成本。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓他們實現那個特性並測試。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓一名內部人員審覈他們的PR,測試升級後的App,並標出有問題的地方。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"符合要求後,合併並部署該特性,這樣,所有創始人\/用戶就可以繼續審覈該App,並提供反饋或者根據需要調整。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們對他們所做的工作感到滿意,就挑選下一個我們希望他們實現的功能,然後再次重複這個過程。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們對他們的工作不滿意,就解僱他們,並尋找替代者。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們採用了上面的漸進式方法,擺脫了巨大的預付合同,開發過程就變得非常順利,非常令人愉快。一切都好極了。開發人員與我們相處得更加愉快,也表現出了更大的靈活性,與我們的溝通也更坦誠,並在更短的時間內交付了更好的工作成果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最重要的是,我們不會長期綁定到一個開發工作室,這使我們擺脫了風險,讓我們感到無比安心。如果對事情的發展方式不滿意,我們就可以在一週後離開。這在一些情況下拯救了我們,並減輕了我們的負擔。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"反過來,我相信開發人員也會喜歡這種靈活性。我們持續合作的內容是雙方每週協商一致的事情,他們不會覺得是迫於先前的合同在做事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你避免了我們的錯誤並僱傭了合適的開發團隊,那麼“大瀑布項目”是否有可能獲得成功?當然有可能,但是,你真有信心自己不會遇到同樣的問題?這一系列的問題讓我對敏捷有了新的認識,也理解了敏捷出現的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客戶合作勝於合同談判"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"個體和互動勝於流程"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可運行的軟件勝於詳細的文檔"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"響應變化勝於遵循計劃"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實證明,許多開發工作室都拒絕採用這種工作方式,而是堅持使用瀑布法,並簽訂大額的預付合同。但在今後所有的工作中,我會堅持我現在學到的東西。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/software.rajivprab.com\/2021\/04\/26\/experiences-working-with-an-outsourced-dev-shop\/"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章