BUAA-OO-Unit3 总结
BUAA-OO-Unit3 总结
1. 数据测试
在本单元中, 采用黑盒测试和白盒测试兼并的手段进行测试.
对于黑盒测试, 主要是以搭建评测机的方式进行测试. 与岳诗扬同学合作搭建评测机, 正确性判定部分采用多人对拍的方式. 笔者认为, 在输出答案唯一的前提下, 多人对拍也不乏是一种兼具效率和客观性的正确性评定方式, 当然也可能大家一起错了...
对于白盒测试, 笔者认为在本单元中, 通读代码检查逻辑的手段十分有效, 一方面是因为规格契约已给出, 保证实现逻辑和JML契约一致即可保证正确; 另一方面又是在本单元中, 大多数方法实现相较简单, 逻辑简明清晰, 易于与标准规格进行比对. 对于较难实现的方法(比如qci, qbs, qlc和sim)可通过Junit进行单元测试, 或通过随机生成数据点进行单一指令的测试.(亲测有效!)
2. 架构设计
本单元的架构基本是跟着JML规格走的....
3. 作业性能分析
3.1 第九次作业
在第九次作业中, 比较难处理的指令是qci和qbs.
qci指令意在判断两点是否处于同一连通块之中, 若采用DFS极有可能超时. 这里笔者采用路径压缩的并查集算法进行连通块判断. 不过, 在特殊情况下, 关系图很有可能会退化成一条链, 使得查询深度较大, 此时可采用按秩合并进行相关优化.
值得一提的是, 若采用递归的形式实现路径压缩的并查集算法, 则极有可能在大数据的数据点下爆栈(函数调用方需要申请一块栈帧用于维护弱保护寄存器). 故而可使用迭代循环的方式替换递归.
而qbs指令则是查询无向图连通块数量. 一种可行的做法是遍历Network中所有person, 分别判断isCircle(应该也不会超时?). 另外一种做法是维护当前连通块数量.
3.2 第十次作业
第十次作业的中, 比较难处理的指令是qlc.
qlc指令考察最小生成树的算法. 由于笔者在hw9中就维护了两个person之间的关系边, 故而在这里笔者采用堆优化的prim算法. Java中可使用优先队列来维护小(大)顶堆.
在本次作业中, 若按照JML中的规格直接编码, qsv指令的复杂度将达到\(O(n^2)\), 在遇到"别有用心"的用例时会TLE. 笔者的做法是在group中动态维护socialValue, 这样查询操作即能降为\(O(1)\)复杂度.
/* keep ageVar & socialValue */
int ageMean = getAgeMean();
ageVar = (person.getAge() - ageMean) * (person.getAge() - ageMean);
people.values().forEach(person1 -> {
valueSum += 2 * person.queryValue(person1);
int age = person1.getAge();
ageVar += (age - ageMean) * (age - ageMean);
});
people.put(person.getId(), person);
ageVar /= peopleSize;
同样使用动态维护策略的还有qgvs, qgps, qgav指令. 若采用以下公式动态维护方差很可能会丢失精度而导致与答案不一,
3.3 第十一次作业
第十一次作业中, 比较难处理的指令是sim.
sim指令考察的是最短路径算法. 朴素dijkstra算法时间复杂度\(O(n^2)\), 为了防止在强测中被卡TLE, 笔者选择堆优化的dijkstra算法, 时间复杂度降为\(O(mlog(n))\), 其中m为边数, n为点数. 在Java中有封装好的容器优先队列, 可以用于维护堆结构.
4. Network扩展
4.1 需求分析
扩展要求
在本次扩展中, 增加了如下几种Person,
- Advertiser: 持续向外发送产品广告
- Producer: 产品生产商,通过Advertiser来销售产品
- Customer: 消费者,会关注广告并选择和自己偏好匹配的产品来购买
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等, 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
需求分析
在本次扩展中, 对项目结构做出以下调整:
- 新增Product产品类, 其中字段有生产商, 所负责宣传的广告商, 销售额.
- 新增PurchaseMessage, AdvertiseMessage, ProductMessage类, 使其继承Message类.
- 新增Person的三个子类Advertiser类, Producer类和Customer类.
为了支持市场影响, 对Network接口新增以下三个接口方法:
- 添加商品进入市场
addProduct(Product product)
/*@ public normal_behavior
@ requires (\forall int i; 0 <= i && i < products.length; products[i].getProductId() != product.getProductId());
@ requires contains(product.producerId);
@ assignable products;
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].getProductId() == product.getProductId());
@ ensures (\forall int i; 0 <= i && i < \old(products.length); (\old(products[i]).getProductId() != product.getProductId()) ==> (\exists int j; 0 <= j && j < products.length; products[j].equals(\old(products[i]))));
@ ensures products.length = \old(products).length;
@ also
@ public exceptional_behavior
@ signals (ProductEqualIdException e) (\exists int i; 0 <= i && i < products.length; products[i].getProductId() == productId);
@ signals (PersonIdNotFoundException e) (\forall int i; 0 <= i && i < products.length; products[i].getProductId() != productId) && !contains(product.producerId);
@*/
void addProduct(Product product)
- 购买商品方法
purchase(int customerId, int productId)
/*@ public normal_behavior
@ requires contains(customerId)
@ requires contains(getProduct(productId).advertiserId)
@ requires contains(getProduct(productId).producerId)
@ requires getPerson(customerId).isLinked(getPerson(getProduct(productId).advertiserId))
@ assignable getPerson(customerId).money, getPerson(getProduct(productId).producerId).money
@ ensures getPerson(customerId).money = \old(getPerson(customerId).money) - getProduct(productId).price;
@ ensures getPerson(getProduct(productId).producerId).money = \old(getPerson(getProduct(productId).producerId).money) - getProduct(productId).price;
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(customerId)
|| !contains(getProduct(productId).advertiserId)
|| !contains(getProduct(productId).producerId)
@ signals (RelationNotFoundException e) contains(customerId)
&& contains(getProduct(productId).advertiserId)
&& contains(getProduct(productId).producerId)
&& !getPerson(customerId).isLinked(getPerson(getProduct(productId).advertiserId))
@*/
public void purchase(int id, int product_id);
- 咨询销售额方法
querySale(int productId)
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < products.length; products[i].getProductId() == productId);
@ assignable None;
@ ensures \result = getProduct(id).getSale();
@ also
@ public exceptional_behavior
@ requires (\forall int i; 0 <= i && i < products.length; products[i].getProductId() != productId);
@ signals (ProductIdNotFoundException e)
@*/
public int querySale(int productId);
5. 学习体会
在本单元中, 学习了JML契约式编程. 首先不得不承认, 在给出JML规格描述后, 编码思路会变得十分清晰. 不过由于规格化描述要保持严谨的逻辑, 在描述一些简单的问题时就会变得相当的复杂, 可读性也会随之下降, 笔者在第二次作业中qlc指令看了好半天才意识到是在说最小生成树. 在实验和扩展中, 我也尝试了自己编写JML规格, 这是一件非常有难度的事, 需要做到不重不漏, 要将自然语言描述的事情用一条条类似于离散数学中谓词逻辑的方式进行严谨描述, 这是非常有难度的事情.
相较于前两个单元, 本单元的编码难度下降不少, 因此笔者在测试方面投入了更多的精力, 也对测试的理解更深一步.