寫在前面
這次的結對項目我做了很長時間,感觸也很多。在這次項目中我使用了Java GUI作為和用戶的交互方式,但是在上Java課的時候我對GUI和事件驅動這里並沒有學的多好,可能是當時對編程還沒有什么理解,對這一部分的知識理解地很吃力,只是死記硬背下來應對了考試,在這次項目過程中我又將我的Java書拿出來把這一部分的知識復習了一遍,現在再來看這一部分的知識與當時的感受完全不同了,當時看起來雲里霧里的知識現在看來就是理所當然和透徹的了,所以上手地也比較快,我學到的知識也立刻得到了實際的應用。
不僅是專業知識,在這次項目里,我接觸了從未想過的結對編程,代碼復審;我一步一步地學了用JUnit寫單元測試,用Eclemma查看單元測試的覆蓋率,分析代碼的性能,真正實際接觸並親身做了一些軟件工程上的環節,而不僅是從書本上學到了理論,這些經歷雖然辛苦,但是帶來了很多收獲,也讓我對這些環節有了更深的了解和記憶。
那么接下來我就用博客記錄一下我這次結對項目。
Coding.Net項目地址
https://git.coding.net/ForeverSevrous/CoupleProject.git
PSP
| PSP2.1 |
任務內容 |
計划共完成需要的時間(min) |
實際完成需要的時間(min) |
| Planning |
計划 |
30 |
* |
| · Estimate |
· 估計這個任務需要多少時間,並規划大致工作步驟 |
30 |
* |
| Development |
開發 |
1000 |
* |
| · Analysis |
· 需求分析 (包括學習新技術) |
60 |
* |
| · Design Spec |
· 生成設計文檔 |
30 |
* |
| · Design Review |
· 設計復審 (和同事審核設計文檔) |
30 |
* |
| · Coding Standard |
· 代碼規范 (為目前的開發制定合適的規范) |
10 |
* |
| · Design |
· 具體設計 |
30 |
* |
| · Coding |
· 具體編碼 |
720 |
* |
| · Code Review |
· 代碼復審 |
60 |
* |
| · Test |
· 測試(自我測試,修改代碼,提交修改) |
60 |
* |
| Reporting |
報告 |
55 |
* |
| · Test Report |
· 測試報告 |
30 |
* |
| · Size Measurement |
· 計算工作量 |
10 |
* |
| · Postmortem & Process Improvement Plan |
· 事后總結, 並提出過程改進計划 |
15 |
* |
信息隱藏(Information Hiding)
信息隱藏指在設計和確定模塊時,使得一個模塊內包含的特定信息(過程或數據),對於不需要這些信息的其他模塊來說,是不可訪問的。
在結對項目中,使用命令行模式執行和使用GUI界面執行出題部分時調用了同一部分核心代碼,在這里我將出題這一部分信息隱藏,當改變人機交互模式的時候就不會給系統帶來全局性的影響。在我的程序中避免了A類調用B類的程序,B類有調用A類的程序這種情況,這也是所謂的“循環依賴”,這種情況會阻礙信息隱藏。
接口設計(Interface Design)
在本項目設計接口過程中,使用有意義的命名方式使接口的功能一目了然,增強了可讀性;類名和方法名也都使用了有意義的命名方式;在開發過程中加好了注釋,方便自己和他人看懂代碼;在接口設計過程中對相關原則也有了一些體會,主要參考以下內容:
https://blog.csdn.net/blueangle17/article/details/55049858
https://jingyan.baidu.com/article/066074d626ea09c3c21cb0b9.html
松耦合(Loose coupling)
耦合的強度依賴於:(1)一個模塊對另一個模塊的調用;(2)一個模塊向另一個模塊傳遞的數據量;(3)一個模塊施加到另一個模塊的控制的多少;(4)模塊之間接口的復雜程度。等等。
模塊內子程序(下一個層次上)應共享數據(有一定的耦合度),而減少全局變量能降低子程序性間的耦合性。
類與類之間通常通過接口的契約實現服務提供者/服務請求者模式,這就是典型的松耦合。
在結對項目中,我盡量減少一個模塊向另一個模塊傳遞的數據量,如:是否有乘除法通過在主函數中判斷調用不同的類,而不是通過傳遞參數。
計算模塊接口的設計與實現過程
吸取在個人項目中同學對我的建議,我畫出了用到的類和其中的方法之間的關系圖(含有方法的作用),希望能有助於大家的理解:

我的結對項目使用的算法都比較普通,唯一還算值得一提的是我改進了異常提醒的代碼,使程序從原來的每運行一次只能報一次錯變成了運行一次就將所有的錯誤都進行提示,這樣就提高了用戶的使用效率,提升了使用體驗。
計算模塊接口部分的性能改進
由於出題時對題目的結果和運算過程中的部分結果是有要求的,所以這次雖然在出題時不要求給出結果,但還是需要調用計算模塊來提供檢查功能,每計算一步就對結果進行判斷,對不合格的運算式舍棄不用,但是在出現有乘法的時候數字就會成倍變大,導致運算式不可用,對於這個問題我在出題模塊內進行了優化,當出現乘法時,盡量縮小乘數,在一定程度上使結果不會超出限定,加快出題速度。
吸取老師的建議,我們在第一次性能分析的基礎上做了CPU性能分析,以下是以下是訪問樹 Call Tree:

以下是熱點 Hot Spots視圖,顯示了消耗時間最多的方法:

以下是內存性能分析圖(包含改進后的成果):


計算模塊部分單元測試展示
測試Core類(包含newExpSome,newExpAll,divideExactly)
在測試Core類中的函數時,我模擬了在實際應用時會出現的情況,結合更大限度地覆蓋更多代碼這一目標,我設計了多組參數,期望能覆蓋到各種情況。通過多次進行單元測試,將多次單元測試結合起來,達到了97%。

設計思路:
- 改變運算符個數的上限:由於運算符個數不同產生括號的代碼不同,所以增加情況個數能夠覆蓋更多代碼。
- 設置是否有括號和乘除。
- 增加運算次數:這樣在使用隨機數的函數中能出現更多的情況,覆蓋更多的代碼,走通一些異常處理代碼。
測試Command類(包含main和write方法)
設計思路:
(測試main方法)
在main方法中實現了對輸入參數的解析和對輸入參數異常的報錯,所以測試這個部分的函數最主要的就是構造不同的異常情況,同時也不要忘記測試參數正確時的情況,因為這也是代碼的一部分(單元測試中第一次就是因此覆蓋率較低)。
(測試write方法)
在write方法中主要實現了將傳入的ArrayList分行寫入文件,所以測試這個部分的思路就是:在測試方法中構造一個ArrayList,向里面傳入一些值,然后以它為參數傳入到write方法中,通過輸出結果判定是否成功執行。

單元測試代碼展示(以測試Core.java為例):
1 import static org.junit.Assert.*; 2 3 import org.junit.After; 4 import org.junit.AfterClass; 5 import org.junit.Before; 6 import org.junit.BeforeClass; 7 import org.junit.Test; 8 9 10 public class CoreTest { 11 12 @BeforeClass 13 public static void setUpBeforeClass() throws Exception { 14 } 15 16 @AfterClass 17 public static void tearDownAfterClass() throws Exception { 18 } 19 20 @Before 21 public void setUp() throws Exception { 22 } 23 24 @After 25 public void tearDown() throws Exception { 26 } 27 28 @Test 29 public void testNewExpSome() { 30 for(int i=0;i<6;i++){ 31 Core.newExpSome(false, 200, 20, 6); 32 } 33 for(int i=0;i<24;i++){ 34 Core.newExpSome(true, 200, 20, 4); 35 Core.newExpSome(true, 200, 20, 4); 36 Core.newExpSome(true, 200, 20, 4); 37 } 38 39 System.out.println("NewExpSome程序正常!"); 40 } 41 42 @Test 43 public void testNewExpAll() { 44 for(int i=0;i<12;i++){ 45 Core.newExpAll(false, 200, 20, 6); 46 } 47 for(int i=0;i<24;i++){ 48 Core.newExpAll(true, 200, 20, 4); 49 Core.newExpAll(true, 200, 20, 4); 50 Core.newExpAll(true, 200, 20, 4); 51 } 52 53 System.out.println("NewExpAll程序正常!"); 54 } 55 56 @Test 57 public void testDivideExactly() { 58 59 } 60 61 }
計算模塊部分異常處理說明
以下為各參數會出現的異常及對應的單元測試樣例
1.出題數量:出題數量范圍錯誤;出題數量輸入值不是數字;未輸入出題數量。
單元測試樣例:
1 String[] args1 = {“-n”,”-20”} 2 Command.main(args1); 3 String[] args2 = {“-n”,”a”} 4 Command.main(args2); 5 String[] args3 = {“-b”,”-c”} 6 Command.main(args3);
2.運算符上限:上限范圍錯誤;輸入值不是數字。
單元測試樣例:
1 String[] args1 = {“-o”,”20”} 2 Command.main(args1); 3 String[] args2 = {“-o”,”a”} 4 Command.main(args2);
3.數值范圍:上下界范圍錯誤;上下界輸入不是數字;下界大於上界。
單元測試樣例:
1 String[] args1 = {“-m”,”-20”,”2000”} 2 Command.main(args1); 3 String[] args2 = {“-m”,”a”,”b”} 4 Command.main(args2); 5 String[] args3 = {“-m”,”40”,”20”} 6 Command.main(args3);
4.輸入的參數不是規定的參數格式,如輸入“-”、“-x”等作為參數。
單元測試樣例:
1 String[] args1 = {“-x”} 2 Command.main(args1);
界面模塊的詳細設計過程
在設計界面的過程中,我分為導航頁,獲得出題參數頁,參數錯誤提示頁,出題完成提示頁,上傳題目做題頁。這幾個頁面的關系如下:

導航頁和獲得出題參數頁使用了同種方式來生成用戶界面,代表導航頁和出題參數頁的類繼承了JFrame類,在這兩個類中還分別聲明了一個內部類繼承了JPanel類,在內部類中加入頁面需要的各種組件,然后在外部類的構造方法中新建一個內部類的示例,並將這個內部類的對象加入到外部類的實例中,在這里以導航頁的代碼為例展示這種方式:
1 import java.awt.event.ActionEvent; 2 import java.awt.event.ActionListener; 3 4 import javax.swing.*; 5 6 public class HelloFrame extends JFrame { 7 public HelloFrame() { 8 setLocationRelativeTo(null); 9 Hello h = new Hello(); 10 add(h); 11 h.make.addActionListener(new ActionListener() { 12 13 @Override 14 public void actionPerformed(ActionEvent e) { 15 // TODO Auto-generated method stub 16 setVisible(false); 17 } 18 19 }); 20 h.ans.addActionListener(new ActionListener() { 21 22 @Override 23 public void actionPerformed(ActionEvent e) { 24 // TODO Auto-generated method stub 25 setVisible(false); 26 } 27 28 }); 29 } 30 31 public static void main(String[] args) { 32 // TODO Auto-generated method stub 33 HelloFrame hf = new HelloFrame(); 34 hf.setTitle("四則運算程序"); 35 hf.setLocationRelativeTo(null); 36 hf.setDefaultCloseOperation(EXIT_ON_CLOSE); 37 hf.setVisible(true); 38 hf.setBounds(300, 200, 640, 410); 39 // 設置顯示位置距離左邊300像素距離上邊200像素及屏幕大小500*400 40 } 41 42 static class Hello extends JPanel { 43 JButton make = new JButton("出題"); 44 JButton ans = new JButton("做題"); 45 JLabel title = new JLabel("歡迎使用四則運算軟件,您是要:"); 46 JLabel l = new JLabel(); 47 private MakeExp makeexp = new MakeExp(); 48 49 public Hello() { 50 51 Icon icon = new ImageIcon("3.jpg"); // 在此直接創建對象 52 l.setIcon(icon); 53 add(l); 54 55 make.setSize(150, 50); 56 make.setLocation(250, 150); 57 make.setFont(new java.awt.Font("Dialog", 1, 20)); 58 make.setToolTipText("點我可以生成含四則遠算算式的文件哦!"); 59 l.add(make); 60 61 ans.setSize(150, 50); 62 ans.setLocation(250, 250); 63 ans.setFont(new java.awt.Font("Dialog", 1, 20)); 64 ans.setToolTipText("點我可以上傳文件做題,還可以記錄成績哦!"); 65 l.add(ans); 66 67 title.setFont(new java.awt.Font("Dialog", 1, 30)); 68 title.setSize(640, 100); 69 title.setLocation(80, -15); 70 l.add(title); 71 72 make.addActionListener(new ActionListener() { 73 74 @Override 75 public void actionPerformed(ActionEvent e) { 76 // TODO Auto-generated method stub 77 setVisible(false); 78 makeexp.setTitle("請輸入要求"); 79 makeexp.setLocationRelativeTo(null); 80 makeexp.setDefaultCloseOperation(EXIT_ON_CLOSE); 81 makeexp.setVisible(true); 82 makeexp.setBounds(300, 200, 640, 410); 83 84 } 85 86 }); 87 88 ans.addActionListener(new ActionListener() { 89 90 @Override 91 public void actionPerformed(ActionEvent e) { 92 // TODO Auto-generated method stub 93 Count2_1 c = new Count2_1(); 94 c.setVisible(true); 95 c.setDefaultCloseOperation(EXIT_ON_CLOSE); 96 } 97 98 }); 99 } 100 } 101 }
參數錯誤提示頁和出題完成提示頁使用了同種方式來生成用戶界面,都是在構造函數中接收字符串,然后將字符串放入JLabel中,再添加到JFrame中,這里以參數錯誤提示頁為例來展示這種方式:
1 static class Waring extends JFrame{ 2 public Waring(ArrayList<String> a){ 3 setLayout(new GridLayout(6,1,15,15)); 4 for(String s:a){ 5 JLabel j = new JLabel(s); 6 add(j); 7 } 8 setSize(500,400); 9 setLocationRelativeTo(null); 10 setTitle("通知"); 11 } 12 }
界面模塊與計算模塊的對接
(UI模塊的設計詳見上一部分,這里主要講述)
我將我要完成的功能分為兩部分,收集用戶的意願(即參數)和到相應的方法中執行,在UI模塊中我主要就是通過GUI組件和相關事件來獲取用戶的意願,並通過判斷參數內容來決定接下來要調用的函數,而計算模塊主要就是被調用,接收參數並進行相關的計算和生成功能。兩部分各司其職,互不干擾。
以下為本項目的主要功能和截圖:

【導航頁】

【獲得出題參數頁】

【參數錯誤提示頁】

【上傳題目做題頁】

【上傳文件格式錯誤提醒】

【獲得最好成績功能】
結對過程

在這次結對項目過程中我和陳琦在工作室中坐在一起進行結對編程,體驗了全新的一個人指導一個人實際操作的開發方式~
關於結對編程
結對編程作為最近流行的編程方式,有不少的優點。第一,結對編程的方式是兩名程序開發人員一個扮演駕駛者的角色,進行代碼編寫,另一個扮演觀察員的角色,對代碼進行評測。這就使得代碼的正確率大大提高,增強代碼的質量。第二, 結對編程會使兩個人都會發現自己平時沒有注意到的地方,相互討論,可以更快更有效地解決問題和發現自身的不足,從而提高了雙方的編碼能力。第三,和團隊編程相比,結對編程只有兩人,想法交流更加迅速,編寫速度更快,編程效率更高。
然而,在面對一些問題時,雙方可能有不同的看法,容易產生分歧,造成不必要的時間內耗。如果兩名開發人員的能力相差過大,可能造成想法上溝通不暢發生爭執,不利於團隊和諧,或者造成代碼基本都是由一方完成,而另一方就是享受成果的局面。
在本次編程中開始由於我們兩人從未接觸過這種編程方式,所以不熟悉各自的職責,導致了一些小的問題,但是經過一段時間的磨合,我們的配合逐漸合拍了起來,漸漸也體會到了結對編程的好處,就是編程完成后兩個人就都理解了代碼,不會像之前一樣只理解自己寫的代碼,同時也省去了將兩個人代碼整合的步驟;而且在編程過程中對於一些問題兩個人可以得出更好的結論,就直接優化了代碼,減少了后續的工作量;兩個人一起編程也可以相互監督,減少開小差的幾率,提高工作效率。
以下是我們的優缺點:
| 陳琦 | 呂曉真 | |
| 優點 | 注重細節,善於優化程序的代碼 |
規划項目進度,思路清晰 |
| 對代碼的理解能力強 |
代碼規范,格式簡潔,便於閱讀 |
|
| 態度積極,不懶散 |
細心,可以發現程序中隱藏的問題 |
|
| 缺點 | 耐心欠佳,在解決不了問題時會變得急躁 |
算法欠佳,有時會導致代碼過長 |
PSP
| PSP2.1 |
任務內容 |
計划共完成需要的時間(min) |
實際完成需要的時間(min) |
| Planning |
計划 |
30 |
60 |
| · Estimate |
· 估計這個任務需要多少時間,並規划大致工作步驟 |
30 |
60 |
| Development |
開發 |
1000 |
3080 |
| · Analysis |
· 需求分析 (包括學習新技術) |
60 |
120 |
| · Design Spec |
· 生成設計文檔 |
30 |
30 |
| · Design Review |
· 設計復審 (和同事審核設計文檔) |
30 |
20 |
| · Coding Standard |
· 代碼規范 (為目前的開發制定合適的規范) |
10 |
10 |
| · Design |
· 具體設計 |
30 |
40 |
| · Coding |
· 具體編碼 |
720 |
2500 |
| · Code Review |
· 代碼復審 |
60 |
210 |
| · Test |
· 測試(自我測試,修改代碼,提交修改) |
60 |
150 |
| Reporting |
報告 |
55 |
100 |
| · Test Report |
· 測試報告 |
30 |
60 |
| · Size Measurement |
· 計算工作量 |
10 |
20 |
| · Postmortem & Process Improvement Plan |
· 事后總結, 並提出過程改進計划 |
15 |
20 |
后記
這篇博客我也寫了很久,在寫博客期間我又從頭到尾回顧了我的整個項目,重新捋了一遍我的思路,從PSP上也感觸頗多,實際編碼的時間總是大大超過預計,自我測試時間也是常常翻倍,這就還需要增強自己的代碼能力,而且在開始做之前就先想好比較完善的方案,考慮到比較多的情況,就是說不要小看編碼前的設計准備工作,要認真做,這樣在自我測試階段才能不會出現太多需要修改的地方,為整個項目進度提速。
