2020HIT软件构造实验:lab2各任务的个人理解

一. 实验目的

就如同这个实验的名字一样,这个实验的重点就在于一方面,从给定的问题中抽象出来需要设计的抽象数据类型(ADT),然后利用面向对象编程(OOP)来实现ADT。

在设计ADT的时候,由于从这个实验开始,程序已经逐渐变得复杂起来,像原来一样没有做好计划就直接写代码肯定是不行的。编写代码之前,我们需要首先写好spec和pre/post condition,以及后面的AF, RI等,同时想好对于表示泄露的处理方案。这些非代码部分虽然一开始写起来很麻烦,但是可以帮助我们在开始写代码之前先捋清楚思路,从而减少我们在后续对于代码的修改工作。
当然,比起后续的修改,更加重要的是代码的可理解性。我们今年的lab4中有一个给不写这些注释的代码debug的任务,在做这个任务的同时,我相信很多同学都和我一样,在内心里把那个不写注释的人渣枪毙几十万次了。很多时候,我们自己都看不懂我们之前写的代码。就算是为了避免养成一个会成为未来同僚的杀人动机的坏习惯,也还请一定要好好写注释(笑)。

除此之外,还有测试部分也是在代码之前完成的,也就是TDD。其好处老师在课内已经讲过,我就不再赘述。当然,TDD的前提是你的spec写的很清晰准确,后续修改不大。像我在lab2的3.3和lab3的前面的一些实现中,因为spec一开始写的不太对,而且在写测试的时候也没发现问题,结果就是我之后想改ADT,就得把我写过的测试进行相当大的变动。这一过程还是有些折磨人的。所以建议大家一定要在写测试和代码之前想好整体程序的结构,不要边做边想。

二. 新的配置内容

这个实验里面的配置部分多了一个eclemma,虽然在实验手册里面没有明说让我们用(后面的lab4才提到),但是这个东西从完善测试的角度而言还是很好用的。当然,不要过度追求覆盖度,覆盖度高自然是好事,但是100%的代码覆盖度也不意味着代码就完全地被充分地测试到了。大二的时间很紧,我们在这个实验的行为付诸实践与否,更多地要看我们这样做能学到些什么,为了一个数字而浪费太多时间就得不偿失了。

三. 实验具体内容

3.1 Poetic Walks

这个实验的目的是让我们对一个有向带权图的抽象类的子类先进行类型的构造,再扩展至<L>泛型,最后利用这个抽象类实现在输入文本的两个词之间插入的功能。比起实现这个程序本身,它更重要的作用是一步一步教会我们编写代码的正确顺序,就是先把这个函数要干嘛,或者怎么实现用注释写下来,再去写这个程序的test,最后在进行实现。

在开始说这个实验的具体实现之前,首先要提到一个坑,就是AF,RI这些概念的讲述,是略晚于实验的开始的。所以,如果发现自己在写完测试之后,就再也看不懂题干了,比起求助谷歌翻译或者金山词霸,预习一下之后的ppt可能是更好的主意。

3.1.1 写测试

这个题干的顺序其实就是让我们养成一个TDD的习惯,本身并没有什么难度,重点在于TDD的思路。注意在写测试之前,也可以先用test strategy整理一下思路。

3.1.2 两种方式实现有向带权图

  1. ConcreteEdgesGraph
    回顾lab1,我们做过一个无向图(虽然当时我们的实现也可以表示有向图),表示这个无向图的时候,我们用的字段就是图的边list和点list。即使换成了带权有向图,也只是代表边的ADT多出来了表示权值的字段而已。相对而言,由于不需要进行广搜,所以对于顶点的类,是否被访问过以及距离之类的字段就可以去掉了,剩下的也就只有一个String类型的名字而已了。既然这样,我们表示图中的每一个点,其实只需要记录它的名字即可。
    实现起来虽然多少有些麻烦,但是并没有什么难度,毕竟这个问题只是想要帮我们养成一个良好的编程习惯而已。

  2. ConcreteVerticesGraph
    比起刚刚那个问题,这个问题要求我们实现点的ADT而不去实现边的ADT,思路无非就是把连着某一个点的边当成了这个点的字段而已,实现起来比起前面那个算是有些难度,但是问题也不大。需要注意的地方是,前面那个问题中,因为边的ADT实现很简单,我们修改一条边的时候,比起对于边的ADT进行修改,倒不如直接废弃掉这条边再造一个新边来的方便,所以是immutable类型。而这个问题中,因为某一个点的ADT中记录着连着这个点的所有边,如果想要再从这个点上连出/连入一条边,就是对这个点的改动。这种情况下,废弃重建的成本就相当离谱了,所以我们这个点的ADT是mutable类型。对于mutable类型,需要进行额外的保护,这也就是这一部分主要想让我们学习到的东西。

3.1.3 实现泛型<L>

这里的修改本身其实很少,把所有实现类里面的String都改成占位符L(toString别改),然后在实现类所有的Edge或者Vertex后面加上<L>,再把test里面调用的构造器加上<L>就可以了。即使有漏掉的地方,eclipse的静态检查也会提示我们。
可以说这个问题的难点并不在于修改本身,更多的在于让我们理解泛型这一思维。

3.1.4 Poetic walks

3.1.4.1 测试

先说测试。这里就体现了我刚刚说的,eclemma覆盖度高未必就是比覆盖度低的要好,比如toString方法,测不测试都可以,就以我的测试为例,为了这一句话的覆盖度,要提前猜出toString这么大一串输出(然后用AssertEquals判定是否符合预期),并且手打出来:

“GraphPoet [graph=ConcreteEdgesGraph [vertices=[the, test, graphpoet, of, this, t@st, is, graphpoet., my], edges=[Edge [source=is, target=my, weight=1], Edge [source=my, target=test, weight=1], Edge [source=of, target=the, weight=1], Edge [source=the, target=graphpoet., weight=1], Edge [source=graphpoet., target=test, weight=1], Edge [source=of, target=test, weight=2], Edge [source=test, target=of, weight=4], Edge [source=of, target=this, weight=1], Edge [source=this, target=is, weight=2], Edge [source=is, target=t@st, weight=1], Edge [source=t@st, target=graphpoet, weight=1], Edge [source=graphpoet, target=the, weight=1]]]]”

实在是得不偿失。同时,在读入输入文件的时候,为了保证程序的健壮性,我们也需要考虑输入文件的至少以下几个可能问题:

  1. 连续多个空格
  2. 词中夹带符号
  3. 大小写混乱
  4. 空行

所以我的测试中使用的输入文件是这样子的:测试输入文件
同时为了保证正确性,我们要考虑以下情况:

  1. 输入的一对邻接词之间在图中不单向连通
  2. 输入的一对邻接词之间在图中单向连通但距离过远
  3. 输入的一对邻接词之间存在一个“桥”且唯一
  4. 输入的一对邻接词之间存在多个“桥”
  5. 输入的词不在图中

故我在test中使用的输入字符串是:“is THis my . tESt tHe . , t@st graphpoet”

从而检查出上述的全部4+5种情况。这些情况显然不是用eclemma覆盖度能检测出来的,覆盖度相同的测试代码可能其实际表现大相径庭。

3.1.4.2 实现

实现的具体思路并不复杂,就是先利用文件建立一个语料库,然后检测参数中一段话的每两个单词之间是否可以再加进来一个作为“桥”,其中这个 前面的单词(之后称为src)—桥---后面的单词(之后称为dst) 这个词组需要曾经在语料库中出现过。

注意:当两个单词之间在语料库中可以找到多个桥的时候,我们需要比较这不同的桥的权值。显然,桥的权值是基于两条边(src—桥 以及 桥—dst)的权值的。我看到的很多博客中,计算桥的权值都是将这两条边的权值相加。但我个人认为,因为我们这一权值表达的实际含义是src—桥---dst这一词组出现的可能性,因此用乘积做权值会更合适一些。
例如:边src-bridge1 的权值为200,bridge1-dst权值为1,而bridge2的两个权值均为100,那么作为src和dst之间的桥,我认为bridge2会更加合适。

3.1.4.3 附加题

这道题的思路本身是不错的,如果我们有一个足够大的语料库,然后将一次建桥行为输出作为下一次的输入,反复迭代直至输出收敛(就是输入输出相同),我们就可以利用非常简单的输入产生一个很复杂(甚至可能有着一定实际意义)的句子了。当然,收集一个庞大的语料库这件事本身就非常耗时,有兴趣且有余力的同学可以进行尝试。

3.2 Re-implement the Social Network in Lab1

这个实验是基于P1中的Graph类,对于lab1中的P3重新进行实现。尽可能地使用Graph中设计好的方法,而不是从 0开始写代码实现或者把 Lab1相关代码直接复制过来。针对getDistance方法,不能修改P1中的rep。

由于大量代码可以直接复用,这一大问题的耗费时间相当的少,也没有什么难度,因此这里就不再赘述。

3.3 Playing Chess

从这一问题开始,我们的软件构造实验可以说是开始踏入正轨了。之前我们都只是在已经给定ADT设计的基础上进行具体实现和测试,而在这里我们需要从0开始设计一套ADT,支持实现特定的功能需求。不同类之间的关系以及整体结构完全由我们自己决定,因此其实现的自由度也很高。

3.3.1 我的实现的结构

首先我需要强调一下,这个问题的自由度很高,每个人的设计肯定有所不同,我这里的实现也只能算是抛砖引玉,顶多可以作为一定的提示存在,希望不要影响或是局限了大家的思路。

我实现的各部分的关系如下图:
简易UML图

3.3.2 Board & Piece & Position

首先,我对于这个软件的入手点是从棋盘开始的。我认为,我们对于棋盘可以有着如下两种理解:

  1. 从棋盘本身的结构来考虑:类似于我们和活人一起下棋时候看到的棋盘,把棋盘当作一个1919或者88的二维数组,每个数组中存放着至多一个棋子。这种情况下,棋子的位置存在棋盘上,而不是棋子的属性。
  2. 从棋盘的功能来考虑:类似于我们看到的棋谱,棋盘从功能上是用来记录盘面上的棋子个数种类和位置的工具,那么我们就可以用一个列表来记录所有的棋子。棋子的位置作为棋子本身的属性,而棋盘就单纯只是一个棋子的聚合体。

这两种思维方式导致的是不同的类与类之间的关系。从上面的简易UML图可以看出来,我采用的是第二种理解。这样子的好处是遍历全部棋子的时候耗费的时间少(不需要考虑没有棋子的点),但是很显然在直观程度上要差很多。如果想要可视化棋盘,还得在转化成二维数组的形式来存储。所以总体上我还是建议采用第一种思维方式。

3.3.3 Action 与 Board 的关系

对于Action类,我的思路是采取一个Board当作字段,而其中的所有方法都是对于这个Board的操作。那么可能会有人问了,那我还要这Action类有何用?直接在Board里面实现方法不好吗?

从我们目前学到的内容来讲,我们在Board里面实现的方法仅限于对棋盘的基本操作。也就是说,一个棋盘,无论是进行什么棋类游戏,都会进行的基本操作,例如将某个在某位置生成新棋子,移除某位置的棋子或是移动某位置的棋子。而各种不同类型的游戏的操作,例如国际象棋的移子和吃子,围棋的落子和提子,都是对这些操作的组合和扩展。这些扩展操作,就可以放在Action类中,因为这样做并不会影响到Board内部,也方便维护和进一步的扩展。

从我们以后会学到的内容来讲,这个将基础操作和扩展操作的思路,有点类似于5-2的Visitor模式(但实际上并不一样!注意区别!)。Action类似于抽象类Visitor,泛指一切棋类操作,而每个Action的子类型作为一种棋类的全体操作的实现,想要完善某种棋类的功能或者增加新的游戏种类的时候,直接修改或者添加对应的子类即可。

3.3.4 Game & Player

Player这个ADT的实现本身很简单,就是包含了用户名字和所使用的棋子的颜色信息即可。(我这里锁定player1执白,player2执黑,所以会出现在围棋中player2先手而国象中player1先手的情况)

相对而言,Game的实现要复杂很多,甚至可以说包含了一半以上的工作量。由于两种类型的游戏共性操作很多,为了避免重复代码,可以考虑将共性操作提到父类抽象类Game中实现。而个性化的操作就可以分别放在子类中了。仅就我个人经验而言,整个lab2中最令人难受的部分就是一大堆输入的判定,为了提升程序的健壮性,需要把用户当作敲莎士比亚的猴子,对任何错误的输入都要进行错误提示以及要求用户进行重输。我的建议是可以将某种共性的输入要求方式(比如输入数字或者字母等)提出来单独作为一个辅助方法来实现,从而尽量减少重复代码(也就减少了我们的工作量)。另一方面,建议大家提前学习一下正则表达式的使用(反正lab3里面早晚要用),对于健壮性判定代码简化有着很大的帮助。

可视化的部分如果用前面提到的Board思路1实现,这里就很简单,直接把数组元素打出来就可以了。执行情况如下:
在这里插入图片描述

四. 总结(建议先看这部分)

  1. 这个实验里面P1的目的在于让我们养成一个良好的习惯,内容会比较繁琐。而P3是让我们初步进行一个编程的工作,对于不太熟悉没有经验的同学而言可能会比较困难。从时间上来看,三者的比例大约是4:0.5:5.5,而P3中最耗时间的部分在于Game,所以要注意时间的分配。
  2. 注意总体时间的规划。这门课的一个很大的特点是课程内容会晚于用到相应内容的实验的发布,所以等到老师讲完再写实验,时间上会很紧张。因此,希望大家一方面注意时间的安排,另一方面不要依靠老师讲课,提高自己的预习能力,才能不至于在ddl的前几天过于紧张。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值