本次实验覆盖课程第 2、3 章的内容,目标是编写具有可复用性和可维护性的软件,主要使用以下软件构造技术:
l 子类型、泛型、多态、重写、重载
l 继承、委派、CRP
l 语法驱动的编程、正则表达式
l 设计模式
本次实验给定了多个具体应用,学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的 ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使 ADT 有更大程度的复用(可复用性)和更 容易面向各种变化(可维护性)。
https://github.com/ComputerScienceHIT/HIT-Lab3-2021110834
- 行星系统
- 原子结构系统
- 社交网络系统
这三个系统都是轨道系统的,但是在行星系统中一个轨道只有一个物体(行星),还要考虑物体的绝对关系;而在原子结构系统中,没一个轨道可以有多个物体,并且原子结构中的电子没有区别都是一样的,而在社交网络系统中要考虑不同物体之间的关系以及关系的变化。
我将基于语法的图数据输入放在了每一个具体的系统类的方法中,首先从外界读取文件的地址,在利用BufferReader来读取txt文件内容,并且用一个字符串列表来储存每一行的内容。在基于语法输入,先创建模板,再将不同的数据读取到不同的变量里面,利用这些变量去创建对象。
-
- 面向复用的设计:CircularOrbit<L,E>
3.1.1 接口设计
首先我建立了一个CircularOrbit<L,E>接口,用于存放一些通用的方法(在接口中已经为每一个方法写了Spec)。
public static <L,E> ConcreteCircularOrbit<L,E> getCircularOrbit();
boolean addTrack(Track track);
boolean remove(Track track);
boolean addCenter(L center);
boolean addObject(Track track,E physicalObject);
public int getTrackNumber();
public int getObjectNumber();
public List<Track<E>> getAllTracks();
public List<E> getAllObject();
public L getCenter();
3.3.2 实现类设计
再编写完接口后,我建立了一个ConcreteCircularOrbit<L,E>实现类去实现了接口的所有方法。而实现类里面的数据包括一个类型为L的中心物体,一个E的列表来表示系统里的所有物体,一个Track的列表,来表示系统的所有轨道。
3.3.3 完成具体类的设计
在编写玩实现类后,就开始针对不同的应用场景去编写具体的实现类,这里选择使用继承或委派关系去实现,我选择了委派关系,即再每一各具体的系统类里面的数据有一个ConcreteCircularOrbit类,这里考虑到不是每一个系统都要应用实现类里的所有方法,而且采取继承关系后子类有些束手束脚。
3.3.3.1 StellarSystem
恒星系统中的数据只有一个轨道系统实现类。
方法如下(图片由于大小原因仅展示了前一部分方法,以下两个轨道也同理):
两个构造器,一个有参(参数是中心天体)、一个无参(主要用于使用基于语法输入的系统)添加中心物体、加入天体、获取轨道数量或者星球数量、从文件中构造系统(前几个小节已详细讲过如何实现)、得到行星的实际位置(通过行星原本的角度,再计算经过t时间后的角度,将其储存在一个Map中返回)、得到系统的熵值以及星球间的实际距离(这两个方法具体实现细节在后几节阐述)、检查方法(用于检测这个系统是否合法)、绘制方法(将系统可视化)。
3.3.3.2 AtomStructure
原子结构系统的数据也是只包括一个实现类。
方法如下:
两个构造器,一个有参(元素类型)、一个无参,添加中心元素,添加电子(需要电子的轨道号),获得原子的轨道数和电子数,获得所有的轨道,电子跃迁(传入参数是一个源头轨道一个目标轨道,由于所有的电子都是等价的,所以我在这里采用的测略是将源头轨道删除一个电子,在向目标轨道加入一个电子),获得系统熵值,系统可视化,从文件中构造系统。
3.3.3.3 SocialNetworkCircle
这个系统的数据和前两个有些许不同,由于人际关系系统要考量人与人之间的关系,所以我把Lab2中的有向图系统在Lab3重新利用,在数据中不仅有一个轨道系统实现类还有一个有向图。
方法如下:
依然是两个构造器,加入中心物体、朋友,得到轨道数、朋友数、熵值、从文件构造系统、可视化等方法和前两个系统一致,独特的方法有加入中心人和朋友的关系,加入或朋友之间的关系,计算朋友之间的逻辑距离。这些方法都是由这个系统自带的有向图系统实现的,要注意的是在这个人际关系中关系没有方向性,所以向有向图中加入新边要一下加入两条。
3.3.4 编写测试用例
在完成三个具体系统的构造后,开始为实现类和三个应用类编写测试用例,下图为行星系统的一部分测试用例。
-
- 面向复用的设计:Track
在编写Track是使用了泛型去应对不同系统的不同轨道。Track中包含的数据有一个表示半径的Number类型和一个物体的集合。
方法如下:
一个有参构造器(参数是轨道半径),想轨道中加入或删除物体,返回轨道的半径或者所有物体的集合,最后重写equals方法来根据半径判断两个轨道是否相同。
L表示的是中心物体的类型,这这次实验中总共有三种不同的物体类型,所以我通过一个继承树来表示它们。
中心物体的公共父类是Center类,它属性仅包括一个由自定义的label类型的名字,方法只有一个有参构造器和一个获取字符串格式名字的方法。
Center类共有三个具体的子类包括恒星、中心人、原子核。其中各自的属性包括各自物体的属性,方法都是一个有参构造器和一些get方法。后来由于要应用Lab2中的有向图系统,所以在人际关系中中心物体和轨道物体需要是同一个类型,所以最后CenterPerson类弃置不用。
-
- 面向复用的设计:PhysicalObject
E表示的是物体类型,这这次实验中总共有三种不同的物体类型,所以我通过一个继承树来表示它们。
轨道物体的公共父类是PhysicalObject类,它的属性就是一个自定义的label类来表示名字。方法有一个有参构造(名字),获取名字和角度的方法,和重写equal来根据名字判断相同的类。
行星类:
电子类:
朋友类:
以上三个具体的轨道物体类都包括有参构造器,get方法以及一些自己的属性。
-
- 可复用API设计
本次实验中要求可复用的API有三个,计算系统的熵值、计算两物体的实际距离、计算两物体的逻辑距离。
计算系统熵值:通过网上查阅相关资料,了解系统熵值的定义和计算方法。
计算两物体实际距离:主要涉及利用三角函数、余弦定理等数学公式进行计算。
计算两物体的逻辑距离:这里利用的是Lab2中的社交关系系统计算两人之间逻辑最短距离的思路进行计算。
-
- 图的可视化:第三方API的复用
我这次实验所用的图形可视化工具是Java自带的Java swing,具体操作是首先写一个CircularOrbitHelper类去继承JFrame类,它的属性包括一Sketchpad类(继承自Jpanel类),和一个轨道系统实现类(这样每一个具体的轨道系统都能应用同一个画图类)。画图的步骤如下:
初始化画板,将画板嵌入画框,设置画板的大小(绘图区域),绘制中心物体为一个30*30像素的实心黑球,在依次绘制每一条轨道,再在轨道上绘制轨道物体(20*20像素的实心黑球)。
- 在构造Track、PhysicalObject 等对象时,使用 factory method 设计模式。
- 在 StellarSystem 应用中,使用 decorator 设计模式,为某些行星增加一颗或多颗“卫星”——原需求中,轨道上的行星是单个的;在该新需求中,轨道上的行星可能携带一颗或多颗卫星。
- 使用迭代器模式,客户端可在遍CircularOrbi对象中的各 PhysicalObject 对象时使用,遍历次序为:从内部轨道逐步向外、同一轨道上的物体按照其角度从小到大的次序(若不考虑绝对位置,则随机遍历)。这里展示了一个公共的迭代器的接口,在每一个具体的类里去实现它。
我将这三个应用放在了同一个主程序里,用户通过选择进入对应的具体程序或者退出。
行星系统进入后默认读取文件并且进行可视化,之后可以选择有六个功能(添加行星、删除行星、计算熵值、计算某段时间后行星位置、计算两个行星之间的物理距离)。在完成每一项功能后稍微移动3图形界面即可完成刷新,在功能完成后会有提醒是否退出系统。
原子结构系统进入后默认读取文件并且进行可视化,之后可以选择有四个功能(添加电子、删除电子轨道、计算熵值、进行电子跃迁)。在完成每一项功能后稍微移动3图形界面即可完成刷新,在功能完成后会有提醒是否退出系统。
社交系统进入后默认读取文件并且进行可视化,之后可以选择有七个功能(添加新的朋友、删除朋友、计算熵值、回复结构、增加社交关系、删除社交关系、计算两人之间的逻辑距离)。在完成每一项功能后稍微移动3图形界面即可完成刷新,在功能完成后会有提醒是否退出系统。
根据OCP原则,对于修改封闭,所以我建立了一个Track的子类Elliptical类,用来表示椭圆轨道,类中主要包括两个Number类的数据表示椭圆的长短半轴,还有一些构造方法和get方法,以及重写equal和hasCode方法。
由于在轨道已经变成椭圆形后,已有的addPlanet方法已经不适合,所以我重载了该方法。
这里变化的要求是使原子核可以表示成为中子或者质子的集合,所以我建立了一个Atom_Change类,属性包括一个Set用来储存中子或质子,还有有add方法可以向其中添加核子。
变化要求社交关系是单向关系,由于之前要求的是双向关系,所以在加入关系时,向有向图系统中加入了两条边,Change后,建立一个新的方法,在加入关系时只加入一条边。
-
- Git仓库结构
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
日期 | 时间段 | 计划任务 | 实际完成情况 |
4/11/2023 | 15:45-17:30 | 学习并完成正则表达式 | 按时完成 |
4/12/2023 | 19:00-20:30 | 完成部分3.4轨道系统接口 | 按时完成 |
4/17/2023 | 13:45-15:30 | 完成3.4轨道系统实现类 | 按时完成 |
4/18/2023 | 15:30-16:30 | 完成3.4第一个具体类 | 按时完成 |
4/20/2023 | 19:00-20:30 | 完成3.4后两个具体类 | 按时完成 |
4/21/2023 | 19:00-20:30 | 编写测试用例 | 按时完成 |
4/23/2023 | 19:00-20:30 | 编写测试用例 | 按时完成 |
4/25/2023 | 19:00-20:30 | 完成3.5轨道类 | 按时完成 |
4/26/2023 | 19:00-20:30 | 完成3.6中心物体继承树 | 按时完成 |
4/28/2023 | 19:00-20:30 | 完成3.7轨道物体继承树 | 按时完成 |
4/29/2023 | 19:00-20:30 | 完成3.8可复用API | 按时完成 |
4/30/2023 | 19:00-20:30 | 完成3.9设计模式的应用 | 按时完成 |
5/1/2023 | 19:00-20:30 | 学习可视化方法 | 按时完成 |
5/2/2023 | 19:00-20:30 | 完成可视化操作 | 按时完成 |
5/3/2023 | 19:00-20:30 | 完成主程序APP和第一个程序APP | 按时完成 |
5/4/2023 | 19:00-20:30 | 完成后两个程序APP | 按时完成 |
5/5/2023 | 19:00-20:30 | 完成除3.12前的实验报告 | 按时完成 |
5/6/2023 | 19:00-20:30 | 完成3.12新的变化 | 按时完成 |
5/7/2023 | 16:00-16:30 | 实验报告收尾 | 按时完成 |
遇到的难点 | 解决途径 |
基于语法的输入完全不了解 | 从网络教程上学习正则表达式和用法 |
不知道如何求解系统的熵值 | 从网上找到具体的算法 |
不会使用可视化工具 | 从别人的建议了解到Java Swing可以用来画图而且不用使用外部的库 |
在这次实验中,我三个轨道系统都使用了委派,将轨道系统实现类放置在每一个每一个具体的轨道系统中,当时考虑的是使用委派可以减少很多由继承所带来的限制,比如子类将继承父类所有的方法而有些子类用不上。但是在之后的实验过程中我也自食恶果,主要的麻烦有两个:一个是由于每一个有关轨道系统的操作我都放置在轨道系统实现类中,所以我在外部使用具体类时如果想调用这些方法就需要在具体类的方法里重现一遍方法(虽然就时调用实现类中的方法),这就导致了一个方法我要写四遍;另一方面,在使用一些公用的静态方法中,我也不能直接将具体类传入(应为方法要求传入的是实现类),又造成了很多麻烦。由此可见,在这种情景下还是继承关系更加方便。
- 重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在五个不同的应用场景下使用,你是否体会到复用的好处?
面向ADT的编程可以大大地提高代码的可复用性、可维护性、也有利于之后对于程序的修改,而直接面向应用场景编程就无法复用代码,以至于反复“造轮子”。复用的好处不仅在于可以省下重复工作的事件,而且可以在编程设计时更加全局化的思考,更有利于开发出对于客户端更加友好的程序。
- 重新思考Lab2中的问题:为ADT撰写复杂的specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
spec可以帮助自己和别人快速回忆或了解方法的用途,前置条件、后置条件以及输出值。RI、AF有益于我们在编程时时刻牢记客户端的要求以规范自己的代码,注意表示泄露则是避免将类内部的数据直接泄露给客户端以防随意修改内部数据。我认为这些都是编程工作的好习惯,在系统性大型编程操作中是很有必要的。
- 之前你将别人提供的API用于自己的程序开发中,本次实验你尝试着开发给别人使用的API,是否能够体会到其中的难处和乐趣?
自己编写API的时候需要考虑方方面面的约束条件和规范,还要尽力为客户端提供更良好的使用体验,同时兼顾复用性、可维护性等特性,确实有些复杂,但是如果最后能够将API完整开发还是很有成就感的事情。
- 在编程中使用设计模式,增加了很多类,但在复用和可维护性方面带来了收益。你如何看待设计模式?
设计模式是大家在实际工作中写的各种代码进行高层次抽象的总结,每一种设计模式都是前人工作经验的凝练,是对于继承关系和委派关系灵活应用的结果。在编程中使用设计模式无疑可以提高编程的效率,还有利于其他人快速了解系统。
- 你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个解析器,使用语法和正则表达式去解析输入文件并据此构造对象。你对语法驱动编程有何感受?
语法驱动的编程可以高效的识别文件的类型,可以将繁琐的输入读取操作一步完成,而且更加模板化,可读性也得到了很大的提升。
- Lab1和Lab2的大部分工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验是你完全从0开始进行ADT的设计并用OOP实现,经过三周之后,你感觉“设计ADT”的难度主要体现在哪些地方?你是如何克服的?
利用别人设计的ADT时,在原有的框架里填充内容,自己只需要依次思考某一个方面的算法或程序,相当于管中窥豹。而自主设计ADT就需要在设计时还需要自己设计整个ADT的继承、委派关系,更改某一个类的数据和方法都有可能影响它的子类或者委派它的类,相较前两个实验更要求具有系统思维,也容易出现一步错、步步错的结果,导致最后效果与预期相距甚远(前两个实验有框架的约束导致错误的设计不太容易传递)。
要克服这些问题最重要的就是在设计ADT时明确“用户”的需求,考虑全面,同时编写测试用例,时刻纠正自己程序,以防偏离“航向”。
- 你在完成本实验时,是否有参考Lab4和Lab5的实验手册?若有,你如何在本次实验中同时去考虑后续两个实验的要求的?
我想去参考,可惜没有Lab4、Lab5。
- 关于本实验的工作量、难度、deadline。
工作量较大,难度较大,deadline比较急迫。
- 到目前为止你对《软件构造》课程的评价。
经过软件构造课程的学习,我不仅掌握了Java编程技术,还对ADT、OOP以及一些编程思想有了更多的了解和体会,在进行软件构造实验的时候也曾遇到许多困难,最后通过自己的努力也都一一克服,大大提高了系统性编程的能力,我认为开设这门课程是十分有益且有必要的。