1. 程式人生 > >結對項目-最長單詞鏈博客

結對項目-最長單詞鏈博客

improve 兩個 ttl 選擇 wikipedia 能力 博客 edi rev

1、Github項目地址

  • Here !

2、PSP表格(程序各個模塊在開發上預計耗費的時間和實際耗費的時間)

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 60 30
· Estimate · 估計這個任務需要多少時間 60 30
Development 開發 1680 2320
· Analysis · 需求分析 (包括學習新技術) 120 100
· Design Spec · 生成設計文檔 30 30
· Design Review · 設計復審 (和同事審核設計文檔) 30 20
· Coding Standard · 代碼規範 (為目前的開發制定合適的規範) 30 30
· Design · 具體設計 60 120
· Coding · 具體編碼 1200 1680
· Code Review · 代碼復審 30 40
· Test · 測試(自我測試,修改代碼,提交修改) 180 300
Reporting 報告 390 510
· Test Report · 測試報告 180 200
· Size Measurement · 計算工作量 30 10
· Postmortem & Process Improvement Plan · 事後總結, 並提出過程改進計劃 180 300
合計 2130 2860

3、關於Information Hiding, Interface Design, Loose Coupling等方法的接口設計

Information Hiding

  • 其實題目給出的三種方法或者說是原則都是和封裝有關的,在我的理解裏封裝是技術,而比如信息隱藏實則是目的。雖然我們在實際的代碼中可以說是沒有用到面向對象的格式來編寫,但其實用到的仍然還是面向對象的思想。比如說計算核心Core的內部功能是不展示出來的,通過作業規定的兩個接口來使用戶和代碼交換各自需要的東西。類似的在用戶輸入模塊、讀取文件模塊等我們都設計了與計算模塊Core類似的接口,每個不同獨立的功能都封裝成了函數,保證信息的隱藏功能。

Interface Design

  • 我查閱了一些資料,找到的大多數的接口設計貌似都是很陌生而且與現在所學格格不入。但還是從其中學習到了一些無論設計什麽接口都應該最起碼遵循的原則,比如命名必須規範優雅,保證接口要做的事情是比較單一的事情(單一性),良好的可擴展性和可移植性,而在實際編程中我們也是這樣做的。

Loose Coupling

  • 這個詞剛剛看上去我甚至都不知道是什麽意思,在維基百科上才大概了解了一些:

In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.[1] Loose coupling is the opposite of tight coupling.

——引用自維基百科

  • 但是仍然覺的對這個概念很不清晰,繼續往下看並按照中文翻譯查了一些資料,大概知道了其還是指一個組件與另一個組件具有直接聯系的程度。仍然還是封裝與非封裝的意思。在這裏舉幾個我們程序中的封裝接口的例子:

void InputHandler(int argc, char* argv[], bool &enable_loop, int &word_or_char, char &head, char &tail, string &Filename);
void ReadFile(string FileName, vector<string> &words);
void DFS_Length(Graph G, int v, vector<string> words, char tail);

4、計算模塊接口的設計與實現過程

4.1 問題分析

以前做過類似的題,輸入的所有單詞能否全部首尾相連形成鏈。由於單詞首尾相連有多種連接方式,故基本的數據結構為圖。
建圖有兩種方式,一種是以單詞為節點,如果單詞間正好可以首尾連接,則添加一條邊,該邊即為連接的字母。另一種建圖方式是以字母為節點,以單詞為邊,出現一個單詞,即把首字母節點向尾字母節點添加一條邊,邊的值即為該單詞。
對於這道題目而言,由於單詞需要輸出,加之對第二種建圖方式掌握並不熟練,因此選擇的是第一種建圖方式。
模型確立後,問題就可以簡化成“求圖中的最長鏈”,即最長路徑問題,顯然問題是多源最長路徑問題。

4.2 數據結構與算法

數據結構為圖,存儲方式為鄰接矩陣,理由是能更契合floyd算法。
對於無環情況,由於為多源最長路徑問題,聯想到最短路徑問題,可以確定為floyd算法。
而對於有環情況,由於出現了正值環,floyd算法不再適用。在找不到更有解決方法的情況下,只能適用DFS深度優先搜索求解。

4.3 模塊組織

ReadFile: 讀取文件的模塊,將文件中的單詞提取進入容器vector中。
Graph: 圖的定義。
InputHandler:處理輸入的模塊,讀取命令行並處理參數。
FindLongestWordList: 計算模塊,內含計算接口。計算出單詞中的最長鏈。

4.4 算法關鍵

首先需要判斷有無環,對於沒有-r參數的輸入來說,如果有環需要報錯。這裏也是用到DFS的染色算法。每個點有三種狀態:未遍歷過,遍歷過,當前序列正在遍歷。如果一次DFS中一個點與正在遍歷中的點相連了,說明DFS回到了之前的點,即圖中有環。
另一問題是由於無環情況最多可有10000個單詞,而floyd算法時間復雜度為O(n^3),暴力的計算顯然是不行的。考慮到對於無環的情況,有如下特性:對於單詞element和elephant,由於無環,這兩個單詞最多只有一個會出現在鏈中。(否則會出現element, t..., ..., ....e, elephant / element,這樣一定是有環的),而如果要滿足字母最多,顯然這時候需要選擇elephant加入鏈中。因此我們可以對於所有首尾字母相同的單詞,保留首尾字母組合中,最長的一個單詞。這樣的操作之後,最多的單詞數目為351,即使是時間復雜度O(n^3)的算法也能很快得出結果。另外可以計算得,最長鏈的長度最大為51。

5、UML圖

技術分享圖片

6、計算模塊接口部分的性能改進

  • 首先是無環情況,其性能最大阻礙是10000個單詞大樣本情況下,floyd算法時間復雜度過高導致的。但是在4.4有介紹過,我們可以通過無環單詞鏈的特性來削減樣本數量,削減後單詞數量少,即使時間復雜度高也能很快跑出結果。因此性能方面上沒有太大問題。

技術分享圖片

技術分享圖片

  • 其次是有環情況,由於DFS算法仍屬於暴力遞歸搜索,並不算很好的算法,其性能也著實較差。但是我們也想不到更好的解決算法,所以並沒有改進。

7、關於Design by Contract, Code Contract的優缺點以及結對作業中的體現

  • 契約式編程對於軟件工程是一個極大的理論改革,對於C/S模式造成了極大的影響和沖擊。對於C/S模式,我們看待兩個模塊的地位是不平等的,我們往往要求server非常強大,可以處理一切可能的異常,而對client不聞不問,造成了client代碼的低劣。而在DbC中,使用者和被調用者地位平等,雙方必須彼此履行義務,才可以行駛權利。調用者必須提供正確的參數,被調用者必須保證正確的結果和調用者要求的不變性。雙方都有必須履行的義務,也有使用的權利,這樣就保證了雙方代碼的質量,提高了軟件工程的效率和質量。缺點是對於程序語言有一定的要求,契約式編程需要一種機制來驗證契約的成立與否。而斷言顯然是最好的選擇,但是並不是所有的程序語言都有斷言機制。那麽強行使用語言進行模仿就勢必造成代碼的冗余和不可讀性的提高。比如.NET4.0以前就沒有assert的概念,在4.0後全面引入了契約式編程的概念,使得契約式編程的可用性大大提高了。此外,契約式編程並未被標準化,因此項目之間的定義和修改各不一樣,給代碼造成很大混亂,這正是很少在實際中看到契約式編程應用的原因。在我們的代碼中,對於模塊間使用了契約的思想,保證雙方地位的平等。調用者的傳入參數必須是正確的,否則責任不在被調用者,而在傳入者。

——優缺點引用自維基百科

8、計算模塊部分單元測試展示

技術分享圖片

  • 在單元測試部分我們對程序中除輸出部分外(由於輸出部分只是一個簡單的輸出到文件)其他所以部分或函數進行的全面的單元測試,如圖共25個,單元測試的全部代碼也已上傳至Github。下面我將拿出部分單元測試代碼具體介紹,並在這部分的最後附上單元測試的測試服概率截圖。

        TEST_METHOD(TestMethod3)
        {
            // TODO: normal_test3
            char* words[101] = { "element", "heaven", "table", "teach", "talk"};
            char* answer[101];
            for (int i = 0; i < 101; i++)
            {
                answer[i] = (char*)malloc(sizeof(char) * 601);
            }

            int l = gen_chain_word(words, 5, answer, 0, 0, true);
            Assert::AreEqual(l, 4);
            Assert::AreEqual("table", answer[0]);
            Assert::AreEqual("element", answer[1]);
            Assert::AreEqual("teach", answer[2]);
            Assert::AreEqual("heaven", answer[3]);
            for (int i = 0; i < 101; i++)
            {
                free(answer[i]);
            }
        }
  • 上面的單元測試代碼是測試計算核心中的gen_chain_word接口函數,由於單元測試需要我手動加入words,所以這裏的單元測試數據比較小,就是構造一個有環有鏈的單詞文本,並且是在輸入‘-r’的情況下,從而得到一個正確的單詞鏈。

        TEST_METHOD(TestMethod6)
        {
            // TODO: normal_test6
            char* words[101] = { "apple", "banane", "cane", "a", "papa", "erase" };
            char* answer[101];
            for (int i = 0; i < 101; i++)
            {
                answer[i] = (char*)malloc(sizeof(char) * 601);
            }

            int l = gen_chain_char(words, 6, answer, 'a', 'e', false);
            Assert::AreEqual(l, 3);
            Assert::AreEqual("a", answer[0]);
            Assert::AreEqual("apple", answer[1]);
            Assert::AreEqual("erase", answer[2]);
            for (int i = 0; i < 101; i++)
            {
                free(answer[i]);
            }
        }
  • 上面的單元測試代碼是測試計算核心中的gen_chain_char接口函數,這裏構造了一個沒有環的文本數據,而且其最多單詞鏈和最長單詞鏈不同,並固定了首尾字母。

        TEST_METHOD(TestMethod2)
        {
            // 正確_2
            int argc = 6;
            char* argv[101] = { "Wordlist.exe", "-r", "-h", "a", "-c", "test_1.txt" };
            char head;
            char tail;
            bool enable_loop;
            int word_or_char = 0;
            string Filename;
            InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);

            Assert::AreEqual(enable_loop, true);
            Assert::AreEqual(word_or_char, 2);
            Assert::AreEqual(head, 'a');
            Assert::AreEqual(tail, char(0));
            Assert::AreEqual(Filename, (string)"test_1.txt");
        }
  • 上面的單元測試代碼是測試接收命令行輸入函數InputHandler,這裏沒有什麽太多好說的, 就是把命令行輸入參數的所有正確組合全部測試一遍即可(參數輸入順序可以改變)。

單元測試覆蓋率截圖(由於C++沒有找到直接測試單元測試覆蓋率的插件,這裏用的方法是將單元測試代碼移至main函數中用OpenCppCoverage插件得到的覆蓋率,部分異常測試沒有放進來,所以覆蓋率沒有達到100%)

技術分享圖片

9、計算模塊部分異常處理說明

  • 在異常處理模塊我們一共自定義了8種類型的異常,接下來我將會結合每種異常的單元測試說明每種異常的設計目標以及錯誤對應的場景(單元測試的構造方法就是保證此函數可以捕捉到異常且捕捉的是與當前錯誤相對應的異常,否則單元測試不通過)。

1. 錯誤的參數組合(其中包括出現多個相同命令比如‘-r’、‘-r’,‘-h’和‘-c’同時出現,‘-h’和‘-c’都沒有,即不指定求何種單詞鏈)


        TEST_METHOD(TestMethod3)
        {
            // 錯誤_1
            int argc = 5;
            char* argv[101] = { "Wordlist.exe", "-r", "-r", "-c", "test_1.txt" };
            char head;
            char tail;
            bool enable_loop;
            int word_or_char = 0;
            string Filename;
            try {
                InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
                Assert::IsTrue(false);
            }
            catch (myexception1& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是‘-r’出現了兩次,錯誤的參數組合。

2. 指定單詞鏈首尾不合法(比如‘-h’、‘1’或者‘-t’、‘ag’)


        TEST_METHOD(TestMethod7)
        {
            // 錯誤_5
            int argc = 6;
            char* argv[101] = { "Wordlist.exe", "-r", "-h", "1", "-c", "test_1.txt" };
            char head;
            char tail;
            bool enable_loop;
            int word_or_char = 0;
            string Filename;
            try {
                InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
                Assert::IsTrue(false);
            }
            catch (myexception2& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是‘-h’指定首字母為‘1’,明顯是錯誤的。

3. 輸入的參數不是指定的那幾個參數,不符合規定(如輸入‘-b’)


        TEST_METHOD(TestMethod9)
        {
            // 錯誤_7
            int argc = 5;
            char* argv[101] = { "Wordlist.exe", "-b", "-r", "-c", "test_1.txt" };
            char head;
            char tail;
            bool enable_loop;
            int word_or_char = 0;
            string Filename;
            try {
                InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
                Assert::IsTrue(false);
            }
            catch (myexception3& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是輸入參數‘-b’顯然是不符合規定的。

4. 文件不存在的情況


        TEST_METHOD(TestMethod2)
        {
            // 錯誤
            vector <string> words;
            try {
                ReadFile("normal_test3.txt", words); // 不存在的文件
                Assert::IsTrue(false);
            }
            catch (myexception4& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是測試了一個在此路徑下不存在的文件。

5. 讀取的文件中有單詞長度超過600


        TEST_METHOD(TestMethod2)
        {
            // 錯誤
            vector <string> words;
            try {
                ReadFile("long_word_test.txt", words);
                Assert::IsTrue(false);
            }
            catch (myexception4& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是文件中存在長度超過600的單詞。

6. 讀取的文件中無環的超過10000個單詞,有環的超過100個單詞


        TEST_METHOD(TestMethod2)
        {
            // 錯誤
            vector <string> words;
            try {
                ReadFile("more_words_test.txt", words); 
                Assert::IsTrue(false);
            }
            catch (myexception4& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是所測試文件中單詞數超過了10000。

7. 讀取文件中有單詞環且參數沒有輸入‘-r’


        TEST_METHOD(TestMethod10)
        {
            // wrong_test2
            char* words[101] = { "alement", "oeaven", "tabla", "teaco", "talk" };
            char* answer[101];
            for (int i = 0; i < 101; i++)
            {
                answer[i] = (char*)malloc(sizeof(char) * 601);
            }


            try {
                int l = gen_chain_char(words, 5, answer, 0, 'n', false);
                Assert::IsTrue(false);
            }
            catch (myexception7& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是傳入單詞可以形成環,且用戶沒有傳入參數‘-r’。

8. 讀取文件中無法形成最少兩個單詞的單詞鏈


        TEST_METHOD(TestMethod11)
        {
            // wrong_test3
            char* words[101] = { "alement", "oeaven", "tabla", "teaco", "talk" };
            char* answer[101];
            for (int i = 0; i < 101; i++)
            {
                answer[i] = (char*)malloc(sizeof(char) * 601);
            }


            try {
                int l = gen_chain_word(words, 5, answer, 'b', 'n', true);
                Assert::IsTrue(false);
            }
            catch (myexception8& e) {
                Assert::IsTrue(true);
            }
            catch (...) {
                Assert::IsTrue(false);
            }
        }

這個單元測試是規定了首尾字母後,單詞鏈中沒有用戶所要求的單詞鏈。

10、界面模塊的詳細設計過程(GUI)

  • 在界面模塊這方面我們沒有實現GUI,而是完成了最基本的命令行模塊。其實如果是命令行模塊的話就非常簡單了,根據在命令行輸入的內容及長度存入char* argv[]以及int argc中,然後再傳給InputHandler函數中對傳入的參數進行分析處理,主要是識別錯誤的參數輸入(第9部分已經詳細介紹)以及將正確的參數組合中的信息存下來,比如說head和tail是否有限定,單詞文本是否允許有環以及要求的單詞鏈是要單詞最多還是單詞總長度最長。由於實現很簡單,這裏不必再貼上代碼贅述。

11、界面模塊與計算模塊的對接(GUI)

  • 命令行模塊與兩個計算核心模塊的對接其實也很簡單。我們從命令行讀入的各類參數如果是正確無誤的,那麽我們可以相對應地確定傳入兩個計算模塊的head、tail、enable_loop以及執行哪個計算模塊的判斷變量。即確定規範的單詞鏈首字母尾字母,如果沒有規定則傳入0,是否允許有環的變量。如果不允許,則需要判斷傳入單詞文本是否可以形成環,如果形成環則報告異常。下面是簡單是一張命令行輸入截圖:

技術分享圖片

12、結對之過程

  • 由於與隊友為舍友,結對時相對簡單很多,只需要到對鋪和隊友一起結對編程就行了。我們的水平差不多, 編程能力和數據結構算法的掌握都不算太好。初期時我們主要是一起討論算法,如何實現基本的功能,數據結構應該用什麽。敲定一個算法之後就開始分頭找資料,最後再匯總資料,交給他來敲代碼或者我來在一些地方進行修改。編寫時經常會遇到一些意料不到的bug,最後必須一起搜索如何解決。但是兩個人在一起編寫代碼時,有一個人來隨時審視代碼,有不懂的地方或者不對勁的地方另一人都可以隨時提出來。因此雖然結對編程效率沒有提高, 但是效果會比兩個單人編寫來的更好。
    總的來說這次題目難度還是沒有那麽爆炸,所以我們之間的合作也比較愉快。至於提意見的藝術是根本用不上的,畢竟是舍友也不會產生矛盾。下面是我們在初期時討論算法的圖片:

技術分享圖片

13、結對編程的優缺點及評價

結對編程優缺點

  • 下面是一些結對編程的優點:程序員互相幫助,互相教對方,可以得到能力上的互補。可以讓編程環境有效地貫徹Design。增強代碼和產品質量,並有效的減少BUG。降低學習成本。一邊編程,一邊共享知識和經驗,有效地在實踐中進行學習。在編程中,相互討論,可能更快更有效地解決問題。當然,結隊編程也會有一些不好的地方:對於有不同習慣的編程人員,可以在起工作會產生麻煩,甚至矛盾。有時候,程序員們會對一個問題各執己見(代碼風格可能會是引發技術人員口水戰的地方),爭吵不休,反而產生重大內耗。兩個人在一起工作可能會出現工作精力不能集中的情況。程序員可能會交談一些與工作無關的事情,反而分散註意力,導致效率比單人更為低下。

評價(隊友陳致遠)

  • 優點:
    • 認真負責,輪流編程時的任務完成準時而且質量很高
    • 有探索精神,有遇到無論軟件問題還是算法問題一定要探個究竟
    • 考慮全面,程序無論正確情況方面還是報錯方面都考慮的很細致
  • 缺點:
    • 我們項目經驗都比較少,有些地方都不是很得心應手

結對項目-最長單詞鏈博客