【老物】磨练,结对编程!(中)

项目 内容
这个作业属于哪个课程 2021春季计算机学院软件工程(罗杰 任健)
这个作业的要求在哪里 结对项目-第二阶段
我们在这个课程的目标是 和团队开发真正的软件,一起提升开发与合作的能力
这个作业在哪个具体方面帮助我们实现目标 通过结对编程学习协作设计与编码、代码复审、CI使用等
成员介绍
项目 内容
结对项目第二阶段的Gitlab仓库地址 Pair Programming
二人的学号后四位
博客地址 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在涉及特殊目录做了不少特判,但实际上只可能对特殊目录进行查询与获取,是不可能增删的,所以应该在目录类Directoryget()/contains()方法里进行特判即可
    • 第四点,我确信指导书的思想是“硬链接就是一个文件内容的另一个逻辑文件名”,所以我还是认为不应该区分RegularFileHardLink,但是我们这阶段新增的INode有问题,INode与文件内容绑定,但只有HardLink有实际的文件内容,因此有必要提取出一个FileContent类进行文件内容管理,多个HardLink之间共享的是同二个INode & FileContent
      重构计划:
      (此图已挂)
    • 第五点,将所有修饰符改为public,删去无必要的异常类(现在程序有些滥用异常),删去陈冗的get()/set()以及套娃的方法控制码量
    • 当然这不会是整体的重构,只是对底层的实体类层次关系与数据结构的重构,我们会尽量保证对接口类的逻辑不产生影响,我认为本阶段程序的缺陷主要是我上阶段盲目的设计和草率的编码风格导致的。当重构不可避免时,就尽早开始吧,拖到后期只能更棘手,今晚强测后我将确认程序在整体行为逻辑上还有那些偏差,然后就开始重构吧🏃​

3.结对编程方式

本次结对编程采用了JetBrainsIDEv2020.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/mv14中情况在MyFileSystem确定新文件名与新父目录后只需考虑是否覆盖与指令类型的4种情况
  • 声明copy()的抽象方法实现复制的多态
2.FileLink(链接文件)
  • 空壳抽象类,用于区分狭义的文件与目录
3.INode(索引节点)
  • 存储硬链接文件用于共享的属性
4.FileContent(文件内容)
  • 管理链接到的硬链接文件,在文件内容修改时同步各硬链接的文件大小
  • 在硬链接自底向上更新大小访问到为null的父目录时,说明已被删,解除相应链接
  • 管理文件内容的write/append
5.SoftLink(软链接文件)
  • 主要方法只有进行重定向
6.HardLink(硬链接文件/文件)
  • 通过ln建立起硬链接的两个逻辑文件共享同二个FileContentINode
7.Directory(目录)
  • 核心类,因为FileSystem所需的核心方法都在本类中以静态方法实现
  • 管理直接的子文件/目录,进行相应的增删查,在增删方法进行父子关系的建立与解除,在查方法进行./..的特判
  • 在子文件/目录管理方法的基础上封装具有语义的方法:如添加子文件夹addChildDir(),创建硬链接文件或进行修改的方法ensureChildFile
  • 核心方法detectFileLike(),通过路径索引返回一个数据结构FileDetecter,其中FileDetecter包括倒数第二级逻辑文件、倒数第一级逻辑文件、倒数第一级逻辑文件名
  • detectFileLike()的基础上为文件系统封装具有指令语义的方法,如getFileLike()/addChildDirRecursively()/getDirectory()
8. MyFileSystem
  • 接口类,完成指令语义级别的逻辑判断,调用底层Directory类的方法进行操作

重构后冗余的绝对路径与构造方法依赖父目录删去,各模块功能相对更清晰了,码量只减少了近200行,因为只是对底层类进行了重构, MyFileSystem与其他工具类基本不受影响,我们程序下一步优化的方向是针对MyFileSystemDirectory类进行优化,MyFileSystem同时进行异常的解析与指令语义的执行,有些臃肿。本阶段新增的5条指令占据了近250行,本阶段实现时已经整合了mv/cp指令,同时考虑到ln/ln -s行为逻辑相近,可以进行整合