軟件工程第2次做業 | 結對項目-最長單詞鏈

項目 內容
本次做業所屬課程 北航2019軟件工程
本次做業要求 要求詳情
我在本課程的目標 提高工程化思想與能力
本次做業的幫助 初次體驗結對編程,瞭解開發流程

一、Github項目地址

https://github.com/PaParaZz1/longest-word-chainc++

二、PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 20 15
· Estimate · 估計這個任務須要多少時間 20 15
Development 開發 2050 2955
· Analysis · 需求分析 (包括學習新技術) 240 360
· Design Spec · 生成設計文檔 25 20
· Design Review · 設計複審 (和同事審覈設計文檔) 15 15
··Coding Standard · 代碼規範 (爲目前的開發制定合適的規範) 10 10
· Design · 具體設計 60 90
· Coding · 具體編碼 1440 1800
· Code Review · 代碼複審 20 180
· Test · 測試(自我測試,修改代碼,提交修改) 240 480
Reporting 報告 80 110
· Test Report · 測試報告 60 90
· Size Measurement · 計算工做量 10 10
· Postmortem & Process Improvement Plan · 過後總結, 並提出過程改進計劃 10 10
合計 2150 3080

三、Information Hiding, Interface Design, Loose Coupling

  • 在接口設計中,咱們首要在結構在進行設計,要求各個方法之間下降依賴,針對不一樣的參數選擇,選擇不一樣的算法,調用不一樣的函數,而非經過接口進入一個大函數再進行判斷。這樣的設計方法條理更加清晰,而且在對方法增刪修改時更加方便,拿本次工程爲例即主計算模塊僅僅暴露出接口,不管是控制檯程序或是用戶交互界面,均可以經過輸入調用指定方法進行運算,以保證「finishing the task at hand without drawing unnecessary attention to itself.」。隱藏了計算的內部邏輯,加強了計算模塊的可移植性和適用性,確保「each of its components has, or makes use of, little or no knowledge of the definitions of other separate components」

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

咱們在設計計算模塊時使用了自定義的對外接口,接口形式以下git

c++
se_errcode Calculate(const string& input_text, string& output_text, LongestWordChainType& longest_type, const char& head, const char& tail, bool enable_circle, WordChainError& handle_error);

對於該接口的說明以下:github

  • 咱們將整個運算邏輯抽象在一個接口內,不一樣的「最長」計算方式使用枚舉類型LongestWordChainType進行描述,head和tail表示單詞鏈的首尾字母,enable_circle表示是否容許輸入中含有環,這樣因此的需求選項均可以被集中在一個接口中,代碼的複用性很是高,且很簡潔。
  • 咱們將輸入輸出抽象爲string,具體的IO邏輯可能涉及讀取文件,與gui協同,輸入輸出格式等問題,這一部分相對於計算邏輯十分獨立,且較容易發生需求上的變化,故獨立在計算模塊以外,給計算模塊的一概整理成string形式,並用非字母符號來分割單詞,完成了計算和IO的解耦,同時string對象不須要手動維護char*數組,增長了程序的魯棒性。
  • 對於異常處理,通常c++編程中是不使用異常的,由於其會對運行效率帶來巨大的影響,一旦拋出異常整個程序的運行時間將大大增長,因此在程序邏輯上,咱們使用自定義的錯誤碼返回值來完成邏輯上的處理,同時創建專門的資源和錯誤信息管理對象handle_error,負責管理資源和錯誤信息記錄

關於計算邏輯的實現,咱們認爲這裏主要的兩部分是數據結構和算法,接下來咱們會對這兩方面分開進行說明。算法

數據結構方面:編程

  • 咱們將該問題抽象爲一個有向圖,該圖中的結點是26個字母,一個單詞即可以表示爲從首字母到尾字母的一條邊,由問題的特性咱們知道,這張圖中是有自圈(例如awa),多重邊(例如awwb, awb),簡單的使用鄰接矩陣,鄰接表是很難應對這種數據的。咱們抽象了新的「邊」元素,使用WordMapElement類去描述它,對於首字母和尾字母相同的邊,存儲在這一個元素之中,並按照單詞含有的字母數量降序進行排列存儲。而首字母,尾字母根據問題去建立結點,使用特殊的「邊」元素,構建出咱們所用的數據結構。
  • 在具體實現上,咱們使用unordered_map<char, unordered_map<char, WordMapElement> >這樣的c++容器結構去存儲,避免了定長數組帶來的維護性差,可擴展性差的問題,同時又具備直接根據key-value來訪問元素的方便性。
  • 在搜索時,咱們須要存儲當前搜到的最長單詞鏈的相關信息,咱們仿照上文中相似的模式創建相似的數據結構。

算法方面:數組

  • 因爲該問題自己是一個NP-Complete問題,因此會有不少的剪枝優化和啓發式搜索算法,那麼從設計框架角度,咱們須要一個可擴展性很高的搜索框架,而不是把算法耦合在總體邏輯中,因此,咱們設計一個通用的搜索接口SearchInterface,定義了公共的接口方法SearchLookUp,任何搜索算法只要繼承接口類重寫這兩個方法便可,其內部的算法優化邏輯將封裝在方法內部,與外部的邏輯無關,這樣能夠很方便的添加優化算法,同時保證架構設計的完整性。

另外一獨特之處:數據結構

  • 在做業文檔給出的接口中,對於單詞數最長和字母數最長,分別設計了兩個接口,可是咱們認爲本質上來說,這只是兩種不一樣的「長度」度量而已,從實現來講只有計算長度時,每條邊對應的長度不一樣這一點差別,因此咱們的設計將其統一在一塊兒,若是將來有新的需求,好比說「其中含字母a的個數最多」,只須要添加新的長度計算方式便可,而不用改動總體邏輯。

五、UML圖

  • 使用visual studio自帶的插件生成的uml圖以下:

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

  • 計算模塊的性能上,通過visual studio 2017自帶的性能探查器分析,咱們獲得了以下的效能分析結果:

  • 能夠發現,效能瓶頸主要存在於ChainSearch方法中,new一個對象時分配內存進行初始化,佔用了大量cpu資源。另外在Search和LookUp方法中大量的搜索等操做也是效能瓶頸之一。

  • 一樣的,在CheckCircle方法中,變量的聲明也佔用了大量cpu資源。說明爲了提高計算模塊的性能,咱們須要在這幾個方面加以改進。

  • 須要注意的是,上述效能分析使用的數據規模並不大,因此咱們猜想隨着數據規模的增大,DFS方法的cpu使用率應該會顯著增長,因此咱們又進行了大數據規模(約9000詞)的測試,結果以下:

  • 能夠發現,當數據規模增長,暴力DFS的時間成本和cpu使用率大幅增長,由此咱們能夠獲得結論,在數據規模小的時候,須要減小變量聲明初始化以及內存分配的相關代碼。而當數據規模很大時,則須要從算法出發減少複雜度,提高性能。

七、Design by Contract, Code Contract

Design by contract (DbC), also known as contract programming, programming by contract and design-by-contract programming, is an approach for designing software. It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.
引用自wikipedia架構

  • 所謂Design by Contract, 簡單來講就是在結對編程的過程當中,咱們須要同伴間制定一個規範,主要針對類以及類的方法,防止在執行代碼先後發生咱們不期待的結果,其中就要求咱們明確執行前對傳入參數的要求,執行中什麼是不能改變的量,執行後期待返回什麼結果,這樣作的優勢是對於代碼的編寫者和使調用者提出要求,編寫者須要保證代碼運行過程當中可以輸出正確的結果,調用者須要保證提供正確無誤的參數,雙方都有必須履行的義務,也有使用的權利,這樣就保證了雙方代碼的質量,提升了軟件工程的效率和質量。缺點在於這無形間提升了對編碼素質的要求,增長了編寫時間,而且契約式編程對程序語言有必定的要求,在咱們的項目中,明確了函數接口的時候,肯定了哪些量是不能變的,那些量是須要返回的,而且在單元測試中使用Assert斷言進行了驗證。

八、計算模塊部分單元測試展現

  • 計算模塊部分的單元測試,咱們的測試思路是從基本類開始測試,而後測試基本類的方法,接着測試使用到這個類的方法,由小到大以保證單元測試的正確性。

  • 處理I/O的函數之一ExtractWord(),測試數據構造思路在於構造出由不一樣的字符分割的單詞,包括沒有單詞的狀況,將分離結果與預設結果逐個對拍。
TEST_METHOD(Test_ExtractWord)
        {
            //TEST  ExtractWord
            WordChainError error1;
            string input_text1 = "_this is      a!@#$test of0extract!word...... ";
            vector<string>  input_buffer1;
            vector<string> result1 = { "this","is","a","test","of","extract","word" };
            ExtractWord(input_text1, input_buffer1,error1);
            Assert::AreEqual(result1.size(), input_buffer1.size());
            for (int i = 0; i < result1.size(); i++) {
                Assert::AreEqual(result1[i], input_buffer1[i]);
                }

            WordChainError error2;
            string input_text2 = "_this___is___another======test of extract[][][]word. ";
            vector<string>  input_buffer2;
            vector<string> result2 = { "this","is","another","test","of","extract","word" };
            ExtractWord(input_text2, input_buffer2,error2);
            Assert::AreEqual(result2.size(), input_buffer2.size());
            for (int i = 0; i < result2.size(); i++) {
                Assert::AreEqual(result2[i], input_buffer2[i]);
            }

            WordChainError error3;
            string input_text3 = "_[][][]....";
            vector<string>  input_buffer3;
            vector<string> result3 = { };
            ExtractWord(input_text3, input_buffer3,error3);
            Assert::AreEqual(result3.size(), input_buffer3.size());
            for (int i = 0; i < result3.size(); i++) {
                Assert::AreEqual(result3[i], input_buffer3[i]);
            }
        }
  • Word類的基本單元測試,主要驗證其構建方法和其內部方法的正確性,構造數據包括單個字母的狀況和多個字母的狀況。
TEST_METHOD(Test_Class_Word)
        {
            //TEST  Class_Word
            Word test1 = Word("a");
            Assert::AreEqual(test1.GetHead(), 'a');
            Assert::AreEqual(test1.GetTail(), 'a');
            Assert::AreEqual(test1.GetWord(), string("a"));
            Assert::AreEqual(test1.GetKey(), string("aa"));

            Word test2 = Word("phycho");
            Assert::AreEqual(test2.GetHead(), 'p');
            Assert::AreEqual(test2.GetTail(), 'o');
            Assert::AreEqual(test2.GetWord(), string("phycho"));
            Assert::AreEqual(test2.GetKey(), string("po"));
        }
  • DistanceElement類內部方法的單元測試,構造數據中驗證對其操做前後順序的影響是否知足需求,以及基本方法的正確性驗證。
TEST_METHOD(Test_Class_DistanceElement_Method)
        {
            //TEST  Class_DistanceElement_Method: SetDistance/GetDistance/SetWordChain/CopyWordBuffer/ToString
            LongestWordChainType type1 = letter_longest;
            DistanceElement testElement1 = DistanceElement(type1);
            Assert::AreEqual(testElement1.GetDistance(), 0);
            vector<string> input1 = { "a","test","of","it" };
            vector<string> output1;
            testElement1.SetWordChain(input1);
            testElement1.CopyWordBuffer(output1);
            for (int i = 0; i < input1.size(); i++) {
                Assert::AreEqual(output1[i], input1[i]);
            }
            testElement1.SetDistance(6);
            Assert::AreEqual(testElement1.GetDistance(), 6);
            Assert::AreEqual(testElement1.ToString(), string("a-test-of-it"));

            LongestWordChainType type2 = word_longest;
            DistanceElement testElement2 = DistanceElement(type2);
            Assert::AreEqual(testElement2.GetDistance(), 0);
            vector<string> input2 = { "another","test","of","it" };
            vector<string> output2;
            testElement2.SetWordChain(input2);
            testElement2.CopyWordBuffer(output2);
            for (int i = 0; i < input2.size(); i++) {
                Assert::AreEqual(output2[i], input2[i]);
            }
            testElement2.SetDistance(2);
            Assert::AreEqual(testElement2.GetDistance(), 2);
            Assert::AreEqual(testElement2.ToString(), string("another-test-of-it"));
        }
  • 將某個方法內部的方法所有驗證事後,就能夠對調用其的方法進行單元測試,下面展現的是計算模塊的總體調用,返回值爲計算結果,數據構造上考慮到了是否有環,是否有頭尾字母的要求等,將計算方法返回結果與正確結果比對。
TEST_METHOD(Test_Calculate)
        {
            //Test Calculate: include CalculateLongestChain/ChainSearch
            WordChainError error;
            string input_text ="Algebra))Apple 123Zoo Elephant  Under  Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism";
            string output_text1 = "";
            LongestWordChainType type1 = word_longest;
            Calculate(input_text, output_text1, type1, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, false,error);
            string result1 = "algebra\napple\nelephant\ntrick\n";
            Assert::AreEqual(result1, output_text1);
            string output_text2 = "";
            LongestWordChainType type2 = letter_longest;
            Calculate(input_text, output_text2, type2, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, false,error);
            string result2 = "pseudopseudohypoparathyroidism\nmoon\n";
            Assert::AreEqual(result2, output_text2);
            string input_text_ring = "Algebra))Apple aaaaa 123Zoo Elephant  Under  Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism";
            string output_text3 = "";
            string result3 = "algebra\naaaaa\napple\nelephant\ntrick\n";
            LongestWordChainType type3 = word_longest;
            Calculate(input_text_ring, output_text3, type3, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, true, error);
            Assert::AreEqual(result3, output_text3);
        }
  • 有關覆蓋率,咱們時間緊張致使沒可以徹底弄明白coveragec插件的使用方法,可是咱們的測試是從基本類開始一步一步往外測試,每一種狀況都有相應的測試用例,咱們有信心本身達到了90%以上的覆蓋率,若是做業有需求,咱們能夠再去從新測試,請相信本次只是對於插件的不熟悉,絕對沒有忽視單元測試的重要性。

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

  • 異常設計上,咱們設計了7種異常
    • 重複單詞異常
    • 環異常
    • 輸入文件異常
    • 輸出文件異常
    • 命令行參數異常
    • 計算模式異常
    • 無結果異常
  • 其中重複單詞異常會在命令行輸出,可是不會影響程序的進行,計算模式異常和命令行參數異常均爲在對命令行進行解析時發生的異常,咱們並無單獨爲其寫一個方法,因此難以在單元測試中驗證,僅會在命令行輸出錯誤信息。app

  • 其他四種異常均在單元測試中進行了驗證。框架

  • 輸入文件異常,對應找不到輸入文件的場景等:

std::ifstream in("notexist.txt");
            std::stringstream buffer1;
            WordChainError error3;
            if (!in.is_open()) {
                char buffer1[MAX_BUFFER_SIZE];
                sprintf(buffer1, "Error Type: can't open input file\n");
                string error_content(buffer1);
                int error_code = SE_ERROR_OPENING_INPUT_FILE;
                error3.AppendInfo(error_code, error_content);
            }
            string errortext3 = error3.ToString();
            Assert::AreEqual(errortext3, string("Error Type: can't open input file\nError Content: Error Type: can't open input file\n"));
  • 輸出文件異常,對應輸出文件被意外關閉的場景等:
std::stringstream buffer2;
            std::ofstream out("close.txt");
            WordChainError error4;
            out.close();
            if (!out.is_open()) {
                char buffer2[MAX_BUFFER_SIZE];
                sprintf(buffer2, "Error Type: can't open output file\n");
                string error_content(buffer2);
                int error_code = SE_ERROR_OPENING_OUTPUT_FILE;
                error4.AppendInfo(error_code, error_content);
            }
            string errortext4 = error4.ToString();
            Assert::AreEqual(errortext4, string("Error Type: can't open output file\nError Content: Error Type: can't open output file\n"));
  • 環異常,對應未選擇-r選項可是輸入單詞可成環的場景:
WordChainError error1;
            string input_text1 = "Algebra))Apple aaaaa 123Zoo Elephant  Under  Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism";
            string output_text1 = "";
            string errortext1;
            LongestWordChainType type1 = word_longest;
            Calculate(input_text1, output_text1, type1, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL,false, error1);
            errortext1 = error1.ToString();
            Assert::AreEqual(errortext1,string("Error Type: input has circle but not enable circle\nError Content: Error Type: input has circle but not enable circle\n"));
  • 無結果異常,對應根據選擇的計算模式和頭尾字母要求,沒有對應結果的場景:
WordChainError error2;
            string input_text2 = "Algebra Zoo";
            string output_text2 = "";
            string errortext2;
            LongestWordChainType type2 = word_longest;
            Calculate(input_text2, output_text2, type2, 'i', NO_ASSIGN_TAIL, false, error2);
            errortext2 = error2.ToString();
            Assert::AreEqual(errortext2, string("Error Type: no available word chain\nError Content: no available word chain for head(i) and tail(0)\n"));

十、界面模塊的詳細設計過程

  • 界面模塊咱們使用了Qt的庫進行了設計。編碼上仍然是c++語言,ui設計上使用了Qt Creator進行設計。

  • 首先進行界面組件需求分析:
    • 兩種導入文本的方式
    • 交互式按鈕,分別是五個功能選項
    • 異常狀況界面提示
    • 正確結果界面顯示
    • 導出結果,保存到文件
    • 使用說明
  • 以上的需求能夠大概代表咱們的用戶界面須要至少五個參數選擇的交互按鈕,兩個界面,其中一個負責寫入文本,一個負責顯示正確結果和錯誤信息。另外須要四個按鈕,分別對應導入文本,運行程序,導出結果和顯示使用說明。

  • 明確了以上需求以後,咱們在Qt Creator中設計了大概的用戶界面(macOS下):

  • 其中使用radiobutton選擇兩種計算方式,checkbox選擇是否容許單詞環,下拉框選擇是否有開頭和結尾字母的要求,這些設計都是爲了方便用戶的使用。

  • 如下爲部分用戶界面的代碼:

//按鈕觸發事件(引入文件以及顯示幫助信息)
void MainWindow::on_pushButton_import_clicked()
{
    QString fileName=QFileDialog::getOpenFileName(this,tr("Choose File"),"",tr("text(*.txt)"));
    QFile file(fileName);
    if(!file.open(QFile::ReadOnly|QFile::Text)){
        QString errMsg="error when import file";
        ui->outputArea->setText(errMsg);
        return;
    }
    QTextStream in(&file);
    ui->inputArea->clear();
    ui->inputArea->setText(in.readAll());
}

void MainWindow::on_pushButton_help_clicked()
{
    dialog = new Dialog(this);
    dialog->setModal(false);
    QString helpMsg="test help";
    dialog->ui->textBrowser->setPlainText(helpMsg);
    dialog->show();
}

十一、界面模塊與計算模塊的對接

  • 模塊對接方面,主要是經過接口函數(做業要求中的Core)進行計算,其中各個參數的值是經過界面模塊的控件傳入的,例如radiobutton控制的值爲-w選項或-c選項,checkbox傳入單詞環的布爾值,combobox傳入是否有-h,-t選項以及對應的字符,輸入框傳入文本或者從文件讀入的內容,界面模塊以下(window下)。

  • 對接的過程主要體如今運行程序按鈕上,咱們爲其綁定了事件調用core的對應函數,即Calculate(content, output,type,head,tail,ring),代碼以下:
void MainWindow::on_pushButton_run_clicked()
{
    int para=ui->radioButton_w->isChecked()?1:2;
    bool ring=ui->checkBox_loop->isChecked();
    string content = ui->inputArea->toPlainText().toStdString();
    char head, tail;
    if (ui->comboBox_h->currentIndex() == 0) {
        head = '\0';
    }
    else {
        head = 'a' + ui->comboBox_h->currentIndex() - 1;
    }
    if (ui->comboBox_t->currentIndex() == 0) {
        tail = '\0';
    }
    else {
        tail = 'a' + ui->comboBox_t->currentIndex() - 1;
    }
    if(content.size()==0){
        QString errMsg="empty input!";
        ui->outputArea->setPlainText(errMsg);
    }
    else{
        //call corresponding function
        string output;
        LongestWordChainType type;
        se_errcode code;
        QString s = "fin";
        WordChainError  error;
        if (para == 1) {
            type = word_longest;
            code=Calculate(content, output,type,head,tail,ring,error);
        }
        else {
            type = letter_longest;
            code=Calculate(content, output, type, head, tail, ring,error);
        }
        if (code == SE_OK) {
            QString result = QString::fromStdString(output);
            ui->outputArea->setPlainText(result);
        }
        else {
            string result = error.ToString();
            QString error= QString::fromStdString(result);
            ui->outputArea->setPlainText(error);
        }
    }
    //cout<<"onclick_run"<<endl;
}

功能運行結果以下:

  • 導入文件:

  • 參數選擇1:

  • 運行結果1:

  • 參數選擇2及運行結果:

  • 導出結果:

  • 錯誤提示:

十二、結對的過程

1三、結對編程以及我的的優勢和缺點

  • 結對編程的優勢在於首先在代碼質量上,取決於水平較高的那一位,而且兩我的一塊兒工做,能夠提升各自的效率和工做能力,實力較差的那一方還能夠從另外一方學習到新的知識和技能,明確編碼規範,以提高本身的工程化思想。分工合做同時也可以讓每一個人專一於本身的職責。而且在編碼過程當中,兩我的會產生更多的思惟碰撞和交流,有利於項目的推動和維護。結對編程的缺點在於因爲代碼是兩我的各自寫的,因此在風格和方法上會有差別,每一個人的理解上也會存在誤差,須要及時的交流。另外假若其中一方不能保證足夠的合做時間,無疑會拖慢項目的進度。

  • 結對過程當中我同伴的優勢是:一、代碼十分規範,風格良好,使我學到了不少。二、在項目開始的時候很快的明確了數據類型和接口定義,爲項目推動打好了基礎。三、編寫代碼效率很高,而且還能及時解答我不瞭解或不熟悉的代碼問題。缺點因爲同伴日常很忙,是在一塊兒編程的時間比較少。個人優勢在於:一、因爲新建工程,單元測試,gui方面都是我主要負責,碰到了不少配環境,引入文件的問題,遇到困難可以快速的查詢解決方法而且解決。二、按時完成要求的開發任務和測試任務。三、努力學習了命名規範和代碼風格,爭取統一。缺點在於編碼能力不強,對c++語言仍是不熟悉,須要很認真的閱讀代碼和查詢資料才能看明白每一個函數的用處。

1四、界面模塊,測試模塊和核心模塊的鬆耦合

  • 這個部分咱們與 申化文 16231247和肖萌威 16061030兩位同窗進行了交換,很是惋惜的是,因爲咱們是自行設計的接口,GUI也調用的是本身的函數,未能在最後封裝後修改徹底部代碼。咱們本來設計的接口接受的輸入是整個字符串,包括了單詞的分割處理。然而Core.dll中的輸入是已經分割完成的單詞數組,輸出也是單詞數組。而咱們的輸出是一個字符串,修改的過程無疑給咱們增長了很大難度,沒可以按時完成。可是咱們的Core.dll成功在他人的GUI跑通,這也給咱們一個提醒,要今早完成要求工做,纔有足夠的時間測試和調試。

1五、自我反思

  • 因爲本身的問題沒可以完成耦合的,很是對不起個人搭檔,在接下來的工程中,我會對本身要求更加嚴格,多與團隊成員進行溝通,明確代碼規範。很是感謝本次結對做業,夥伴對個人包容和指教!
相關文章
相關標籤/搜索