磨练,结对编程!(中)
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2021春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目-第二阶段 |
我们在这个课程的目标是 | 和团队开发真正的软件,一起提升开发与合作的能力 |
这个作业在哪个具体方面帮助我们实现目标 | 通过结对编程学习协作设计与编码、代码复审、CI使用等 |
成员介绍
项目 | 内容 |
---|---|
结对项目第二阶段的Gitlab 仓库地址 |
Pair Programming |
二人的学号后四位 | 3110, 3142 |
博客地址 | MadokaHomura(朱正阳), zixfy(赵子轩) |
前情回顾
一、结对体验
1.From zzy
本次任务我主要负责test
部分。随着指导书的需求增多以及异常情况的复杂,任务的难度也有很大的提升。我认为此次任务的难度主要集中在文件系统中对软/硬链接的理解,以及软/硬链接在不同指令下的表现。
由于此次作业难度的增大,我和zzx之间的讨论激烈程度也增加了。我认为这相较于上次我的单方面听从指挥有较大的进步。尽管大多数时候我的想法或观点经过讨论之后是有些问题的,但我认为能够提出问题,增进交流,也是一种进步。
虽然这次我没有当driver
,但我也没能完成Navigator
的职责,主要设计仍然是由zzx完成的,我只是在此基础上提出了一点建议。类与方法的具体实现我也没有参与其中。test
方面做到也不够好,由于oo
期间就没有认真做单元测试,我这次做测试完全是摸着上次作业的测试走路,刚开始测试效率很低。在zzx完善了测试工具类之后,测试效率才提高(甚至有时候我还写出一些错误的测试误报bug浪费时间,着实是帮倒忙)
总的来说,这次结对算是一种全新体验,虽然思路、理解、设计方面较上次有更多的参与,但是我实际编码和测试做的都不尽人意,希望下一次能够有所突破吧。
2.From zzx
- 我对自己在这阶段的表现不满意 因为近两周课业繁重其实并没有全身心投入结对编程,为了能尽早完成编码任务没能和上阶段一样就每一部分的具体编码细节与zzy进行深入讨论。我想说下个阶段需要充分利用每一分钟了😅
- 这次新加入的
ln [-s]
对第一阶段的程序架构产生了冲击,原有设计无法顺利增量开发,代码量的陡增(1,611)与不少冗余的产生让我有了很重的危机感;今天我仔细地想了一下,第一阶段的一些设计暴露出了许多问题,我此刻的想法如下:- 每个文件/目录(逻辑上的文件)都存储本身的绝对路径是个很蠢的设计,每条指令至多输出一条(非指令参数中的)绝对路径,更优解应该是需要输出时倒序拼接成绝对路径,进一步优化就是加入绝对路径的
cache
,逻辑文件被移动才可能更改绝对路径。存储绝对路径也让我在mv/cp
里感到很麻烦,需要递归设置文件的新路径,逻辑文件应该只保存name/size/count/father
,其他的基本信息由INode
管理 - 其次,程序在进行逻辑文件构造时要求传入父目录这个参数,我认为这也是存储绝对路径带来的衍生问题,没有父目录便确定不了绝对路径,但设置父子文件关系的功能应该下放到
Directory
去管理,这个地方产生了很违和的冗余 - 第三点,对于特殊目录
./..
的处理,上阶段后我们将这两个目录放进了目录的子文件Map
中,但这样直接混淆了特殊目录与真实的子文件,导致mv/cp
在涉及特殊目录做了不少特判,但实际上只可能对特殊目录进行查询与获取,是不可能增删的,所以应该在目录类Directory
的get()/contains()
方法里进行特判即可 - 第四点,我确信指导书的思想是“硬链接就是一个文件内容的另一个逻辑文件名”,所以我还是认为不应该区分
RegularFile
与HardLink
,但是我们这阶段新增的INode
有问题,INode
与文件内容绑定,但只有HardLink
有实际的文件内容,因此有必要提取出一个FileContent
类进行文件内容管理,多个HardLink
之间共享的是同二个INode & FileContent
重构计划:
- 第五点,将所有修饰符改为
public
,删去无必要的异常类(现在程序有些滥用异常),删去陈冗的get()/set()
以及套娃的方法控制码量 - 当然这不会是整体的重构,只是对底层的实体类层次关系与数据结构的重构,我们会尽量保证对接口类的逻辑不产生影响,我认为本阶段程序的缺陷主要是我上阶段盲目的设计和草率的编码风格导致的。当重构不可避免时,就尽早开始吧,拖到后期只能更棘手,今晚强测后我将确认程序在整体行为逻辑上还有那些偏差,然后就开始重构吧🏃
- 每个文件/目录(逻辑上的文件)都存储本身的绝对路径是个很蠢的设计,每条指令至多输出一条(非指令参数中的)绝对路径,更优解应该是需要输出时倒序拼接成绝对路径,进一步优化就是加入绝对路径的
3.结对编程方式
本次结对编程采用了JetBrains
系IDE
自v2020.2.x
起支持的Code With Me插件,相比git
协作实时性更强,效果拔群总体使用的体验还是不错的,但也存在一些问题:网络连接不稳定时会出现诸如不给提示、复制粘贴失败、代码写不上等现象。本次的代码全部是以zzx为host完成的。下附工作截图
二、设计与实现
1.思路
a. 用户系统
首先是新增的用户系统类与原文件系统类之间的交互方式,本阶段我们使用了单例模式。同时考虑到AppRunner
中是先实例化文件系统后实例化用户系统的,因此文件系统构造后无法获得root User
的引用,因此用户系统构造后需要通知其设置root
用户
public class FileSystemPluginManager {
private static MyFileSystem fs;
private static MyUserSystem us;
public static void setUpFileSystem(MyFileSystem fs) {
FileSystemPluginManager.fs = fs;
}
public static void setUpUserSystem(MyUserSystem us) {
FileSystemPluginManager.us = us;
fs.notifyUserSystemSetup();
}
public static void notifyUserExited() {
fs.notifyUserExited();
}
...
}
用户系统相对独立而且易实现,其大致数据结构为: UserSystem
管理全局用户表(Hashmap
)与用户组表(Hashmap
);UserGroup
管理组内用户表(Hashmap
),记录作为其主组的用户数;User
管理其所属的用户组表(Hashmap
);特别地,名字均不为root
b.一切皆文件
Linux
中文件系统的一大核心思想是一切皆文件,文件/目录都拥有一个索引节点(iNode
),其中文件iNode
存储文件内容,而文件夹存储的是子文件列表。地别地,硬链接文件与一个普通文件共享一个iNode
,亦或普通文件就是一个硬链接文件,而本阶段中的软链接文件可以理解为一个存储了用于重定向的路径的文件,因此我们提取出一个INode
类用于存储creator, modifyTime, fileContent
等实际的文件信息,而FileLike
类存储AbosulatePath, size, name
等逻辑上的文件信息。
文件系统中的实体关系图如下,其中FileLink
是区别于目录的链接型文件,包括软链接文件(SoftLink
)与硬链接文件/文件(File
),注意到Directory/SoftLink
所引用的INode
实例并没用存储子目录列表/重定向路径,而是在本类中存储这些属性,从现实中到面向对象的转换中,这里产生了一定的冗余
关于软链接的重定向,由于上阶段中索引文件/目录分为两种方法,一种是获得最后一级文件/目录,一种是获取其父目录(考虑到mkdir/touch
时其可能不存在的情况),而加入软链接后,比如touch
指令中如果最后一级文件/目录是软链接,需要继续重定向,编写起来很冗余,因此整合了索引文件/目录的两种方法,保证在完全重定向的情况下保证最后一级文件/目录不是软链接,并且对于/
结尾的路径,在以获取文件的意向进行索引或最后一级文件/目录确实是文件类型时直接抛出路径非法的异常
关于硬链接对INode
的共享,硬链接文件与INode
的链接关系在创建普通文件时隐式或通过ln
显式创建。当INode
文件大小修改时通知所有linked
的硬链接文件进行自底向上的更新,但rm [-r]
时理应递归解除所有硬链接文件和INode
的链接关系,但可以这样懒更新进行优化:当文件系统中某一文件/目录被删除,其父目录被设置成null
,硬链接文件自底向上更新大小时若遇到null
,说明文件已被删除,此时再接触硬链接文件和INode
的链接关系
2.测试
a. 第一阶段DEBUG
代码覆盖率测试固然可以十分全面地调试程序在各种可能的情况下的行为,但对于自己应该写但却没有写的代码却是无能为力的。而且做到代码测试全覆盖也无法保证程序的最终质量,我们在第一阶段中产生的Bug
都是源于对极端/特殊数据的测试遗漏,这也启发我们在完成代码主体的覆盖率测试后应当更加注重程序对极端情况的处理,保障程序的鲁棒性与健壮性
b. 本阶段测试
在第一阶段过后,我们发现测试过程中有两个降低编写测试代码效率的问题:
i) 对于期望异常抛出的测试
在上阶段中异常捕获测试使用的是@Test
注解与以下方法
Exception expected = null;
try {
doSomeThing();
} catch (Exception e) {
expected = e;
}
Assert.assertTrue(expected instanceof MyException);
为了提升代码可读性与简化编码,我们将上述方法利用泛型封装成异常测试类ExceptionTestAble
@Test
public void deleteGroup() throws UserSystemException {
us.addUser("test");
new ExceptionTestAble(MyUserSystemException.class) {
@Override
public void run() throws Exception {
us.deleteGroup("test");
}
}
};
ii) 黑盒测试
在进行各模块的单元测试后进行程序整体功能的测试,但如果在单元测试函数中根据指令直接去调用FileSystem/UserSystem
的函数,指令数较多时降低了测试代码的可读性,并且写起来麻烦
在尝试重定向I/O
直接调用AppRunner
进行输入输出捕获时发现在多个@Test
方法中调用AppRunner
时貌似由于其获取输入使用的是Scanner
而产生了线程问题,遂弃之。我们封装了一个对拍机类用于FileSystem/UserSystem
的构造,输入指令序列的解析与期望输出比对,可以使测试用例编写者只关注构造测试样例的数据本身
@Test
public void test78() {
FileSystemTester.quickTest(
Arrays.asList("mkdir /buaase",
"cd buaase",
"mkdir -p /buaase/dir1/dir2",
"ls dir1",
"touch /buaase/file.txt",
"info /buaase"),
Arrays.asList("Make directory /buaase",
"/buaase",
"Make directory /buaase/dir1/dir2",
"dir2",
"root root 1 5 0 2 /buaase")
);
}
由于指导书issue
部分存在很多边界情况和思维盲区,我们吸取上次强测的教训,对issue
中涉及到的情况也都进行了测试。
最后一次提交的测试报告如下图所示
三、工作量
1.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 5 |
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 1660 | 1880 |
· Analysis | · 需求分析 (包括学习新技术) | 180 | 155 |
· Design Spec | · 生成设计文档 | 30 | 10 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 120 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 240 | 300 |
· Coding | · 具体编码 | 420 | 480 |
· Code Review | · 代码复审 | 120 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 600 | 660 |
Reporting | 报告 | 120 | 100 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 10 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 80 | 65 |
合计 | 1785 | 1950 |
2.码量
Java | 总行数 | 代码行数 | 注释行数 | 空行数 |
---|---|---|---|---|
Src | 2,046 | 1,611 | 167 | 268 |
Test | 2,052 | 1,770 | 47 | 235 |
Total | 4,098 | 3,381 | 214 | 503 |
(update at 4.3)
已重构, 主要针对的是解决上文中提到的各模块功能划分不明确,底层实体关系混乱而导致的冗余与高耦合的问题,重新划分为Directory(目录)、INode(索引节点)、FileContent(文件内容)、FileLink(链接文件)、SoftLink(软链接文件)、HardLink(硬链接文件/文件)、FileLike(逻辑文件)
1.FileLike(逻辑文件)
- 负责实现
Directory/FileLink
的共性方法,主要有三:getAbsPath(FileSystem)
以通过文件系统获取逻辑文件的绝对路径,updateSize(int)/updateWhenMove(int)
用于自底向上更新大小/递归修改更新时间,moveToDir(...)
实现将逻辑文件转移到另一文件夹的功能,我们分析出cp/mv
的14
中情况在MyFileSystem
确定新文件名与新父目录后只需考虑是否覆盖与指令类型的4
种情况 - 声明
copy()
的抽象方法实现复制的多态
2.FileLink(链接文件)
- 空壳抽象类,用于区分狭义的文件与目录
3.INode(索引节点)
- 存储硬链接文件用于共享的属性
4.FileContent(文件内容)
- 管理链接到的硬链接文件,在文件内容修改时同步各硬链接的文件大小
- 在硬链接自底向上更新大小访问到为
null
的父目录时,说明已被删,解除相应链接 - 管理文件内容的
write/append
5.SoftLink(软链接文件)
- 主要方法只有进行重定向
6.HardLink(硬链接文件/文件)
- 通过
ln
建立起硬链接的两个逻辑文件共享同二个FileContent
与INode
7.Directory(目录)
- 核心类,因为
FileSystem
所需的核心方法都在本类中以静态方法实现 - 管理直接的子文件/目录,进行相应的增删查,在增删方法进行父子关系的建立与解除,在查方法进行
./..
的特判 - 在子文件/目录管理方法的基础上封装具有语义的方法:如添加子文件夹
addChildDir()
,创建硬链接文件或进行修改的方法ensureChildFile
- 核心方法
detectFileLike()
,通过路径索引返回一个数据结构FileDetecter
,其中FileDetecter
包括倒数第二级逻辑文件、倒数第一级逻辑文件、倒数第一级逻辑文件名 - 在
detectFileLike()
的基础上为文件系统封装具有指令语义的方法,如getFileLike()/addChildDirRecursively()/getDirectory()
8. MyFileSystem
- 接口类,完成指令语义级别的逻辑判断,调用底层
Directory
类的方法进行操作
重构后冗余的绝对路径与构造方法依赖父目录删去,各模块功能相对更清晰了,码量只减少了近200
行,因为只是对底层类进行了重构, MyFileSystem
与其他工具类基本不受影响,我们程序下一步优化的方向是针对MyFileSystem
与Directory
类进行优化,MyFileSystem
同时进行异常的解析与指令语义的执行,有些臃肿。本阶段新增的5
条指令占据了近250
行,本阶段实现时已经整合了mv/cp
指令,同时考虑到ln/ln -s
行为逻辑相近,可以进行整合