和不少人同樣,此次編程是痛苦的……是崩潰的……
這篇博文中1. 2. 3.和咱們此前提交的結對編程項目總結十分類似,不想看的話從4.開始看起就能夠了。html
1. 初始設計node
2. 遇到的問題c++
3. 最終實現git
4. 我的感悟github
5. 總結編程
## 1. 初始設計:一開始,咱們錯誤地理解了題意,認爲要實現兩個獨立的功能:對用戶提供的表達式進行求值,和按照設置隨機生成表達式。json
對於表達式求值模塊,咱們打算用最傳統的方法,用運算符棧和運算數棧兩個棧進行計算。這部分由個人隊友實現。api
對於生成表達式模塊,考慮到題目要求較高的靈活性,咱們打算使用二叉樹表示算式,經過遞歸實現算式的生成、求值及翻譯成字符串。
在這個部分,咱們最初打算劃分紅許多個模塊。從上而下,首先是Generator模塊,實現與用戶的交互、二叉樹的生成;而後是Setting類,考慮到各類設置信息不少,用一個類負責保存各類設置信息和解析xml;其次是Node,即二叉樹結點;而後是Num類,用來將三種類型,即整數、小數、分數,封裝在一個對象裏,從而上層能夠直接調用;最後實現分數類Fraction,封裝了各類運算。
這部分由我實現。數組
咱們遇到的第一個問題在於總體結構太過於繁瑣。Generator, Setting, Node, Num, Fraction,五個模塊、四層封裝,太過於複雜;何況Num這一層次只是判斷一下操做數類型,彷佛比較雞肋。通過討論以後,咱們去掉了Num這一層次,直接在Node中保存操做數信息。架構
咱們遇到的第二個問題在於,忽然從助教那裏得知整數除法應該整除。咱們本來考慮丟棄不能整除的式子從新生成,後來發現這樣速度太慢。
最後,咱們決定放棄絕對的隨機性,而選擇配湊。咱們考慮了好幾種配湊方式,最後決定對於整數的除法,經過給定一個整數值、配湊生成表達式的方式,生成被除數與除數的算式。
事實上,咱們也考慮過總體更改成配湊方式,但配湊方式邏輯較複雜,並且難以處理乘方,因此咱們只打算對整數除法進行配湊。不過到了後來,咱們驚喜地發現乘方的冪次也能夠配湊。
咱們遇到的第三個問題在於對於dll的使用很是不熟悉。咱們爲此研究了好久,才終於攻克這一關。
首先,咱們對於dll的概念很是不瞭解,爲了瞭解這些基礎概念就花了好久。
而後,很是尷尬地,咱們本來打算導出Generator類,可是用dll導出類很是複雜。有兩種可選的方案,一是將Generator及其屬性的類所有導出,這樣一來會暴露本身的內部實現,違反了封裝原理;另一個是導出抽象類,可是這個概念咱們兩人都不太熟悉,並且這樣會增長一層調用,加大代碼複雜度。
在咱們和ui進行了進一步溝通以後,咱們瞭解到ui其實只須要函數;再加上咱們編碼中也出現了不知道如何讓Setting對象成爲全局對象這一問題,咱們決定直接使用全局變量與c函數接口。
最後,咱們生成dll後不知如何運行。查找了不少資料以後,咱們才學會經過另外一個項目,即testDll項目,調用本身生成的dll。
咱們遇到的第四個問題在於,咱們發現用std::string做爲接口有必定問題。經過查找資料,咱們查到不適宜用stl做爲dll接口,而最好用純c。所以咱們提供了一個char數組的接口,本想刪除std::string接口。不過考慮到彷佛不少ui都使用c++,這兩個接口咱們都提供了。
另外,咱們還陸陸續續解決了一系列除零、模零、隨機數範圍有問題之類的bug,並知足了某組ui的提供文件接口的需求。
## 3. 最終實現:Fraction模塊:和最初設想同樣,封裝分數的運算與顯示功能。
Node模塊:二叉樹結點類。有操做符結點與操做數結點兩種,操做數結點又分整數、小數、分數三種。有遞歸計算、遞歸解析爲表達式和判斷兩個表達式是否等效功能。
對於判斷表達式是否等效,思路以下:首先判斷根節點是否相等,而後若是兩子樹非空,遞歸分別判斷兩子樹是否相等。假如兩子樹不等,但根節點爲+或*,則交換兩子樹,分別遞歸判斷是否相等(即,加法與乘法可交換)。
Setting模塊:保存各個變量值,經過幾個函數進行設置,有輸入檢查,若是輸入不合法就丟棄輸入、採用默認值。原本咱們打算用xml或json,但後來發現參數很少,並且ui組大多數沒有使用xml / json,所以咱們也沒有提供相應接口。
Generate模塊:調用Node與Setting,按照不一樣的設置生成表達式。
有兩種生成方式:
雖然咱們成功地實現了全部功能,和UI對接的時候也僞裝信心滿滿、咱們作出來的東西天下第一牛逼,但其實至少我內心很是清楚其實有至關多的缺陷。幾個耿耿於懷的問題以下:
首先,咱們寫了過多的重複邏輯。
一開始,咱們提供了std::string的接口,咱們內部全部的字符串操做也是用std::string完成的。好比將分數轉換成字符串,咱們採用的方法即是對分數重載<<操做符,用std::stringstream轉換爲字符串。
然而,後來考慮到std::string有兼容性問題,咱們增長了char數組的接口。當時考慮到須要防止越界訪問,又想要增長效率,因而就用snprintf重寫了一份;所以,對分數重載的<<操做符也用不了了,咱們只能又寫了一個toString函數。
如今想來,其實直接調用std::string的接口,比較一下size,若是沒有超出size的話strcpy進去便可;就算必定要重寫一個函數,也徹底能夠直接用stringstream的getline等函數。真的不知道本身當時腦子裏在想什麼。也許就是貪圖那一點點效率吧,不過也是後來才意識到,這個做業中效率其實沒有那麼重要。
所以,最後的結果就是一樣的代碼邏輯被重複了好幾遍,整個代碼結構也變得至關混亂。到最後,雖然我已經檢查了好幾遍了,但我仍是不太肯定重複代碼中邏輯是否是一致的、會不會有我注意不到的地方有不一致的地方,這樣至關缺少可讀性和可維護性。引覺得戒吧。
這也一樣讓我聯想到《代碼大全2》中說的,代碼的各類優勢有時候是負相關的,應該針對需求的特性去編程。此次爲了增長一丁點點效率,我犯了重複代碼邏輯的錯誤,從而下降了可讀性和可維護性。一樣,此次我也糾結了好久如何生成【絕對隨機】的算式,卻保證整數可以整除,後來才意識到絕對隨機不是很必要。這說明,明確需求是很重要的!不能按本身的想法瞎寫!
其次,此次咱們的測試作得不夠到位。
一來是由於一開始寫代碼圖快,有點懶得寫測試集,本身人工測試也懶得把各類狀況一一測試,因此很多問題是被UI調用後發現的……(在此隆重感謝康鑫同窗)。以後注意了增強了測試力度,問題也少了許多。
二來,是由於generate模塊實在不知道如何測試……由於這個模塊是隨機生成式子,咱們無法給出預期值,若是另外寫一段解析表達式、判斷表達式是否知足要求的代碼感受也不太現實,這段代碼自己就可能會有不少bug……因此最後咱們仍是人工測試的。這裏要感謝隊友,他測試起來比我耐心多了=w=
最後,仍是dll沒有導出類的問題。雖然用dll導出類會有不少麻煩、此次你們不少人都是直接導出的函數,可是以後考慮仍是以爲導出類更合理。由於咱們建樹時有動態申請內存,若是導出類能夠在析構函數中清理內存,而如今這樣導出函數只能在api文檔中不停強調要調用clear函數記得調用clear函數啊必定要調用clear函數。
關於導出類的困難上面提過了,此次咱們畏難求快沒有嘗試抽象類,但我打算以後試試。
必需要至關愧疚地說,開始工做前我和隊友其實對結對的瞭解都不太深入,以爲就各寫各的、有問題交流一下,最後合併一下就能夠了。咱們都以爲一我的寫一我的看是一個至關形式化沒什麼卵用的東西。不知道隊友是怎麼想的,反正我最後以爲這個想法是很錯誤的,咱們此次由於結對合做的問題致使徹底沒發揮結對應有的做用。
首先,我和隊友在編碼風格上不太同樣,並且也沒有事先約定。我偏向c++的編碼風格,並且會很是誇張地空格、換行、分多文件……而個人隊友比較偏c,代碼寫得比較緊湊。因此咱們一開始的合做花了不少力氣糾結風格的不一致,而非好好工做。
這給個人感悟是,不少東西必定要事先約定好。確實存在的矛盾就是確實存在的,不可能寫着寫着就消失,若是一開始不約定好確定會浪費後面的時間。
第二個問題在於,咱們一開始錯誤地理解了題意,個人隊友一開始寫的工做全都是無用功……(爲何咱們兩個都很是默契地一樣地理解錯了題意呢,果真是隊友麼)等到咱們終於意識到題意理解錯了的時候,咱們兩人都完成了各自要寫的基本功能了,因此隊友只能來理解個人代碼,在個人基礎上繼續工做。
然而,一來咱們基本沒有一我的寫、一我的看,二來咱們風格相差太大,並且c與(半吊子)c++之間有着巨大的鴻溝,因此到後面隊友也一直沒有徹底理解個人代碼,致使咱們合做很是尷尬。
理解錯題意是真的沒辦法,因此這裏須要吸收的經驗主要在於怎樣可以快速止損。
首先,一我的寫一我的看仍是有必要的,真的是有必要的。我由於本身編碼習慣不太良好,會常常性地亂寫、寫到通常想起什麼就去寫別的之類的,因此不太想讓別人看我寫,本身由於懶也不想看別人寫。可是後來才意識到,偏偏是習慣很差才須要別人看着寫,才能糾正一些壞習慣。另外,看別人寫代碼的過程才能更好地理解代碼的邏輯,也方便找出可能的錯誤、在現有代碼基礎上繼續工做,而研究一個已經完成的代碼真的很是痛苦。(我看過,真的讓我絕望)假如咱們從一開始就一我的看一我的寫,後面的工做會容易不少。
其次,風格問題真的要從一開始就約定。(這是第二遍提了,這真的是血的教訓)我以後問過隊友,看代碼的障礙究竟在哪裏,才意識到他花了不少時間在處理咱們二人風格上的不一致,而非理解個人代碼邏輯。
最後,咱們在溝通上也有不少問題,不少時候我也想固然地覺得隊友可以理解個人代碼、而沒有給出不少的幫助(我大概高估了本身代碼的可讀性吧),致使中途有一段時間咱們相互拖累。之前一直覺得溝通只是套話官話,如今才明白真的很重要。
總結而言,咱們在一開始沒有作好功課,對結對編程理解很膚淺、覺得只是形式化的東西而已,因此根本沒有從結對中得到太多的好處,總體體驗和我的做業沒什麼差異。
無法寫什麼積極的收穫了,但消極的收穫也算是收穫吧……
關於對接,我從一開始就很迷茫,不太理解到底如何對接;在羣裏看到dll後,我就立馬查了不少資料,花了好一段時間改爲了dll,而後就很是熱心地向團體項目的作UI的隊友兜售個人半成品Core,由於我真的很是懼怕對接出錯,想早點對接。
對接過程總體而言是比較順的,可是仍是有幾個問題:
首先,我發佈出去的dll沒有通過足夠多的測試,我常常只是試了試可以工做、某幾種狀況沒問題就隨便發出去了。這種敷衍之後應該避免=w=
第二,我dll更新太多,給一開始跟我對接的UI組帶來了很大的困擾……其實我如今也不太明白更新應該是怎樣的頻率,確實時不時就有些新發現的bug須要修復,可是太快更新確實不合適。仍是說這說明咱們發佈dll太早了?但願有人可以指教。
第三,我一開始沒太關注UI組的需求,具體表如今某組UI強烈要求我使用文件接口但我以爲這樣很差就堅持用std::string接口=w=後來仍是增長了文件接口,並且意識到這樣不太好。應該儘可能知足UI需求。
對接還有一個要點就是要及時迴應UI反映的問題,不過這個我自我感受挺好的……基本上UI發的消息只要我在線就秒回,就算身邊沒電腦也立馬用手機去github上面查代碼有沒有出錯,以致於最後幾天我一看到有UI的人跟我發消息就緊張……
### 4.4. 關於重構:咱們的初始規劃、中間過程和最後架構上面提到了,能夠看到我又重構了……上次作我的做業我就很是崩潰,被本身重構到想哭了,當時幾乎要發誓堅定不重構。然而這一次仍是重構了。
就很是崩潰,不知道爲何一開始的架構老是錯的。至今爲止的編程,有時候架構多了、總體臃腫而冗餘,有時候架構少了、各個功能過分耦合。有的功能能夠合併有的功能應該分開。道理天然都知道,可是怎麼判斷是多了仍是少了仍是正好?怎麼判斷什麼功能應該合併什麼應該分開?No idea.
以後羣裏也討論這個問題,看到某位大佬說重構是很正常的事情,新手很難一開始就選擇正確的架構的。豁然開朗,也感到了安慰,看來不是個人問題,實踐是檢驗真理的第一標準,不少東西要寫了才知道有問題。架構很大程度上不是一個理性的東西,而是一個感性的東西,須要不少經驗積累,慢慢養成一種直覺。(因此說之後仍是要重構……)
具體到此次來講,我學到的經驗和上次差很少,不要盲目地增長封裝的層數。
## 5. 總結:總之雖然此次寫得很糟心但仍是有不少收穫…… 謝謝看到這裏!