OO課程目前已經進行了三次的作業,容我在本文中做一點微小的工作。
第一次作業
第一次作業由于難度不大,是以筆者程式實際上寫的也比較随意一些。(點選就送指導書~)
類圖
程式的大緻結構如下:

代碼分析
可以看出,整體的功能還是相對零散的,耦合狀況也基本還可以。然而類似
Main.main
、
Polynomial.Polynomial
兩個函數的複雜度仍有點高。筆者後來查閱了阿裡Java開發規範手冊,發現兩個問題:
- 單個方法的長度不宜過長,入口點方法(
)也是一樣Main.main
- 不宜在構造函數中攜帶過多的計算邏輯。手冊第七頁,第11條中也有明确的規定:
11. 【強制】構造方法裡面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。
筆者的程式這一點就有待修改,同時,類似
public Polynomial(String str) {
// something inside
};
這樣的構造函數,更适合寫在類似
public static Polynomial parsePolynomial(String str) {
// something inside
}
的靜态方法中。
公測
我方
公測不出意外,筆者的程式在公測階段沒有出現任何錯誤。
對方
對方的程式在公測被測出了一個bug,是棧溢出,溢出的位置是正規表達式。
對方試圖用一個規模龐大的正規表達式來先行判斷整個表達式是否合法,于是在遇到規模較大的資料時出現了棧溢出的情況。
互測
不出意外,筆者的程式未被測出bug。
對方的程式被測出了一個bug,對方仍在對象内使用傳統的
int
數組,而且空間開的非常小,最終導緻數組越界,程式出錯(然而事實上對方在頂層進行了Exception的catch,故沒有出現crash)。
反思
第一次由于任務算法難度很低,且工程量也不很大(小幾百行的量,甚至據說部分大佬壓縮到了100行内),是以筆者的很多寫法較為随意,大約整個工程用時一小時不到。是以在工程性方面沒做過多的考量,架構也沒有進行很充分的優化。
第一次據筆者觀察,很多同學還對java這門語言完全處于不熟悉的狀态,對于正規表達式等概念及其具體原理也完全不了解,更不用說面向對象的設計思想了。據筆者所知,像這樣試圖用一個龐大的正規表達式判斷格式的同學并不在少數。然而了解正規表達式相關原理的同學都應該清楚,正規表達式不是讓你這麼用的。正規表達式更多的用于相對簡單且沒有複雜的重複和嵌套的一些的模式比對,以及其内部關鍵位置資訊的提取。(更多資訊和算法原理可以翻閱Wikipedia - Regular Expression,大緻原理為有限狀态自動機,本文不再贅述。)
此外,在C#、Java這類的語言中,直接使用傳統數組在大部分情況下并不是明智的選擇,Java中沒有像C++那樣提供很完善的面向使用者的指針機制,為的是一定程度上的系統安全性。而傳統數組存在靈活性極差的問題。簡單來說,數組開小了,會crash;數組開大了,空間的浪費很嚴重(甚至有部分大佬還在适用oi/acm味滿滿的長度高達100050超大數組[滑稽])。是以,更推薦各位優先使用Java封裝好的資料結構,例如序列類的
Vector
ArrayList
(實際上後者效率更高,推薦
ArrayList
),例如Key-Value類的
HashMap
(筆者在後來還自行繼承封裝了一款支援預設值的
HashDefaultMap
),而不是去自己實作一個所謂的資料結構。或者也可以這樣說,你能想到能做到的算法封裝,人家Java作者早就想到了。IT界從來不缺勤奮的輪子工,需要的是聰明的懶人。
第二次作業
第二次作業,是實作一個傻瓜電梯。問題一如既往的簡單,但是真正一去寫,诶?細節這麼多?恩,真正的工程終于來了。(點選就送指導書~)
第二次作業,筆者的程式結構大緻如下(圖檔規模略大):
筆者将所有的輸入請求抽象為了
Request
類,再将操作和RUN指令分别繼承為了
OperationRequest
和
RunRequest
,
OperationRequest
類再繼承為内部請求類(
InnerRequest
)和外部請求類(
OuterRequest
)。
可以看到,這一次的耦合狀況較上一次有好轉(沒有出現紅色字)。然而依然存在部分方法複雜度略高的情況(比如入口點函數,依然是紅字狀态)。看來,功能還需要進一步拆散。
第二次作業中,筆者的程式公測出現了一個bug。後來經檢查,是因為沒有仔細研讀guide book上的一個功能需求而導緻的功能缺失造成的(沒有支援數字前的
+
對方在公測中沒有出現任何bug。
我方一開始有一處被對方認定為imcomplete,這段代碼如下:
package configs;
/**
* 全局設定類
*/
public abstract class ApplicationConfig {
/**
* 層數限制
*/
public static int default_max_floor = 10;
public static int default_min_floor = 1;
public static int default_present_floor = 1;
/**
* 時間戳限制
*/
public static long default_max_timestamp = 0xffffffffL;
public static long default_min_timestamp = 0L;
/**
* 預設電梯數量
*/
public static int default_lift_count = 1;
/**
* 最大可接受的合法請求數
*/
public static int max_valid_requests = 10000;
}
對方給出的理由是,guide book上面規定不準使用public。然而,根據筆者之前長時間大量的工程經驗來看,這樣的寫法還是很常見的(
public static
來設定常數的形式)。
況且,筆者在guide book中找到的原文如下:
必須要實作電梯、樓層、請求隊列、排程器、請求這五個類,且類中不允許出現 Public 屬性。
說的是public屬性是不被允許的,那麼
public static
究竟算不算在内呢?筆者先後問了多個助教,助教們之間意見也不是很統一(總體還是認為可以使用
public static
的更多)。
後來筆者基于這個問題與吳際老師進行了交流,老師的意見可以歸結為如下幾點:
- 這種寫法不是不可以,但是
的值仍有被非法篡改的可能。public static
- 比如,使用
來将設定類的值限定為常量可以有效地防止修改。public final
- 比如,使用getter方法來進行靜态的通路也可以從根本上杜絕非法篡改。
筆者後來再次翻閱了代碼規範手冊,發現一般常量類的東西需要使用
static final
關鍵字,同時命名方式一般為全部大寫并按照單詞來下劃線分隔(例如:
YES
UNKNOWN_REASON
等)(詳見手冊第3頁,常量定義)。
以及,最終看了下助教仲裁的結果,結果是不算bug。
很不巧,這次沒有發現對方的bug。不過可以看的出來,是一個邏輯思維清晰,但是代碼規範和工程思維比較欠缺的同學。
總結
第二次作業某種意義上算得上是個真真正正的工程了,筆者這次雖然還是有點小遺憾,但總算是找到了久違的OOP工程的手感。
筆者的程式部分地方還是存在代碼品質問題,還是有進一步改進的空間的。
此外,因為guide book讀漏了而導緻的公測bug這個實在是不應該的,以後必須要注意仔細研究需求。
第三次作業
第三次作業是第二次作業的更新版,采用了相對智能的電梯排程措施,然後需求細節一樣較為繁瑣。(點選就送指導書~)
筆者的程式結構如下(圖檔規模還是較大):
筆者在第二次
LiftController
類外部套了一個
Scheduler
進行更加智能的排程。
此外,筆者為了友善在調試時看清楚整個程式内部的計算邏輯細節,設定了Debug資訊輸出接口。(詳情見【技巧】Java工程中的Debug資訊分級輸出接口)
代碼品質
可以看出,還是老毛病,有些核心方法的規模過大。
筆者的程式在公測環節沒有出現任何錯誤。
對方的程式在公測環節出現了一處錯誤,出錯位置在
邊界情況測試
-->
副請求時間==主請求開門時間,不捎帶
。
筆者在公測環節找出了對方程式的兩個bug:
- 對方的程式在一個地方存在queue對象非法通路的問題(準确的說,是越界通路)
- 對方的程式在處理一次性處理多個請求的時候,并未嚴格按照輸入順序進行輸出(筆者判斷的沒錯的話,對方應該是将主請求進行了單獨處理,進而導緻了這樣的錯誤)
不出意料,對方未發現筆者程式的bug。
第三次作業沒有再犯第二次作業的低級錯誤,也沒有被挑出bug。
然而實際上,第三次作業仍然有着一些的缺陷:
- 和第二次作業一樣功能不夠分散
- 由于需求分析花了非常多的時間,導緻這次作業起步時間很晚,很多架構實際上并不是很好的設計(筆者寫程式的時候自己就已經在這麼覺得,然而時間緊迫還是選擇了優先完成任務)
等到下次作業,筆者會對一部分的架構進行大改,争取用更優的架構來面對接下來的project。
此外,還是必須吐槽一句,guide book中很多該明确的需求并沒有明确到位。也許現實中的開發真的沒那麼特别明确細緻的需求,然而我們的公測環節卻有着無比明确的細節需求(而實際項目中的此類細節需求很大一部分是開發者自己定義的,其餘的是甲乙雙方協商确定的),這兩者之間,顯然存在着不可調和的沖突。希望OO課程組就這一問題進行制度上的改善。
以上三次作業,讓筆者感到自己還有一些不足:
- 部分方法依然複雜度偏高(有的已經接近100行),應該根據其功能子產品進一步拆分
- 對于需求很多時候還是沒有弄得特别清楚,導緻第二次公測挂掉了一個點(實際開發中,需求還是非常重要的)
經過了研讀阿裡Java代碼規範手冊後,筆者意識到自己在OOP方面仍然有不少需要進一步規範和改進的地方。筆者之後會盡力做到:
- 充分明确需求
- 動手寫代碼之前仔細研究架構的合理性和擴充性
- 嚴格遵循代碼規範
其他
筆者在釋出此文章之前,聽過不少來自身邊同學的各類吐槽,也看了一些其他同學已經釋出的文章,感覺不少同學對有些事情的認識還是存在着一些誤區,筆者準備在此結合自己在工程開發方面的經驗和踩過的坑,和大家聊一聊。
代碼越短就越好?
之前看到一些同學的作業,不少作業裡面都在說自己的程式寫的還不夠好,下次争取精簡的更短。
其實,這是個很錯誤的認識。代碼短等于代碼品質高嗎?當然不是!
舉個很經典的例子,讓我們來看幾段程式:
/* 程式一 */
public static void main(String[] args) {
int i = 2;
System.out.println(i++ + ++i);
}
/* 程式二 */
public static void main(String[] args) {
int i = 2;
int j = i++;
int k = ++i;
System.out.println(j + k);
}
/* 程式三 */
public static void main(String[] args) {
int i = 2;
int j = i;
i += 1;
i += 1;
int k = i;
System.out.println(j + k);
}
好了,現在請告訴我,這三個程式的運作結果都是什麼。
沒錯,輸出的結果都是
6
,這三個程式是等價的。那麼請告訴我,哪個程式你最先看懂,哪個程式你最先計算出了結果?
我想大部分人的順序都是:
程式三
->
程式二
程式一
。然而程式從短到長的順序是什麼呢?完全和這個相反。
讓我們回到這個問題的定義上來——究竟什麼樣的代碼叫做高品質代碼?我們來思考幾件事:
- 代碼是誰寫的?人寫的。
- 代碼是幹什麼的?實作功能的。
- 實作功能是幹什麼的?創造價值的。
那麼我們再來思考一個問題:從結構化程式設計,到面向對象程式設計,甚至到後來的函數式程式設計,這些東西存在的意義是什麼?
毫無疑問,最終目的肯定是創造更多的價值。而決定能否創造價值、創造多少價值最核心最根本的因素,是寫代碼的人。
這下很明顯了,這一切的發展,圍繞的都是開發程式的人。說的更直接一些,能有助于提高開發者的開發速度,提高開發者對程式的維護和擴充能力,提高團隊合作效率的代碼,就是高品質代碼。
這麼看來,像剛才那樣第一個程式,的确很短,也不得不說能駕馭的了這種程式,開發者也肯定挺厲害。但是這種東西真的能讓自己以外的人(甚至自己過了一段時間後)快速的了解麼?顯然不能。是以類似這樣的,短小精悍但是實際上不利于整體開發效率和品質的代碼,也是很糟糕的代碼。
當然,說了這些,并不是說短小的代碼有錯。而是,不應該為了短小而短小,而應該是為了讓程式更加清晰可讀而将程式進行适度的精簡。
System.out調試很低級?
看到過一些同學(作業裡的和身邊的都有)之前在抱怨,自己隻會輸出調試如何如何如何。。。。
然而我還是和上一節一樣,一句話:這一切,圍繞的都是開發程式的人。說的更直接一些,能幫你更快定位和找出bug(包括程式bug和邏輯bug)的辦法,就是好辦法。
筆者在第三次作業中,從ubuntu系統
ssh
的DEBUG模式獲得了一些靈感,自己開發了一個可用于快速debug的分級debug資訊輸出控制子產品。(更多資訊詳見【技巧】Java工程中的Debug資訊分級輸出接口)
實踐表明,筆者使用此輸出調試時,一樣可以很快的定位錯誤,甚至還具備了一般的debugger較為欠缺的邏輯bug發現能力(實際上debugger使用者一般容易更多的傾向于局部的程式bug,而具有連貫前後文的輸出調試則可以很立體的将邏輯bug展現在你面前)。
以及,再次聲明,說這些,不是在反對使用debugger。而是想告訴各位,适合自己的,就是最好的。
代碼規範不重要?
之前,在QQ群上讨論了一下代碼風格的問題,然後馬上就有一位ACM大佬,出來對這件事情嗤之以鼻。
這位大佬的邏輯大概是——代碼風格好就沒bug了?代碼亂七八糟就有bug了?
好,不說别的。首先,問問各位,出自你們自己之手的電梯程式,如果全篇沒有一個注釋,代碼寫的亂七八糟,到處都是奇奇怪怪的英文縮寫甚至中英文混搭,大小寫也在亂用。
請問,一個月後你們自己拿來再看看,你還能記得多少,随便找個方法你能講得出來參數都什麼意義傳回的是啥不?事實證明,基本沒人能做到,甚至可以說大部分代碼都不記得了(是的,不要對人類的那點記性太過樂觀)。
然而實際上,獨立的開發是很少見的,哪裡有項目開發,哪裡就有teamwork。你拿着一個自己都看不明白甚至都不願意看的程式,來和别人合作完成項目,指望别人比你自己還了解你的程式?然後别人根本用不了,你得改來改去。後來,别人終于調用上去了,然後由于各種奇奇怪怪的問題導緻不相容(不可能?自己去看看阿裡Java手冊,這種事情多得是)。這開發效率可能有多高呢?在這個時間就是金錢就是機遇的時代,把大量時間浪費在這種本可以避免的地方,無異于自掘墳墓。
然後好不容易終于跑起來了,下次再維護,發現之前的代碼又帶來了各種坑,又得繼續從自己挖的坑裡頭爬出來繼續挖坑再跳進去。。。。毫無疑問,這分明是一個惡性循環。
試想,如果你是個人開發者,你的代碼嚴格遵循一定的規範,并按照規範寫了注釋。那麼無論過了多久,你都能很快的看明白自己的程式并進行有效的維護。
如果你是開發團隊,嚴格遵循了代碼規範,那麼在不同人代碼對接的時候,大家遵循的都是同一個标準,可以減少很多不必要的溝通麻煩和相容性問題,極大的提高開發效率。
而對于純算法競賽選手,他們寫程式的模式永遠就是
寫代碼
調試
AC
AC不掉繼續調試
AC
寫題解
扔掉
這麼幾個步驟。而比賽場上(ACM為例)一共就五個小時(而且一般還都是各寫各的,所謂的teamwork也僅僅是交流思路),平時刷題一個題撐死了也沒幾天(甚至很多題就是幾小時幾十分鐘的事),完全沒有任何長期維護和團隊開發的需求在内,他們當然會選擇怎麼用的爽怎麼來。
忽略代碼規範重要性的,隻能說他們根本沒體驗過被自己挖的坑坑無數遍的痛苦,更不懂效率對于一個項目(尤其是初創項目)的重要性。
此外,對于有些常數優化控,且不說在現代的編譯環境下你們那一套還能不能奏效,就算都奏效,請自己上騰訊雲看看雲伺服器一個月多少錢,再看看一個科班出身的程式猿一個月工資多少錢,誰輕誰重自己掂量掂量吧。。。