1. 程式人生 > 其它 >BUAA OO 2022 第一單元

BUAA OO 2022 第一單元

一、程式結構

UML類圖與架構設計

第一次作業

  • 通過Lexer和Parser解析字串,遞迴下降生成Expr物件時去除括號
  • 重寫Expr.toString(),生成記錄運算順序的字尾表示式SuffixString。格式"X1(運算元) X2(運算元) +(操作符) ..."。字尾表示式的操作符包括+、*、^int(表示式因子的冪運算)、!(取反);
  • Parser維護了一個字尾表示式運算元表
  • Suffix類獲取包含操作符運算順序的String和運算元表,用棧進行計算結果得到Quantic物件,對Quantic物件呼叫print()方法輸出。

第二次作業迭代思路:

//紅色為資料型別的改變
//藍色為新增類與方法

一、引入三角函式後的統一儲存形式

  • 多項式的每項基本形式:ax^b -> ax^b(sin容器)(cos容器)
  • 選用hashmap,merge()方法和lambda函式實現合併同類項的書寫比較簡潔。
    • Key為自定義型別BaseKey,重寫hashcode()equal()後便於合併同類項。形式:x^b(sin容器)(cos容器)
    • 由於需要維護可變型別BaseKey作為hashmap的key的不可變性,以及value代表的係數為不可變型別BigInteger,沒有出現深淺拷貝的Bug

二、sum函式因子的處理

  • 將sum函式作為因子Factor,在遞迴下降處理表達式中,類似處理表達式因子的模式,進行遞迴下降處理
  • 具體實現:識別出sum函式的求和表示式,為此表示式新建lexer和parser物件,返回求和後的Expr作為因子。型別為抽象介面Factor

三、自定義函式的處理

  • 新建自定義函式類,類中使用static成員變數儲存預先讀入的函式形參表和函式表示式
  • 遞迴下降過程中識別到自定義函式因子時,新建自定義函式類的物件,傳入讀取到的實參表,返回替換後的Expr作為因子。

第三次作業迭代思路

一、巢狀函式:作業二實現

二、三角函式因子為表示式因子

  • sin(bracket),bracket型別由Single換為Quantic即可

三、三角函式的化簡

  • sin(0)->(0);cos(0)->(1),Pre
    類內字串替換即可;
  • sin(bracket),bracket為常數因子或冪函式因子時去括號。Suffix.print()輸出字串時按有無'+''*'作為常數因子或冪函式因子的判斷依據;
  • 誘導公式:括號內負號外提。三角函式符號=f(括號內表示式符號,指數,三角名)lexer.getTrigon()內處理;
  • sin(brackt)**2->(1-cos(brackt)**2); CalculateExpr()化簡,取最短字串為結果;
    cos(brackt)**2->(1-sin(brackt)**2); CalculateExpr()化簡,取最短字串為結果;

使用的OO度量

度量指標 說明
LCOM Lack of Cohesion in Methods – Class 方法的內聚缺乏度 值越大,說明類內聚合度越小。
FANIN Fan-in – Class 類的扇入 表示呼叫該模組的上級模組的個數,扇入越大,表示該模組的複用性好。
FANOUT Fan-out – Class 類的扇出 表示該模組直接呼叫的下級模組的個數,扇出過大表明模組複雜度高,但扇出過小也不好。
OCavg Average opearation complexity 類的平均操作複雜度
OCmax Maximum operation complexity 類的最大操作複雜度
WMC Weighted method complexity 類的加權方法複雜度
CogC Cognitive complexity 方法的認知複雜度
ev(G) Essential cyclomatic complexity 方法的基本圈複雜度 衡量程式非結構化程度。
iv(G) Design complexity 方法的設計複雜度 模組和其他模組的呼叫關係。軟體模組設計複雜度高意味模組耦合度高,這將導致模組難於隔離、維護和複用。
v(G) cyclonmatic complexity 方法的獨立路徑的條數

類的內聚和相互間的耦合情況

以下為三次作業的類的屬性個數、方法個數、LCOM、FANIN、FANOUT。
大致依據類的功能分為儲存類和執行類分別統計。執行類包括解析、計算、化簡輸出三部分。

hw1


hw2

hw3

分析:

設計要求高內聚低耦合,即LCOM值要小,FANIN值要大,FANOUT值要合理。

  1. 儲存類的FANIN明顯高於執行類的FANIN
  • 儲存類的複用率高:說明儲存型別選取的hashmap<BaseKey,BigInteger>較為合適,與同學交流時也發現自己的儲存與計算的程式碼實現較為簡潔,hw1-3均使用了Basekey和merge方法,沒有過多迭代過程
  • 執行類的複用率低,說明執行類的邏輯仍然存在面向過程性。
  1. LCOM較低,說明方法的高內聚實現較好。

方法的規模與分支複雜度情況

以下為擷取hw3的複雜度較高的方法和類,三次作業的問題大致相似,複雜度主要集中在Lexer、Suffix的符號識別和字串輸出化簡兩部分。由於分支複雜度較高,這兩部分的測試時間和bug也相應較多。

優點

  • 儲存類的資料型別選取合適:
    • 選用hashmap,merge()方法和lambda函式實現合併同類項非常簡潔。
    • Key為自定義型別BaseKey,重寫hashcode()equal()後便於合併同類項
    • 由於需要維護可變型別BaseKey作為hashmap的key的不可變性,以及value代表的係數為不可變型別BigInteger,沒有出現深淺拷貝的Bug
public class Quantic {
    private HashMap<BaseKey, BigInteger> quanticMember;

    public void quanticAdd(Quantic nextTop) {
        nextTop.getQuanticMember().forEach((key, value) ->this.quanticMember.merge(key, value, BigInteger::add));
        //hashmap1合併進hashmap2,類似合併同類項原理
    }
  • forEach()方法用於對 HashMap 中的每個鍵值對執行指定的操作。匿名函式 lambda 的表示式 作為 forEach()方法的引數傳入。
  • merge()方法用於合併兩個hashmap,使用lambda表示式 (oldValue, newValue) -> (oldValue + newValue) 作為重對映函式。
  • Java 8的方法引用更方便,方法引用由::雙冒號操作符標示,使用BigInteger::add作為重對映函式即可
  • 由於hashmap.merge()在插入hashmap2中不存在的key與其對應的value時不會呼叫重對映函式,故減法不能使用BigInteger::subtract作為對映函式;解決辦法為減數先取反,再與被減數呼叫quanticAdd()即可
  • 乘法將兩個BaseKey相乘後的新BaseKey作為merge方法的key引數,係數的乘積作為value引數,重對映函式BigInteger::add
public Quantic quanticMulQuantic(Quantic nextTop) {
        Quantic ans = new Quantic();
        for (BaseKey j : this.quanticMember.keySet()) {
            for (BaseKey y : nextTop.quanticMember.keySet()) {
                BigInteger coeJ = this.quanticMember.get(j);
                BigInteger coeY = nextTop.getQuanticMember().get(y);
                ans.getQuanticMember().merge(new BaseKey(j,y),coeJ.multiply(coeY), BigInteger::add);
            }
        }
        return ans;
    }

缺點

  • 由於對應用設計模式的經驗較為缺乏,架構設計時沒有采用設計模式,存在顯著的面向過程性
  • lexer和parser的實現時,複雜度過高。在hw1中把減號的語義去除,即parser的語義分割符僅為'+''*''-'僅作為Expr、Term、Single和Trigon的符號位,在後續作業的係數的符號=f(符號,指數)的轉化時出了很多錯誤,例如負號外提出錯sin(-1)**2化簡成-sin(1)**2與識別負號位時未考慮pos==length的情況出錯。同時parser的三個方法均出現了表示符號的引數的巢狀傳遞情況,debug時出現非常大的麻煩。

二、bug分析

第二次公測

  • 自定義函式的函式宣告中未考慮空格和tab,WA了兩個點

第二次互測

  • lexer的一個分支忘記新增pos==lengthreturn的終止條件,導致+-+1出現exception
    反思:迭代開發作業可以使用之前作業的強測資料來測試本次作業的修改是否影響到之前的功能;做測試時不要忘記測試原先的基礎功能是否維持正常
  • sin(-1)**2化簡成-sin(1)**2,未考慮偶數次冪時消去負號的情況。
  • 沒有注意到互測的指數輸入可以大於8
    反思:公測時也要注意互測的資料規範;修改指數的資料範圍時有一處遺漏導致出錯

第一次和第三次作業未出現Bug

整體分析

  • 總體來說,由於儲存類的程式碼行和圈複雜度明顯低於工作類,未出現計算方面的Bug。
  • Bug主要集中在lexer和parser的識別和解析字串部分,此部分的圈複雜度較高,出現遺漏分支與修改不完善的情況。
  • 輸出部分的圈複雜度最高,針對此部分做的測試較多,未出現bug。

三、hack策略

  • 自動化評測機測試時,對於資料生成程式的要求較高,當資料輸入的限制較多與程式功能較為簡單時,自動生成資料的強度不容易控制,容易出現數據較弱或不合法的情況。
  • 根據被測程式構造樣例的思路:
    • 圈複雜度較高的環節:讀入、輸出、三角優化;
    • 資料範圍是否覆蓋全面:例如sum的BigInteger
    • 新增功能是否考慮全面:例如函式宣告的空格和i**n的情況

四、心得體會

  • 不要出現50行+的方法,否則Bug修復的時候有方法超過60行的風險Orz
  • 為debug模式重寫toString()便於除錯,尤其是包含巢狀hashmap的類;滿足作業化簡要求的的輸出單獨建立print()方法
  • 儘量在作業的開始階段留出一定的可擴充套件空間,降低後續的修改規模&重構風險:比如作業1的時候偷懶按照冪次為單個char寫的程式,不僅導致忘記冪次的前導符號和前導0,還導致作業2把冪次修改成大於8時只修改了lexer部分,字尾表示式有關冪次計算的部分忘記修改了;作業2的時候沒有把sum和自定義函式作為字串替換,而是在遞迴下降的過程中作為表示式因子替換,給作業3的巢狀函式留出了擴充套件性,作業3的相關功能也在作業2得到了測試;程式擴充套件一些額外的功能也意味著有更大的測試空間,對於資料生成程式的格式要求會降低,比如在作業1中不對括號層數和指數作限制時,測試的強度更高,資料生成程式也更簡潔一些。