(對對碰)軟工結對做業

最長單詞鏈問題

一、項目github連接

傳送門c++

二、計劃(夢想)中的PSP時間分配和實際(現實)的PSP時間分配

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 60 80
· Estimate · 估計這個任務須要多少時間 14天 12天
Development 開發 9天 7天
· Analysis · 需求分析 (包括學習新技術) 0.5天 1天
· Design Spec · 生成設計文檔 120 150
· Design Review · 設計複審 (和同事審覈設計文檔) 60 60
· Coding Standard · 代碼規範 (爲目前的開發制定合適的規範) 30 50
· Design · 具體設計 1天 1天
· Coding · 具體編碼 5天 5天
· Code Review · 代碼複審 1.5天 1天
· Test · 測試(自我測試,修改代碼,提交修改) 1.5天 1天
Reporting 報告 1天 1天
· Test Report · 測試報告 2小時 4小時
· Size Measurement · 計算工做量 1小時 0.5小時
· Postmortem & Process Improvement Plan · 過後總結, 並提出過程改進計劃 0.5天 0.5天
合計 10天 12天

三、看教科書和其它資料中關於Information Hiding, Interface Design, Loose Coupling的章節,說明大家在結對編程中是如何利用這些方法對接口進行設計的

  • 信息隱藏
    咱們的設計在多個層面作了很好的封裝,很好的保證了各個模塊間的信息隱藏性,將搜索算法邏輯封裝在具體實現內部,向外給出統一的公告接口,將計算邏輯封裝在calculate這一個接口函數中。git

  • 接口設計
    接口設計上首先是一個全局對外接口calculate,咱們詳細分析了實際需求的本質,設計了一個通用接口來知足各類各樣的需求,在數據結構封裝上咱們提供了數據訪問和修改專用的接口,規範數據結構的使用,防止出現隱藏bug。沒有使用任何全局變量,只使用了一些全局宏來輸出log進行debug。對於可能出現的各種優化算法,咱們設計了公共的接口類,規範了統一的算法接口。github

  • 鬆耦合
    在計算邏輯和IO之間鬆耦合,計算邏輯和具體的IO方式無關,設置了規範的數據類型對象和錯誤信息提示對象,很好的完成了鬆耦合。算法

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

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

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);
對於該接口的說明以下:數組

咱們將整個運算邏輯抽象在一個接口內,不一樣的「最長」計算方式使用枚舉類型LongestWordChainType進行描述,head和tail表示單詞鏈的首尾字母,enable_circle表示是否容許輸入中含有環,這樣因此的需求選項均可以被集中在一個接口中,代碼的複用性很是高,且很簡潔。數據結構

咱們將輸入輸出抽象爲string,具體的IO邏輯可能涉及讀取文件,與gui協同,輸入輸出格式等問題,這一部分相對於計算邏輯十分獨立,且較容易發生需求上的變化,故獨立在計算模塊以外,給計算模塊的一概整理成string形式,並用非字母符號來分割單詞,完成了計算和IO的解耦,同時string對象不須要手動維護char*數組,增長了程序的魯棒性。架構

對於異常處理,通常c++編程中是不使用異常的,由於其會對運行效率帶來巨大的影響,一旦拋出異常整個程序的運行時間將大大增長,因此在程序邏輯上,咱們使用自定義的錯誤碼返回值來完成邏輯上的處理,同時創建專門的資源和錯誤信息管理對象handle_error,負責管理資源和錯誤信息記錄app

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

數據結構方面:

咱們將該問題抽象爲一個有向圖,該圖中的結點是26個字母,一個單詞即可以表示爲從首字母到尾字母的一條邊,由問題的特性咱們知道,這張圖中是有自圈(例如awa),多重邊(例如awwb, awb),簡單的使用鄰接矩陣,鄰接表是很難應對這種數據的。咱們抽象了新的「邊」元素,使用WordMapElement類去描述它,對於首字母和尾字母相同的邊,存儲在這一個元素之中,並按照單詞含有的字母數量降序進行排列存儲。而首字母,尾字母根據問題去建立結點,使用特殊的「邊」元素,構建出咱們所用的數據結構。

在具體實現上,咱們使用unordered_map<char, unordered_map<char, WordMapElement> >這樣的c++容器結構去存儲,避免了定長數組帶來的維護性差,可擴展性差的問題,同時又具備直接根據key-value來訪問元素的方便性。

在搜索時,咱們須要存儲當前搜到的最長單詞鏈的相關信息,咱們仿照上文中相似的模式創建相似的數據結構。

算法方面:

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

另外一獨特之處:

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

五、各個實體間關係的UML圖

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

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

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

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

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

  • 能夠發現,當數據規模增長,暴力DFS的時間成本和cpu使用率大幅增長,由此咱們能夠獲得結論,在數據規模小的時候,須要減小變量聲明初始化以及內存分配的相關代碼。而當數據規模很大時,則須要從算法出發減少複雜度,提高性能。
  • 此外, 從算法角度來講,可使用更好的剪枝策略,好比只使用入度爲0的點和環上的點進行搜索,或者採用啓發式搜索策略
    七、看Design by Contract, Code Contract的內容,描述這些作法的優缺點, 說明你是如何把它們融入結對做業中的
    ----------
    契約編程 Design by Contract

這種編程方式的特色是嚴格規定前置條件,後置條件,不變項,好比大二oo課程中就有相似的訓練(JSF),這樣作的好處是嚴格限制了輸入輸出條件,函數可能產生的反作用,從而能夠很好的規範程序接口,同時,基於這種規定,能夠更好地進行單元測試,覆蓋到每個函數和方法的具體細節,使得程序的正確性獲得了更大的保證, 可是與之相對的,就須要開發者付出大量的時間和精力,作很是詳盡的測試,對於工期很緊的項目可能沒法實際操做。我認爲在時間中,能夠選擇部分絕對不能出現問題的核心模塊,使用這種方式進行開發,對於比較邊緣的模塊,並不須要作這麼多,從而讓開發兼顧開發效率和正確性。

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

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

  • 處理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);
        }

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

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

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

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

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三、結對編程的優缺點

優勢:

  • 能夠兩我的交替負責開發和設計,可以有不少討論問題的機會
  • 開局自帶code reviewer,對於代碼質量有很是大的提高
  • 能夠互相給對方的code寫測試,這種開發和測試並行的方式效率很高
    缺點
  • 須要兩我的有必定的公共技術棧,不然相差太遠不少思考方式或者編碼習慣上的問題會阻礙效率
  • 有可能會產生矛盾,甚至是1+1<1

1四、界面模塊,測試模塊和核心模塊的鬆耦合(附加題)

  • 這個部分咱們與 申化文 16231247和肖萌威 16061030兩位同窗交換了GUI模塊和計算模塊,可是因爲是最後一天晚上才交換,以前沒有約定公共的接口格式,致使在輸入輸出數據轉化時須要額外的時間開銷,咱們本來設計的接口接受的輸入是整個字符串,包括了單詞的分割處理。然而對方的處理方式是輸入已經分割完成的單詞數組,輸出也是單詞數組,最終咱們未能在截止時間前完成調用對方GUI的工做。可是咱們的計算模塊成功在他人的GUI跑通,這說明咱們的計算模塊是兼容性比較高的。此次經歷告訴咱們:必定要儘早約定公共接口設計,同時更要儘早完成工做不要壓線趕DDL。
相關文章
相關標籤/搜索