BUAA-OO-第三单元总结

BUAA-OO-第三单元总结

写在前面

​ 首先,庆祝自己渡劫OO第三单元,并且在本单元做到了"金身不坏"。本单元考察的主要内容是JML规格编程,连带一些堆栈、图等算法知识。本单元让我提前的、很好的、充分的的体会到了一回"乙方"的苦涩生活。JML规格就是我们本次作业中的甲方,我们就是乙方。作为乙方,我们的首要目的是了解并完成甲方的要求;但这还不够,因为在生意场上,总会有一些默认的"潜规则",光完成要求是不够的,你还得有一些"亮点"。具体到作业的来说,就是在完成规格编程的基础上进行算法的优化。本单元的大致内容就是如此。

​ 本次博客不会对JML规格的阅读方法做过多的介绍,这是翻阅JML手册就能够了解的。

一、架构设计

​ 总的来说,本次作业在顶层设计上,不需要我们考虑过多的架构问题(稍微有一些),理论上来讲,只需要按部就班地实现指导书所要求的类,就能够完成任务。但是,强测和互测中都有性能要求,我们在一些方法中,不得不"曲线救国",改变实现的方法。这就对一些数据的存储和组织方式提出了要求。本部分所介绍的架构设计主要也是从这方面入手的。

0.我们要做什么?

​ 有的人可能说:JML不就是按图索骥吗?OO第三单元不就是填空吗?这样说不是没有道理,但是作为合格的乙方,我们不能这么想。

​ 刚拿到本单元的作业时,我也能够直接按照JML的规格完成代码。但是,我对整个作业的结构却没有清晰的感触。尽管通过了评测,但是这种状况让我感到担心。于是,我就在博客园上阅读往届学长的博客,但我对得到的结果也不是很满意。于是我决定在我的文章中先分析一下本单元的任务。

​ 本单元的三次作业是高度迭代的(想不迭代都不行),具体任务是按照JML规格完成方法,宏观任务则是完成一个社交网络系统。我们主要关注三个类:MyPerson、MyNetWork和MyGroup。MyPerson类,是最根本的一个类,一个一个的MyPerson对象就相当于一张图中的点。每个MyPerson都有一些信息,其中最关键的就是acquiaintance[]数组,它记录了某个Person的熟人,利用这个数组,可以不断查找......这便是我们所要实现社交网络的根基;MyGourp则相当于一个一个的小群组,每个小群组可以存放一定数量的MyPerson对象,并且提供了一些查询方法;MyNetWork则是最顶层的社交网络,很多最宏观的查询方法都需要在这里实现,如查询两个Person是否"连通"。把握住这些关键信息,其余的功能迭代都是小case。

​ 当然,后续还会增加一些新的类和方法,但只要理解了宏观任务,按照规格填写代码的时候就不会感到心里发虚了。

1.利用规格要求合理选择容器

​ 合理选择容器进行数据结构的组织有助于我们的查询、计算操作。

​ 本次作业中,MyPerson、MyGroup等多个类的实例一个最大的特征就是,他们拥有唯一的id;相应的,本次作业中存在着许多复杂的查找指令。从这一特性出发,我建议使用Haspmap对各类对象进行存储,能够方便查找。

​ 当然,并不是在所有情况下我们都无脑地使用Hashmap进行存储。例如getReceivedMessage()方法,要求"得到某个Person最近接受的四条信息"。针对这种独特的要求,使用链表来存储可能是更好的选择——链表有序,且便于插入。值得注意的是,JML规格中描述"一堆属性"的方式看上去像是数组,但我们最好不要拘泥于规格。

2.以空间换时间

​ 以空间换取时间,是我们在编程时提升性能的一个主要策略。在本单元中,这一思想大有用武之地。

​ MyGroup中要实现的getValueSum()方法,它的复杂度为O(n^2)。如果按照规格要求的写法,每次查询的时候都用二重循环计算一次,那么在互测当中就会有一些"歹人"攻击你的代码。鉴于我们的神经网络数据输入并不是交互式的,我们完全可以在构建网络的时候将未来会在查询指令中出现的值提前计算好。除了这一值,还有不少其他的值可以提前缓存。

3.算法适应

​ 我们知道,有些算法对数据的自身特点或是组织结构都有要求。你当然可以说"这次我先这样写,下次我再改一下",但有些工作却能大大减轻你的负担。在本单元,我们的也会想办法使得我们的架构更加适应我们的算法。

​ 首先是存储上的适应。在第一次作业中,我们需要判断两个Person是否连通。观察往届博客,发现并查集是一个推荐的算法,我们只需要数组存储某个Person的id和其父节点的id即可(路径压缩会有所不同)。看到这里,只要学过一点Java的同学都会采用Hashmap来做这个工作,鉴于Person的id的特性,其好处不言而喻。

​ 另外,我们也可以基于前面的作业对后面的作业做一些预测。既然和图有关,那么最短路径、最小生成树这些算法自然是跑不了。2022年的OO第三单元,在第一次作业中使用并查集的同学会非常吃香,因为后面的作业可以使用并查集优化的克鲁斯卡尔算法,很好地提高了代码的复用率,其好处不言而喻。这也是一种对算法的适应。

​ 还有,就是通过增加适度的类来帮助我们完成算法。Person对象固然关键,但在算法中,它可能不能很好地代替结点和边的作用。我们可能需要自己设计结点类和边类,来简化我们的算法。

4.复用代码

​ 提高代码的复用率,也是我们架构设计中所追求的(尽管有的时候它并不影响你的程序的正确性)。刚刚提到的并查集是本次作业中的一个例子,合并和寻找可以使用同一个方法完成,这样我们能使用多个并查集Hashmap。

​ 指导书给出的另外一个建议也有助于我们复用代码,就是针对诸多的异常类,建立一个计数器类Count,用于记载异常发生的次数。我一开始对这个建议不屑一顾,但到后面的作业、总共有十几个异常类的时候,我就开始感到后悔了:尽管直接复制粘贴也不会影响代码的正确性,但这实在是太丑陋了。所以,提前观察框架,虚心接受建议,也有助于提高我们的代码复用率,让我们的程序看起来更加美观。

5.建议

​ 本单元每一次作业,都会有一个比较棘手的算法需要我们处理。我建议后来者在阅读JML规格后,即便知道要实现什么算法,也不要随便直接找一份代码就照着写;应当尽可能地去寻找优化的方法,参考往年博客的建议,这会使得我们事半功倍。

​ 另外,JML规格的约束众多,而作为人,我们需要学会捕捉关键信息。打个比方,在JML中,倘若向数组添加一个元素,那就要加之以好几道约束以保证逻辑的严谨性。这可能使得JML规格庞大而繁杂,但熟练的阅读者需要大胆地"忽略"一些信息,去阅读关键信息。

二、测试与Hack

​ 本次单元,我没有在强测和互测中被发现Bug,我会介绍一些测试方法和Hack的思路。

1.Junit测试与对拍测试

​ Junit测试的方法在指导书和官方推荐的博客中有介绍,本处就不再赘述了。主要对其做一下评价。Junit的测试方法科学、综合、可分析并且精确,能够对程序的每一个规格方法进行充分的测试,但是它的缺点就是构造数据比较麻烦,我并没有将其作为本单元的主流测试方法。

​ 对拍测试则是依靠随机数据生成器。我和另外几位同学合作,搭建了具有全局开关的数据生成器,可以通过调节数据生成的方式,来构造比较复杂的图进行测试。通过对拍测试,我们还帮助其他同学发现了bug。对拍测试在本单元中能够很有效地保证程序的正确性

​ 不过稍加思考,在工程代码中,Junit测试代表的测试方法还是更加使用一些,毕竟在庞大的工程中,你很难拿到两份或以上的代码。Junit测试的存在意义远大于在本单元中帮助我们进行测试。

2.逻辑测试

​ 根据JML规格,阅读自己实现的代码,从逻辑上思考是否有疏漏之处。虽然效率不高,但细心的话,这种测试方法也能够发挥作用

3.Hack

​ 由于我们拥有数据生成器,我们能够很方便的对他人的程序进行随机测试(当然,导出jar包有些不方便,但研讨课上有同学分享了相关方法)。另外,我们可以根据一些特殊的指令构造性能测试数据,如qbsqgav等。使用python可以轻松地构造这些数据。

4.碎碎念之云端测试

​ 虽然做测试是很有意义的工作,但是跑大量的数据总是让我的Y700P 2019风扇呼啸或者是Macbook Air M1发热,影响使用体验。在某位学长的指导下,我免费试用了一个阿里云服务器,并在上面进行对拍测试,发现体验非常好:不会浪费本机性能,也不必担心本机状况。我仅在第三单元采用了这种测试方法,倘若在第二单元也能够想到这种测试方法,或许可以做到"白天自动跑,晚上看日志"

​ 总的来说,在本单元中对自己的代码进行测试,收益是很高的,推荐后来的同学们多做测试,多学会做测试。

三、Network拓展

​ 我们可以令Advertiser、Producer、Customer继承Person接口,令AdvertiseMessage、ProduceMessage、PurchaseMessage继承Message接口,并实现这些接口,构建相关类。

​ 由于sendMessage()方法的规格已经十分复杂,故而新的方法不在它的基础上编撰。

1.Advertiser发送广告

​ 发送广告的方法和sendMessage()方法有很大的类似之处,首先使用自然语言描述:

  • 将某个AdvertiseMessage发送给Advertiser的同Group中的Customer
  • 每个AdvertiseMessage有独特的ProductId
  • 接收到AdvertiseMessage的Person的购物清单数组product[]添加ProductId
  • 一些条件在添加本条AdvertiseMessage的时候得到保证
    /*@ public normal_behavior
      @ requires containsMessage(id) && (getMessage(id) instanceof AdvertiseMessage) &&
			@ getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
			@ assignable people[*].messages, people[*].products, messages;
      @ ensures (\forall Person p; getMessage(id).getGroup().hasPerson(p) &&
      @ 				(p instanceof Customer); p.getSocialValue() == \old(p.getSocialValue()) + 
      @ 				getMessage(id).getSocialValue());
      @ ensures (\forall int i; 0 <= i && i < people.length && 
      @					!(getMessage(id).getGroup().hasPerson(people[i]) && 
      @					(p instanceof Customer));
      @         \old(people[i].getSocialValue()) == people[i].getSocialValue());
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && 
      @ 				\old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length; 
      @					messages[j].equals(\old(messages[i]))));
      @ ensuers (\forall int i; 0 <= i && i < people.length && 
      @					!(getMessage(id).getGroup().hasPerson(people[i]) && 
      @					(people[i] instanceof Customer));
      @         \old(people[i].products.length) == people[i].products.length) &&
      @					(\forall int j; 0 <= j && j < people[i].products.length;
      @					(\exists int k;	0 <= k && k < \old(people[i].products.length);
      @					people[i].products[j] == people[i].products[k]))
      @ ensures (\forall int i; 0 <= i && i < people.length && 
      @					getMessage(id).getGroup().hasPerson(people[i]) && 
      @					(people[i] instanceof Customer);
      @         \old(people[i].products.length) == people[i].products.length - 1) &&
      @					(\forall int j; 0 <= j && j < \old(people[i].products.length);
      @					(\exists int k;	0 <= k && k < people[i].products.length;
      @					people[i].products[j] == people[i].products[k]) &&
      @ 				(\exists int l; 0 <= l && l < people[i].products.length;
      @					people[i].products[l] == ((AdvertiseMessage) 
      @					getMessage(id)).getProductId()))
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id) ||
      @					(containsMessage(id) && !getMessage(id) instanceof AdvertiseMessage);
      @ signals (PersonIdNotFoundException e) containsMessage(id) && 
      @          !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
      @*/
public void sendAdvertiseMessage(int id) throws
		MessageIdNotFoundException, PersonIdNotFoundException;

2.Customer购买商品

​ 首先给出自然语言描述:

  • 检查id为id2的Customer清单中是否有对应的商品
  • 倘若有,则由Advertiser向Producer发送id为id1的PurchaseMessage
  • 将Customer的清单中的该商品移除,更新Producer的产品清单
  • 一些条件在添加本条PurchaseMessage时得到保证
    /*@ public normal_behavior
      @ requires containsMessage(id1) && (getMessage(id1) instanceof PurchaseMessage) &&
			@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
      @ getMessage(id).getPerson1() != getMessage(id).getPerson2() && 
			@ ((MyCustomer) getPerson(id2)).hasRequest(((MyPurchaseMessage) getMessage(id2)).
			@ getProductId())
			@ assignable messages, getPerson(id2).products;
			@ assignable getMessage(id).getPerson1().socialValue, 
      @ assignable getMessage(id).getPerson2().messages, 			
      @ assignable getMessage(id).getPerson2().socialValue;
			@ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && 
      @ 				\old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length; 
      @					messages[j].equals(\old(messages[i]))));
      @ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
      @         \old(getMessage(id).getPerson1().getSocialValue()) + 
      @					\old(getMessage(id)).getSocialValue() &&
      @         \old(getMessage(id)).getPerson2().getSocialValue() ==
      @         \old(getMessage(id).getPerson2().getSocialValue()) + 
      @					\old(getMessage(id)).getSocialValue();
			@	ensures (\forall int i; 0 <= i && i < (\old((Customer) getPerson(id2)).products)).
			@					length;(\exists int j; 0 <= j && j < ((Customer) getPerson(id2)).products.
			@					length;(Customer) getPerson(id2)).products[i] == \old((Customer) 			
			@					getPerson(id2)).products[j]);
			@	ensures !(\exists int i; 0 <= i && i < ((MyCustomer) getPerson(id2)).
			@					products.length;((Customer) getPerson(id2)).products[i] ==
			@					((PurchaseMessage) getMessage(id2)).getProductId);
			@ ensures (\old((Customer) getPerson(id2)).products).length == 
			@					((Customer) getPerson(id2)).products).length + 1;
			@ ensures (\old(((Producer) getPerson(getMessage.getPerson2().products))).length ==
			@					((MyProducer) getPerson(getMessage.getPerson2()).products).length - 1;
			@ ensures (\forall int i; 0 <= i && i <= (\old(((Producer) getPerson(getMessage(id1).
			@					getPerson2().products))).length);(\exist int j; 0 <= j && j <= ((Producer) 
			@					getPerson(getMessage(id1).getPerson2().products)).length;(\old(((Producer) 
			@					getPerson(getMessage(id1).getPerson2().products))))[i] == (Producer) 
			@					getPerson(getMessage(id1).getPerson2()).products)[j])
			@ ensures (\exist int i; 0 <= i && i < ((Producer) getPerson(getMessage(id1).
			@					getPerson2())).length;((Producer) getPerson(getMessage(id1).getPerson2())).
			@					products[i] == ((PurchaseMessage) getMessage(id1)).getProductId)
			@ also
			@ public exceptional_behavior
			@ signals (MessageIdNotFoundException e) !containsMessage(id) ||
      @					(containsMessage(id) && !getMessage(id) instanceof PurchaseMessage);
      @ signals (RelationNotFoundException e) containsMessage(id) &&
      @          !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
      @ signals (ProductIdNotFoundException e) containsMessage(id) && 
      @          !(((MyCustomer) getPerson(id2)).hasRequest(((MyPurchaseMessage) 		
      @						getMessage(id2)).getProductId()))
      @*/
public void sendPurchaseMessage(int id1, int id2) throws
		MessageIdNotFoundException, RelationshipNotFoundException, ProductNotFoundException;

3.Producer生产产品

​ 自然语言描述如下:

  • 检查是否存在id对应的ProduceMessage,倘若存在,则发送
  • 更新Producer的产品清单和Customer的获得物品清单,并且更新两者的Money属性
  • 一些条件在添加本ProduceMessage的时候已经得到保证
    /*@ public normal_behavior
      @ requires containsMessage(id1) && (getMessage(id) instanceof PurchaseMessage) &&
			@ hasPerson(getMessage(id).getPerson1()) && hasPerson(getMessage(id).getPerson2()) &&
			@	isLinked(getMessgae(id).getPerson1(), getMessgae(id).getPerson2());
			@ assignable messages;
			@ assignable getPerson(getMessage(id).getPerson1()).money;
			@ assignable getPerson(getMessage(id).getPerson1()).products;
      @ assignable getPerson(getMessage(id).getPerson2()).money;
      @ assignable getPerson(getMessage(id).getPerson2()).purchaseList;
      @ assignable getPerson(getMessage(id).getPerson2()).messages;
      @ ensures \old(getPerson(getMessage(id).getPerson1()).getMoney()) ==
      @					getPerson(getMessage(id).getPerson1()).getMoney() - ((ProduceMessage) 		
      @					getMessage(id)).getMoney;
      @ ensures \old((Producer) getPerson(getMessage(id).getPerson1().products).length ==
      @					((Producer) getPerson(getMessage(id).getPerson1()).products.length + 1;
      @ ensures (\forall int i; 0 <= i && i <= ((Producer) getPerson(getMessage(id).
      @					getPerson1())).products.length;
      @					(\exist int j; 0 <= j && j <= (\old((Producer) getPerson(getMessage(id).
      @					getPerson1()).products).length;((Producer) getPerson(getMessage(id).
      @					getPerson1())).products[i] == (\old((Producer) getPerson(getMessage(id).
      @					getPerson1())).products[j]));
      @ ensures !(\exist int i; 0 <= i && i <= ((Producer) getPerson(getMessage(id).
      @					getPerson1()).products).length;((Producer) getPerson(getMessage(id).
      @					getPerson1())).products[i] == ((ProduceMessage) getMessage(id)).
      @					getProductId());
      @	ensures \old(getPerson(getMessage(id).getPerson2()).getMoney()) ==
      @					getPerson(getMessage(id).getPerson2()).getMoney + ((ProduceMessage))
      @					getMessage(id)).getMoney;
      @ ensures \old((Customer) getPerson(getMessage(id).getPerson2()).purchaseList).length 
      @					== ((Customer) getPerson(getMessage(id).getPerson2()).purchaseList).length 
      @					- 1;
      @ ensures (\forall int i; 0 <= i && i <= (\old((Customer) getPerson(getMessage(id).
      @					getPerson2()).purchaseList).length);
      @					(\exist int j; 0 <= j && j <= ((Customer) getPerson(getMessage(id).
      @					getPerson2())).purchaseList.length;(\old((Customer) 	
      @					getPerson(getMessage(id).getPerson2()).purchaseList[i] == ((Customer) 
      @					getPerson(getMessage(id).getPerson2())).purchaseList[j]));
      @ ensures (\exist int i; 0 <= i && i <= ((Producer) getPerson(getMessage(id).
      @					getPerson1()).purchaseList).length;((Producer) getPerson(getMessage(id).
      @					getPerson1())).purchaseList[i] == ((ProduceMessage) getMessage(id)).
      @					getProductId());
			@*/
public void sendPurchaseMessage(int id1) throws
		MessageIdNotFoundException, RelationshipNotFoundException, ProductNotFoundException;

四、心得体会

​ 首先,就是心疼一下助教们,尤其是出题助教,因为这个单元的JML规格阅读起来已经很困难了,书写的难度就更加恐怖。助教不仅要编写代码结构,还需要对付可怕的JML规格......真是太辛苦了!

​ 然后,我也要感谢这我在这一单元作出的努力。本单元的作业完成起来并不困难,所以我有充分的时间来准备测试,并且在本单元做到了"金身不坏";我也充分地学习到了一些测试的方法和思想,学习了其他语言和脚本的相关知识。我在这一单元的收获不亚于在前两个单元的高强度训练中的收获。

​ 最后,JML规格语言的学习也让我对规范编程、面向需求编程有了更深的理解,也温习了一些算法的知识,很有效地提高了我的综合编程能力。

posted @ 2022-06-03 19:11  Enqurance  阅读(35)  评论(1编辑  收藏  举报