1. 程式人生 > 其它 >2022面向物件設計與構造第三單元總結

2022面向物件設計與構造第三單元總結

一、測試策略

本單元作業我基本的測試策略就是單元測試 + 情況覆蓋

  • 單元測試:使用JUnit工具,對每一個類建立一個測試類,然後再在測試類內編寫各方法對應的測試方法。
  • 情況覆蓋:由於本單元需要實現的介面都由JML描述,所以在編寫測試方法時,只需要覆蓋各方法的JML中寫明的各種情況即可。比如對於Network介面中的storeEmojiId方法,它的JML描述為:
 /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < emojiIdList.length; emojiIdList[i] == id);
      @ assignable emojiIdList, emojiHeatList;
      @ ensures (\exists int i; 0 <= i && i < emojiIdList.length; emojiIdList[i] == id && emojiHeatList[i] == 0);
      @ ensures emojiIdList.length == \old(emojiIdList.length) + 1 &&
      @          emojiHeatList.length == \old(emojiHeatList.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
      @          (\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]) &&
      @          emojiHeatList[j] == \old(emojiHeatList[i])));
      @ also
      @ public exceptional_behavior
      @ signals (EqualEmojiIdException e) (\exists int i; 0 <= i && i < emojiIdList.length;
      @                                     emojiIdList[i] == id);
      @*/
    public void storeEmojiId(int id) throws EqualEmojiIdException;

由JML可知,這個方法有兩種執行的可能,一種是emojiIdList不存在輸入的id,此時將id加入emojiIdList,並將對應的emojiHeatList置零。另一種是emojiIdList存在輸入的id,此時則會丟擲EqualEmojiIdException異常。基於上面的複習,我編寫了以下的測試方法:

    @org.junit.jupiter.api.Test
    void storeEmojiIdTest() throws Exception {
        network.storeEmojiId(1);
        System.out.println(network.containsEmojiId(1));
        network.storeEmojiId(1);
    }

前兩行是為了測試第一種情況,第三行是為了測試第二種情況。若方法實現無誤,則測試方法執行之後應該會先輸出一個true,然後再丟擲一個EqualEmojiIdException異常。

當然,這個測試方法並不完備,它並沒有全面覆蓋JML中的每一個ensure語句,我只是以它舉例說明測試的基本思路。

二、架構設計

由於本單元作業的基礎架構已由官方包決定,所以下面主要分析高時間複雜度方法的優化策略。

2.1 第一次作業

2.1.1 MyNetwork類isCircle()方法的優化

  • 優化前:
    • 演算法:深度優先搜尋。
    • 時間複雜度:O(n!)
  • 優化策略:
    使用並查集,用樹來組織並查集,在MyPerson類中新增一個father屬性,表示該物件在並查集樹中的父節點,初始值為自身。當呼叫addRelation方法時,首先判斷該relation兩端的person在不在同一個並查集樹中。若在,則直接新增,否則合併兩個樹。合併的原則是:將較高的那棵樹的根節點作為新樹的根節點,另一棵樹的根節點作為新樹根節點的子節點。
  • 優化後時間複雜度:
    addRelation時間複雜度不變,isCircle時間複雜度降為O(log2n),整體時間複雜度大大降低。

2.1.2 MyNetwork類queryBlockSum()方法的優化

  • 優化前:
    • 演算法:二重迴圈 + isCircle
    • 時間複雜度:O(n^2*log2n)
  • 優化策略:
    為MyPerson類設定一個minIndex屬性,表示該MyPerson物件所在的連通分量中所有MyPerson物件在people陣列中的最小下標值。物件剛加入Network時,minIndex設為此時該物件的下標。當不同連通分量合併時,新的根節點的minIndex值設為原連通分量兩個根節點的最小minIndex值。而查詢一個MyPerson物件的block時,只需要找到該物件所在連通分量的根節點,查詢根節點的minIndex值,將起與此物件的下標值相比較,若兩者相等,則說明該物件之前的所有MyPerson物件都不與其連通,block為真。
  • 優化後時間複雜度:
    addRelation時間複雜度不變,queryBlockSum時間複雜度降為O(log2n),整體時間複雜度大大降低。

2.2 第二次作業

2.2.1 MyNetwork類queryLeastConnection()方法的優化

  • 優化前:
    • 演算法:普通的Kruskal演算法
    • 時間複雜度:O(mn)
  • 優化策略:
    建立一個Relation類,用來儲存Network中的關係(即圖的邊),再在MyNetwork中增加一個connectGroups屬性,用來儲存各連通分量根節點和分量邊集合的一一對映。當addRelation時,首先判斷該relation兩端的person在不在同一個連通分量中。若在,則直接將新的relation加進對應的connectGroups項,否則合併兩個根節點對應的relation集合,再把新的relation加進去。這樣,使用Kruskal演算法時就可以先直接得到該點所處的連通分量的所有邊,然後使用該邊集進行快排。此外,同樣可以將並查集應用到Kruskal演算法連通分量的檢驗中,具體實現不再贅述。
  • 優化後時間複雜度:
    addRelation時間複雜度不變,queryLeastConnection的時間複雜度降為O(mlog2n),整體時間複雜度明顯降低。

2.2.2 MyGroup類getValueSum()方法的優化

  • 優化前:
    • 演算法:二重迴圈 + isLinked
    • 時間複雜度:O(n^3)
  • 優化策略:
    為MyPerson類設定一個inGroups屬性,它是一個列表,表示該物件所在的所有群組。同時為MyGroup設定一個valueSum屬性。當向群組加人時,先把該群組的id加進該person的inGroups列表,然後遍歷該person的所有熟人,如果熟人也在該群組裡,則將valueSum加上二倍的權值,從群組刪除人時同理。特別地,當addrelation時,如果兩端的person存在公共群組,則將這些群組的valueSum都加上二倍權值。同時,為了儘可能地減小時間複雜度,我將能使用Hashset/hashmap的地方都做了替換。
  • 優化後的時間複雜度:
    考慮到指導書給出的限制:一個person所在是群組最多為20個,所以可以認為addRelation時間複雜度不變,addToGroup和delFromGroup的時間複雜度增加到O(n),getValueSum的時間複雜度降為O(1),整體時間複雜度大大降低。

2.3 第三次作業

2.3.1 MyNetwork類sendIndirectMessage()方法的優化

  • 優化前:
    • 演算法:普通的單源點最短路徑演算法(Dijkstra演算法)
    • 時間複雜度:O(n^2 + m)
  • 優化策略:
    使用小根堆來維護Dest陣列,降低尋找最小值操作的時間複雜度。
  • 優化後時間複雜度:
    sendIndirectMessage的時間複雜度降為O((m + n) * log2n),考慮到點和邊的實際新增方式,所以時間複雜度實際上是從O(n^2)降為了O(nlog2n),整體時間複雜度明顯降低。

三、程式碼效能問題

本單元我僅在第一次作業出現過一次效能問題,就是前文已述的queryBlockSum()方法,此處不再贅述。

四、Network擴充套件

4.1 基本擴充套件思路

新建MyAdvertiser、MyProducer、MyCustomer類,分別用來表示Advertiser、Producer、Customer,並在MyNetwork中建立advertisers、producers、customers屬性。為了實現這些新增人物的特殊屬性,MyAdvertiser應該有一個儲存當前正在推送廣告的產品id屬性和僱主id屬性,以及customers和producers的一個副本;MyProducer的屬性應該包含產品的id、價格、銷售量,以及customers的一個副本;MyCustomer的屬性應該有自己收到的所有廣告組成的列表。同時實現這三個類內部以及MyNetwork的相關方法。

4.2 相關介面方法

  • MyAdvertiser
    • getter-setter方法
    • sendAdvertise方法:向消費者傳送廣告(產品id)
    • sendBuyingMessage方法:消費者通過它向生產商傳送購買需求
  • MyProducer
    • getter-setter方法
  • MyCustomer
    • getter-setter方法
    • buy方法:向廣告商傳送購買需求
  • MyNetwork
    • setAdvertiser方法:為生產商繫結一個廣告商
    • advertise方法:讓廣告商向所有消費者傳送一輪廣告
    • buyProduct方法:讓消費者購買一定數量的某產品
    • queryProductSales方法:查詢產品銷售額
    • queryProductPath方法:查詢產品銷售路徑
    • 其它的get*、contains*查詢方法,add*增添方法

4.3 核心業務介面JML

選擇MyNetwork的前三個核心業務功能介面

  • setAdvertiser方法:
/*@ public normal_behavior
  @ requires (containsProducer(producerId) && containsAdvertiser(advertiserId));
  @ assignable getAdvertiser(advertiserId).productId, getAdvertiser(advertiserId).producerId;
  @ ensures getAdvertiser(advertiserId).productId == getProducer(producerId).productId;
  @ ensures getAdvertiser(advertiserId).producerId == producerId;
  @ also
  @ public exceptional_behavior
  @ signals (ProducerIdNotFoundException e) !containsProducer(producerId)
  @ signals (AdvertiserIdNotFoundException e) (containsProducer(producerId) && !containsAdvertiser(advertiserId));
  @*/
public void setAdvertiser(int producerId, int advertiserId);
  • advertise方法:
/*@ public normal_behavior
  @ requires containsAdvertiser(advertiserId);
  @ assignable customers[*].receivedAdvertise;
  @ ensures (\forall int i; i >= 0 && i < customers.length;
  @		(\forall int j; j >= 0 && j < \old(customers[i].receivedAdvertise.length);
  @			(\exist int k; k >= 0 && k < customers[i].receivedAdvertise.length; \old(customers[i].receivedAdvertise)[j] == customers[i].receivedAdvertise[k])));
  @ ensures (\forall int i; i >= 0 && i < customers.length;
  @		(\exist int j; j >= 0 && j < customers[i].receivedAdvertise.length; customers[i].receivedAdvertise[j] == getAdvertiser(advertiserId).productId));
  @ ensures (\forall int i; i >= 0 && i < customers.length;
  @		customers[i].receivedAdvertise.length = \old(customers[i].receivedAdvertise).length + 1);
  @ also
  @ public exceptional_behavior
  @ signals (AdvertiserIdNotFoundException e) !containsAdvertiser(advertiserId));
  @*/
public void advertise(int advertiserId);
  • buyProduct方法:
/*@ public normal_behavior
  @ requires (containsCustomer(customerId) && getCustomer(customerId).contains(productId) && amount >= 0);
  @ assignable getProducer(productId).sales;
  @ ensures getProducer(productId).sales == \old(getProducer(productId).sales) + getProducer(productId).price * amount;
  @ also
  @ public exceptional_behavior
  @ signals (CustomerIdNotFoundException e) !containsCustomer(customerId));
  @ signals (ProductIdNotFoundException e) (containsCustomer(customerId) && !getCustomer(customerId).contains(productId));
  @ signals (AmountException e) (containsCustomer(customerId) && getCustomer(customerId).contains(productId) && amount < 0);
  @*/
public void buyProduct(int customerId, int productId, int amount);

五、學習體會

本單元的學習給我帶來了兩大體會:

  • 一是考慮問題一定要全面。JML是一個嚴謹性很高的語言,能囊括一個方法的各種情況,但凡具體實現與JML有絲毫偏差,都會產生意想不到的錯誤。因此,在學習JML的過程中,我的考慮問題的思維也不免變得更加嚴謹、全面。
  • 而是效能與功能同樣重要。在過去的程式碼實踐中,我一直都只關注自己程式碼的功能是否符合要求,很少去思考演算法是否最優、時間複雜度能否減小、是否存在冗餘操作等問題。本單元的學習讓我意識到了效能的重要性,也為我培養了一些降低時間複雜度的基本思維。