OOP課第一階段總結

郑雅斌發表於2024-04-21

前三次OOP作業總結Blog

  • 前言

    • 作為第一次3+1的總結,這次題目集的難度逐漸升高,題量、閱讀量和測試點的數量變化都很大,所以對我們的程式設計和理解能力提出了更高的要求。例如在第一次到第三次的題目集中,類的數量由三個增長到了十餘個。投入的時間也由最開始的45個小時到了後來的1824小時。這個過程從主觀來說非常的痛苦,但是好在並不是全無收穫。

    • 在這三週的時間裡存在很多的問題,一開始根本不懂得資料如何去儲存,也不知道類如何去使用。第一階段的題目集中非常著重的考察了正規表示式的使用,這一特點在第三次題目集中尤為顯著。這也意味著很多東西都需要從0開始,這個從無到有的過程才是對我們能力的訓練。

    • 在第三次題目集中測試點也增長到了前所未有的28個,情況非常的複雜,而且存在一個非常明顯的現象。在題目集開放至第三天的時候,達到滿分的只有寥寥幾人,其餘很多人都停留在了94、97分的階段,300多人提交了一萬多份答卷,而透過率卻只有堪堪1%。造成這一現象的主要原因是一些特殊測試點的存在,比如“答案為空字元”、“空白卷”等測試點成為了學生去往滿分的路上最大的攔路虎。這說明,在設計程式碼的時候,並沒有考慮到一些非常極端的情況,這是一個很大的缺陷。為了解決這些問題,我們陷入了非常痛苦的迴圈之中:

      不斷地用額外的測試點進行除錯

      不斷地詢問別人遇到的問題

      不斷地修改已經提交了幾十遍的程式碼

  • 正文

    • 第一次題目集

      這次題目集難度倒是不高,但是對於接觸物件導向程式設計不久的小萌新來說還是頗具挑戰:

      題目主幹:

      設計實現答題程式,模擬一個小型的測試,要求輸入題目資訊和答題資訊,根據輸入題目資訊中的標準答案判斷答題的結果。

      其中需要處理的資料一共只有兩個型別:

      題目內容 "#N:"+題號+" "+"#Q:"+題目內容+" "#A:"+標準答案

      答題資訊 "#A:"+答案內容

      這裡需要使用正規表示式去分割各個資訊的不同部分,並且儲存起來

      UML類圖:

      可以看到一共設計了三個類,分別是AnswerSheet,ExamPaper,Question

      簡單展示一下題目集一的複雜度:

      複雜度:

      Method CogC ev(G) iv(G) v(G)
      AnswerSheet.AnswerSheet() 0 1 1 1
      AnswerSheet.getAnswer(int) 0 1 1 1
      AnswerSheet.saveAnswer(String) 0 1 1 1
      ExamPaper.ExamPaper() 0 1 1 1
      ExamPaper.getQuestionContent(int) 0 1 1 1
      ExamPaper.judgeAnswer(int, String) 0 1 1 1
      ExamPaper.saveQuestion(int, String, String) 0 1 1 1
      ExamPaper.sortByNumber() 0 1 1 1
      Main.main(String[]) 11 1 7 9
      Question.Question(int, String, String) 0 1 1 1
      Question.getContent() 0 1 1 1
      Question.getNumber() 0 1 1 1
      Question.getStandardAnswer() 0 1 1 1
      Question.judgeAnswer(String) 0 1 1 1

      從這個表格中可以看出Main的複雜度比較高,主要是因為資料的讀入和輸出都是在主函式里面完成的:

      public class Main {
          public static void main(String[] args) {
              Scanner scanner = new Scanner(System.in);
              int questionCount = Integer.parseInt(scanner.nextLine());
              ExamPaper examPaper = new ExamPaper();
              for (int i = 0; i < questionCount; i++) {
                  String input = scanner.nextLine().trim();
                  String[] parts = input.split("#");
                  int number = Integer.parseInt(parts[1].split(":")[1].trim());
                  String content = parts[2].split(":")[1].trim();
                  String standardAnswer = parts[3].split(":")[1].trim();
                  examPaper.saveQuestion(number, content, standardAnswer);
              }
              AnswerSheet answerSheet = new AnswerSheet();
              String answerInput = scanner.nextLine().trim();
              while (!answerInput.equals("end")) {
                  String[] answers = answerInput.split("\\s+");
                  for (String answer : answers) {
                      answerSheet.saveAnswer(answer);
                  }
                  answerInput = scanner.nextLine().trim();
              }
              examPaper.sortByNumber();
              for (int i = 1; i <= questionCount; i++) {
                  String questionContent = examPaper.getQuestionContent(i);
                  String answer = answerSheet.getAnswer(i);
                  System.out.println(questionContent + "~" + answer.replace("#A:",""));
              }
              for (int i = 1; i <= questionCount; i++) {
                  String answer = answerSheet.getAnswer(i);
                  boolean result = examPaper.judgeAnswer(i, answer.replace("#A:",""));
                  System.out.print(result ? "true" : "false");
                  if(i>=1 && i!=questionCount){
                      System.out.print(" ");
                  }
              }
          }
      }
      
    • 第二次題目集

      第二次題目集我完成的不好,這是顯而易見的,因為越來越多的資料需要處理,讓我一個對陣列的使用很是生疏的初學者較為棘手,所以這次題目及的程式碼就已經寫成一坨亂麻了,非常的難看。

      問題主要出現在對於試卷資訊的處理上:

      試卷資訊

      格式:"#T:"+試卷號+" "+題目編號+"-"+題目分值

      由於這次新增了試卷資訊,需要給出得分,題目分值和題目編號一一對應,正常來說需要使用到HashMap這樣的對映關係,將題目編號和題目分值以對映的關係儲存在一起,便可以透過題目編號尋找到相應的分值。但是當時我鐵了心的要用List實現,後來發現非常的困難,但是我對於HashMap的使用非常的生疏,所以也沒有及時的更改我的思路,導致最後的程式碼修修補補,毫無邏輯可言。

      UML類圖:

      主函式非常的複雜,屬於是劍走偏鋒,兩百多行的程式碼,主函式就佔了半壁江山:

      複雜度:

      Class OCavg OCmax WMC
      AnswerQuestion 2.2 3 11
      AnswerSheet 1.25 2 5
      ExamPaper 1 1 5
      Main 28 28 28
      Question 1 1 5
      Score 1 1 4
      Method CogC ev(G) iv(G) v(G)
      AnswerQuestion.AnswerQuestion() 0 1 1 1
      AnswerQuestion.getExamQuestionList(int) 3 1 3 3
      AnswerQuestion.isFullScore(int) 3 1 3 3
      AnswerQuestion.isPaperExist(int) 3 3 2 3
      AnswerQuestion.saveExamQuestion(int, int, int) 0 1 1 1
      AnswerSheet.AnswerSheet() 0 1 1 1
      AnswerSheet.getAnswer(int) 1 2 2 2
      AnswerSheet.getAnswers() 0 1 1 1
      AnswerSheet.saveAnswer(String) 0 1 1 1
      ExamPaper.ExamPaper() 0 1 1 1
      ExamPaper.getQuestionContent(int) 0 1 1 1
      ExamPaper.judgeAnswer(int, String) 0 1 1 1
      ExamPaper.saveQuestion(int, String, String) 0 1 1 1
      ExamPaper.sortByNumber() 0 1 1 1
      Main.main(String[]) 72 5 27 29
      Question.Question(int, String, String) 0 1 1 1
      Question.getContent() 0 1 1 1
      Question.getNumber() 0 1 1 1
      Question.getStandardAnswer() 0 1 1 1
      Question.judgeAnswer(String) 0 1 1 1
      Score.Score(int, int, int) 0 1 1 1
      Score.getPaperNumber() 0 1 1 1
      Score.getQuestionScore() 0 1 1 1
      Score.getQuestionSequence() 0 1 1 1

      我看到這個資料的時候也是感覺到了非常的誇張,基本上成為了主函式戰士,一個人把其他類的活幹了一半。

      還是看一下資料的輸入部分吧:

      資料輸入:

      while (true) {
                  String input = scanner.nextLine().trim();
                  if (input.equals("end")) {
                      break;
                  }
                  String[] parts = input.split("#");
                  String types = parts[1].split(":")[0].trim();
                  if (types.equals("N")) {
                      int questionNumber = Integer.parseInt(parts[1].split(":")[1].trim());
                      String content = parts[2].split(":")[1].trim();
                      String standardAnswer = parts[3].split(":")[1].trim();
                      examPaper.saveQuestion(questionNumber, content, standardAnswer);
                  }
                  if (types.equals("S")) {
                      String[] answerParts = input.trim().split("\\s+");
                      int answerNumber = Integer.parseInt(answerParts[0].replace("#S:", "").trim());
                      sNumbers.add(answerNumber);
                      AnswerSheet answerSheet = new AnswerSheet();
                      for (String answerPart : answerParts) {
                          if (answerPart.contains("A")) {
                              answerSheet.saveAnswer(answerPart.replace("#A:", "").trim());
                          }
                      }
                      answerSheets.add(answerSheet);
                  }
                  if (types.equals("T")) {
                      String[] paperParts = input.trim().split("\\s+");
                      int paperNumber = Integer.parseInt(paperParts[0].replace("#T:", "").trim());
                      tNumbers.add(paperNumber);
                      tCount++;
                      for (String paperPart : paperParts) {
                          if (paperPart.contains("-")) {
                              answerQuestion.saveExamQuestion(paperNumber, Integer.parseInt(paperPart.split("-")[0].trim()), Integer.parseInt(paperPart.split("-")[1].trim()));
                          }
                      }
                  }
              }
      

      在這裡使用了一個while迴圈來不斷地讀取每一行的資料,當遇到end的時候便直接退出迴圈。這段程式碼對資料的分割都是透過split完成的,而不是使用正規表示式,對於這道題來說,前者的功能堪堪夠用,但是對於第三次題目集來說就有一點狗騎呂布的感覺了。😰

    • 第三次題目集

      這是我需要著重來說明的一次題目集,經過前兩次的進化,這次的題目已經來到了慘無人道的地步,28個測試點等著我去擊破。由於第二次題目集上糟糕的表現,我這次完全從頭開始,設計好了每一個類承擔的功能,但是主函式戰士的我還是把主類寫到了200行()。

      本次題目集加入了很多功能:

      學生資訊 "#X:"+學號+" "+姓名+"-"+學號+" "+姓名....+"-"+學號+" "+姓名

      答卷資訊 "#S:"+試卷號+" "+學號+" "+"#A:"+試卷題目的順序號+"-"+答案內容+...

      刪除題目資訊 "#D:N-"+題目號

      錯誤提示資訊

      錯誤格式判斷

      一開始我還是一如既往的使用split對資料進行分割處理,但是發現有著很大的漏洞:

      錯誤格式判斷怎麼解決???

      這下是真的逃不掉正規表示式的使用了,花了一個晚自習上網學習正規表示式的使用,然後才磕磕絆絆的開始寫錯誤格式判斷的程式碼,每寫一次都要用很多個資料去測試我寫的正規表示式是否存在漏洞:

      錯誤格式判斷:

      String regexOfN = "#N:\\d+ #Q:(.+) #A:(.+)*";
                  String regexOfT = "#T:\\d+( \\d+-\\d+)+";
                  String regexOfX = "#X:\\d+(\\w+ \\w+)+(-\\w+ \\w+)*";
                  String regexOfS = "#S:\\d+ \\w+( #A:(\\w+)-(.*)*)*";
                  String regexOfD = "#D:N-\\d+";
                  boolean flag=input.matches(regexOfN)||input.matches(regexOfT)||input.matches(regexOfX)||input.matches(regexOfS)||input.matches(regexOfD);
      

      這麼一點程式碼,卻是整道題含金量最高的地方之一(個人觀點),畢竟花費了非常多的時間去除錯這段程式碼,也有很多測試點涉及到了這部分的功能。一共有12個測試點需要進行錯誤格式判斷,幾乎就是整道題一半的分值都被這段程式碼所左右了。

      然後由於題目新增和很多新的資料資訊,各個資料之間的關係也變得非常複雜,有一點很噁心的地方是,在學生的答卷資訊中的題號居然是試卷資訊中的順序號(人麻了,問題在於真的有學生不按照順序答題嗎)。

      其次對於使用List還是使用HashMap我思考了很久。顯而易見的是,這道題目有很多地方使用後者是更方便的,因為資訊中存在很多一一對映的關係,使用HashMap非常適合用來處理這樣的關係,但是筆者可能比較倔強,前兩個題目集用的是List那我就繼續用,筆者的室友覺得這簡直不敢相信會有多麼複雜,但筆者就要證明List肯定沒有問題。

      用到的List:

              ArrayList<AllAnswerSheet> allAnswerSheetArrayList=new ArrayList<>();
              ArrayList<AnswerSheet> answerSheetArrayList=new ArrayList<>();
              ArrayList<AllPaper> allPaperArrayList=new ArrayList<>();
              ArrayList<Question> questionArrayList=new ArrayList<>();
              ArrayList<Paper> paperArrayList=new ArrayList<>();
              ArrayList<Student> studentArrayList=new ArrayList<>();
              ArrayList<DeleteQuestion> deleteQuestionArrayList=new ArrayList<>();
              ArrayList<AllStudentScore> allStudentScoreArrayList=new ArrayList<>();
              ArrayList<Score> scoreArrayList=new ArrayList<>();
      

      UML類圖:

      這次一共用到了十個類,但是實際上可以有更多,其中有一些類比較特殊,比如AllAnswerSheetAnswerSheet它們之間存在一種整體與部分之間的關係,前者用於儲存所有學生的答題情況,後者用於儲存單個學生物件的答題情況。

      為什麼要這樣做呢?

      主要是因為,每個學生的答題數量可能不一致。所以肯定不能以統一的格式去保留每個學生的答題情況,因此只能讓每一個學生物件的答題情況先單獨儲存起來,再用一個陣列去儲存每一張答卷。效果有點類似於:

      效果圖:

      這樣子就可以用兩個List實現類似於HashMap的效果,並且可以處理每個學生答題數量不一致的情況。

      --------------------------以下是一些投機取巧(可以略過)-----------------------------

      1. 在刪除題目時,筆者直接將需要刪除的題目的標準答案改成“???”,這樣在遍歷的時候當檢測到標準答案為“???”的時候,就可以知道應該輸出題目被刪除的警示資訊了。

      2. 依然是在刪除題目時,筆者直接將刪除的題目的題目內容改成“the question "+題號+" invalid~0”,這樣當遇到第一條的情況時,我不需要寫額外的輸出,可以直接按照題目原有的順序輸出警示資訊啦。

        for(int i=0;i<deleteQuestionArrayList.size();i++){
                    for(int j=0;j<questionArrayList.size();j++){
                        if(questionArrayList.get(j).getQuestionNumber()==deleteQuestionArrayList.get(i).getDeleteQuestionNumber()){
                            questionArrayList.get(j).setQuestionContent("the question "+questionArrayList.get(j).getQuestionNumber()+" invalid~0");
                            questionArrayList.get(j).setAnswerStandard("???");
                        }
                    }
                }
        
      3. 在讀入答題資訊的時候,可能會遇到這樣一種極端的情況:

        極端情況:#S:1 20201103

        答卷資訊只有試卷號和學號,這樣的話在儲存題號以及答案的時候將會是空的,便會出現bug。所以,當檢測到不存在任何一個答題的時候,就存入“#A:0-??”這樣一段資訊,由於題目號不可能為0,所以當便遍歷到這張答卷的時候直接就會輸出答案不存在的警示資訊。

        Pattern pattern=Pattern.compile("#S:(\\d+) (\\w+)( #A:(\\w+)-(.*))*");
                            Matcher matcher=pattern.matcher(input);
                            if(matcher.find()){}
                            int answerNumber=Integer.parseInt(matcher.group(1));
                            String studentID= matcher.group(2);
                            String[] answerParts;
                            if(matcher.group(3)==null){
                                answerParts="#A:0-??".split("#A:");
                            }else answerParts=matcher.group(3).split("#A:");
                            for(int i=1;i<answerParts.length;i++){
                                String studentAnswer;
                                if(answerParts[i].split("-").length>1){
                                    studentAnswer=answerParts[i].split("-")[1].trim();
                                }else studentAnswer="";
                                //System.out.println(Integer.parseInt(answerParts[i].split("-")[0]));
                                answerSheetArrayList.add(new AnswerSheet(Integer.parseInt(answerParts[i].split("-")[0]),studentAnswer));
                            }
        

        看到這裡可能就覺得筆者耍了太多小聰明瞭,但是其實這裡面的每一步都是為了解決一個極端情況(嗚嗚嗚)。

        --------------------------以上是一些投機取巧(可以略過)---------------------------

        現在簡單的來看一下第三次題目集的程式碼質量吧(慘不忍睹):

        複雜度:

        Class OCavg OCmax WMC
        AllAnswerSheet 1 1 9
        AllPaper 1 1 7
        AllStudentScore 1 1 8
        AnswerSheet 1 1 6
        DeleteQuestion 1 1 4
        Main 25 49 50
        Paper 1 1 6
        Question 1 1 8
        Score 1 1 4
        Student 1 1 6
        Package v(G)avg v(G)tot
        1.88 113
        Module v(G)avg v(G)tot
        pta3test 1.88 113
        Project v(G)avg v(G)tot
        project 1.88 113
        Method CogC ev(G) iv(G) v(G)
        AllAnswerSheet.AllAnswerSheet() 0 1 1 1
        AllAnswerSheet.AllAnswerSheet(int, String, ArrayList) 0 1 1 1
        AllAnswerSheet.addAnswerSheet(int, String) 0 1 1 1
        AllAnswerSheet.getAnswerSheetArrayList() 0 1 1 1
        AllAnswerSheet.getAnswerSheetNumber() 0 1 1 1
        AllAnswerSheet.getAnswerStudentID() 0 1 1 1
        AllAnswerSheet.setAnswerSheetArrayList(ArrayList) 0 1 1 1
        AllAnswerSheet.setAnswerSheetNumber(int) 0 1 1 1
        AllAnswerSheet.setAnswerStudentID(String) 0 1 1 1
        AllPaper.AllPaper() 0 1 1 1
        AllPaper.AllPaper(int, ArrayList) 0 1 1 1
        AllPaper.addPaper(int, int) 0 1 1 1
        AllPaper.getPaperArrayList() 0 1 1 1
        AllPaper.getPaperNumber() 0 1 1 1
        AllPaper.setPaperArrayList(ArrayList) 0 1 1 1
        AllPaper.setPaperNumber(int) 0 1 1 1
        AllStudentScore.AllStudentScore() 0 1 1 1
        AllStudentScore.AllStudentScore(String, String, ArrayList) 0 1 1 1
        AllStudentScore.getScoreArrayList() 0 1 1 1
        AllStudentScore.getStudentID() 0 1 1 1
        AllStudentScore.getStudentName() 0 1 1 1
        AllStudentScore.setScoreArrayList(ArrayList) 0 1 1 1
        AllStudentScore.setStudentID(String) 0 1 1 1
        AllStudentScore.setStudentName(String) 0 1 1 1
        AnswerSheet.AnswerSheet() 0 1 1 1
        AnswerSheet.AnswerSheet(int, String) 0 1 1 1
        AnswerSheet.getAnswerQuestionContent() 0 1 1 1
        AnswerSheet.getAnswerQuestionNumber() 0 1 1 1
        AnswerSheet.setAnswerQuestionContent(String) 0 1 1 1
        AnswerSheet.setAnswerQuestionNumber(int) 0 1 1 1
        DeleteQuestion.DeleteQuestion() 0 1 1 1
        DeleteQuestion.DeleteQuestion(int) 0 1 1 1
        DeleteQuestion.getDeleteQuestionNumber() 0 1 1 1
        DeleteQuestion.setDeleteQuestionNumber(int) 0 1 1 1
        Main.main(String[]) 0 1 1 1
        Main.processingDate() 230 17 48 54
        Paper.Paper() 0 1 1 1
        Paper.Paper(int, int) 0 1 1 1
        Paper.getPaperQuestionNumber() 0 1 1 1
        Paper.getPaperQuestionScore() 0 1 1 1
        Paper.setPaperQuestionNumber(int) 0 1 1 1
        Paper.setPaperQuestionScore(int) 0 1 1 1
        Question.Question() 0 1 1 1
        Question.Question(int, String, String) 0 1 1 1
        Question.getAnswerStandard() 0 1 1 1
        Question.getQuestionContent() 0 1 1 1
        Question.getQuestionNumber() 0 1 1 1
        Question.setAnswerStandard(String) 0 1 1 1
        Question.setQuestionContent(String) 0 1 1 1
        Question.setQuestionNumber(int) 0 1 1 1
        Score.Score() 0 1 1 1
        Score.Score(int) 0 1 1 1
        Score.getStudentAnswerScore() 0 1 1 1
        Score.setStudentAnswerScore(int) 0 1 1 1
        Student.Student() 0 1 1 1
        Student.Student(String, String) 0 1 1 1
        Student.getsID() 0 1 1 1
        Student.getsName() 0 1 1 1
        Student.setsID(String) 0 1 1 1
        Student.setsName(String) 0 1 1 1

        看完之後感覺整個人都不好了,這就是主函式戰士的後果(你是誰的部下)。但是還得接著看:

        在官方對Kiviat圖的介紹中:

        Kiviat 圖中的綠色環形區域即被測量維度的期望值,維度上的點則是測量得到的實際值。當一個維度上的點位於綠色環形區域中時表明這個維度的測量結果是符合期望的。

        所以不得不說,這個期望值有點偏離過多。

  • 總結

    這三次題目集的完成情況光從紙面成績來看似乎還算優良

    簡直爛的要命

    這是筆者對自己的嚴厲批評。程式碼不規範,從每次的主函式複雜度就可以看得出來,主函式承擔了太多它不該承擔的功能。而且筆者發現自己還存在一個很嚴重的問題,我願稱之為:學習後移

    解釋:

    “學習後移”指的是筆者總是在下一次題目集去解決上一次題目集出現的問題,因為在前一次題目集的時候可能掌握的東西較少,需要學習的東西較多,但是卻無法在前一次題目集的時間內解決,只能留到下一次題目集接著學習然後再完成。讓我的學習出現了滯後性。

    這樣就會造成一個比較嚴重的後果,每一次的程式碼都要大改,總是會出現很多不能在下一次題目集繼續使用的程式碼,也就是複用性不高。而且寫的程式碼質量也不高,看似拿到了滿分卻實則毫無章法。舉個例子:

    老闆給員工A安排了一個種黃瓜的任務,經費1萬。

    員工A請挖掘機給這黃瓜種子挖了一排坑,花了九千。

    只剩下一千作為以後澆水施肥的費用,半年後黃瓜長出來了,看著和正常的黃瓜看起來沒什麼區別,但是吃起來卻酸澀無比。

    所以筆者只能加倍的努力了,逃出這個學習後移的窘境,避免筆者的黃瓜吃起來又酸又澀

    最後,OO課的難度確實是前所未有的,這對我來說可能是一個很大的挑戰,但是哪有前路一片坦蕩的人生,磕磕碰碰沒事,只要學到了就是值得。真的是讓筆者切身的體會到了,什麼叫做:

    學而不思則罔,思而不學則殆

  • 番外

    • 寫部落格的時候typora是真的好用啊,比起部落格網的編輯器真的是天淵之別啊。
    • 什麼,你說啟用typora你花了89塊?筆者只花了3.99,藉助於什麼都有的神奇某寶。
    • 透過這個就可以學會如何寫部落格了:MarkDown語法
    • 筆者非常正經,為了增加一些閱讀的趣味性所以這篇部落格可能存在一些笑點,不要嘲笑筆者(哈哈哈)。

相關文章