目录
1 实验目标概述
本次实验覆盖课程第 2、3 章的内容,目标是编写具有可复用性和可维护性 的软件,主要使用以下软件构造技术:
- 子类型、泛型、多态、重写、重载
- 继承、委派、CRP
- 语法驱动的编程、正则表达式
- 设计模式
本次实验给定了多个具体应用,学生不是直接针对每个应用分别编程实现, 而是通过ADT 和泛型等抽象技术,开发一套可复用的ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使ADT有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
2 实验环境配置
本次实验使用IntelliJ IDEA开发,使用Maven环境。
3 实验过程
3.1 待开发的三个应用场景
1 TrackGame
3 AtomStructure
5 SocialNetworkCircle
径赛场地赛程编排(TrackGame):在本实验中,我们将场地形状简化成圆形。圆形周围划分为等距的若干跑道,在短距离赛跑比赛中每个跑道容纳1名运动员(本实验 无需考虑长距离赛跑中运动员不区分跑道的情形)。场地中心点无物体,无需考虑各跑道的半径,将各运动员看作独立个体(彼此之间不存在关系),比赛过程中运动员不能切换跑道。 给定一组运动员,通过比赛方案编排算法,将他们分为n组,为每组内的每个运动员分配具体的跑道。从而,每一组运动员就形成了一个轨道系统。
原子结构模型(AtomStructure):一个原子由原子核和绕核运动的电子组成,原子的质量主要集中在由质子和中子构成的原子核上,核外分布着电子。在物理学家Rutherford于 1909年提出和Bohr于1913年改进的核结构模型中,原子可看作是以原子核为中心的多条电子轨道的结构。本实验中,将原子核作为整体看作中心点的物体,无需考虑其内部的质子和中子。无需考虑电子之间的关系。电子在轨道的分布遵循特定的原理,这里无需考虑电子的绝对位置(Heisenberg 的测不准原理),也无需考虑电子在轨道上的运动。但电子可以在不同轨道之间跃迁。
社交网络的好友分布(SocialNetworkCircle):在一个社交网络中, 用户a有一级好友、二级好友、…、n级好友,其中一级好友是指与a直接有社交关系的人,i级好友是指与 a 无直接的社交关系,但是与 a 的 第 i-1 级好友有直接社交关系的人。因此,若以a为中心点,则可将其 各级好友表示为多轨道的结构,第i条轨道上分布着a的i级好友,一个人只能出现在一条轨道上。
可以看出共性在于他们都符合轨道系统的结构。其中TrackGame没有中心物体,AtomStructure和SocialNetworkCircle都有唯一的中心物体。TrackGame每条轨道上只能有一个物体。只有SocialNetworkCircle需要处理对象之间的关系。
3.2 基于语法的图数据输入
3.3 面向复用的设计:CircularOrbit<L,E>
- 定义泛型接口CircularOrbit<L,E>,作为轨道系统的顶层接口。
- 其中,L为中心物体的类型,E为轨道物体的类型
- 定义下列函数:
- 迁移轨道物体至某条轨道void transit(E obj, Track t);
- 在轨道上移动轨道物体至一定角度void move(E obj, double angle);
- 添加一条轨道void addTrack(Track t);
- 移除一条轨道void removeTrack(Track t);
- 添加一个中心物体void addCentralObject(L obj);
- 添加一个轨道物体 void addPhysicalObject(Track t, E obj);
- 删除一个轨道物体void removePhysicalObject(E obj);
- 清除所有轨道物体void clearPhysicalObject();
- 添加一个关系void addRelation(Object a, Object b, double dist);
- 删除一个关系void removeRelation(Object a, Object b);
- 获取关系连接的目标物体,已经相应的边权Map<Object, Double> getRelationTargets(Object obj);
- 获取关系连接的源物体,已经相应的边权Map<Object, Double> getRelationSources(Object obj);
- 获取所有轨道,按半径从小到大排列List getTracks();
- 获取所有轨道物体Set getPhysicalObjects();
- 获取某条轨道上的所有轨道物体Set getPhysicalObjects(Track track);
- 获取轨道物体所在的轨道Track getObjectTrack(E obj);
- 获取物体所在的角度位置double getObjectAngle(E obj);
- 获取所有中心物体的集合Set getCentralObjects();
- 删除某个中心物体void removeCentralObject(L obj);
3.4 面向复用的设计:Track
定义一个不可变类型Track,仅需一个成员字段radius表示半径:
private final double radius;
重写equals,hashCode和toString方法
equals仅需比较半径是否相等:
if (this == obj) return true;
if (!(obj instanceof Track)) return false;
Track other = (Track) obj;
return radius == other.radius;
hashCode使用成员变量radius的hashCode即可:
return Double.hashCode(radius);
3.5 面向复用的设计:Person
由于SocialNetworkCircle 中的CentralUser和Friend类型字段完全相同,抽象出Person类型作为他们的共同基类。
定义不可变类型Person表示人,它是CentralUser和Friend的基类型。
Person需要三个字段,分别表示姓名、年龄、性别。
private final String name;//姓名
private final int age;//年龄
private final char gender;//性别
重写hashCode,用三个字段的hashCode做计算的结果作为Person的hashCode
return name.hashCode() * 1331 + Integer.hashCode(age) * 13 + Character.hashCode(gender);
重写equals,三个字段同时相等时,Person对象才相等
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
return name.equals(other.name) && age == other.age && gender == other.gender;
3.6 面向复用的设计:L
3.6.1 AtomCore
定义不可变类型AtomCore表示原子核,它是AtomStructure的中心物体。
仅需一个字段elementName表示元素符号
private final String elementName;
重写equals,hashCode和toString方法
equals仅需比较元素符号是否相等:
return (obj instanceof AtomCore) && elementName.equals(((AtomCore) obj).elementName);
hashCode使用成员变量radius的elementName即可:
return elementName.hashCode();
3.6.2 CentralUser
定义不可变类型CentralUser表示中心用户,它是SocialNetworkCircle的中心物体。
CentralUser继承Person,无需额外属性。
重写equals方法,仅需验证对象类型并调用基类equals即可:
return (obj instanceof CentralUser) && super.equals(obj);
无需重写hashCode,直接从Person继承即可
3.7 面向复用的设计:PhysicalObject
定义接口PhysicalObject表示轨道物体,所有CircularOrbit派生类中使用的轨道物体类型需实现此接口。
3.7.1 Athlete
定义不可变类型Athelete表示朋友,它是TrackGame的中心物体。
定义final字段name, number, nation, age, bestScore分别代表姓名、编号、年龄、国籍、年龄、历史最好分数。
private final String name, number, nation;
private final int age;
private final double bestScore;
重写equals和hashCode:
equals中各个字段都相等即Athlete对象相等
if (this == obj) return true;
if (!(obj instanceof Athlete)) return false;
Athlete other = (Athlete) obj;
return name.equals(other.name) && number.equals(other.number) && nation.equals(other.nation) && age == other.age && bestScore == other.bestScore;
hashCode中使用各字段的hashCode的多项式运算结果作为Athlete的hashCode
int val = 0;
val = val * 31 + name.hashCode();
val = val * 31 + number.hashCode();
val = val * 31 + nation.hashCode();
val = val * 31 + Integer.hashCode(age);
val = val * 31 + Double.hashCode(bestScore);
return val;
3.7.2 Electron
定义不可变类型Electron表示电子,它是AtomStructure的中心物体。
考虑到Electron没有特殊属性,人为加入一个全局唯一id属性区分不同的电子。
private static final Object counterLock = new Object();
private static int counter;
private final int id;
counter为全局计数器,每构造一个电子就加一。counterLock为同步锁,确保线程安全性。
构造函数中,将counter的值赋给id,并使counter自增1
synchronized (counterLock) {
id = counter;
counter++;
}
重写hashCode,使用id的hashCode
重写equals,判断类型和id是否相等即可
3.7.3 Friend
定义不可变类型Friend表示朋友,它是SocialNetworkCircle的中心物体。
Friend继承Person,无需额外属性。
重写equals方法,仅需验证对象类型并调用基类equals即可
无需重写hashCode,直接从Person继承即可
3.8 可复用API设计
定义类型CircularOrbitAPIs,为辅助函数类,用于计算熵值等。
实现函数getObjectDistributionEntropy,用于计算轨道系统的熵值:
public static <L, E> double getObjectDistributionEntropy(CircularOrbit<L, E> c)
内部实现为遍历每一个Track,根据track上物体的数量和物体的总数量套用公式计算即可:
实现函数getLogicalDistance,用于计算轨道系统两物体间逻辑距离
实现函数getPhysicalDistance,用于计算轨道系统两物体间物理距离
3.9 图的可视化:第三方API的复用
使用java swing进行GUI设计和多轨道系统的可视化展示。
3.9.1 初始界面
点击“切换为此应用”按钮,弹出文件选择对话框,可选择相应的文件初始化多轨道系统:
3.9.2 TrackGame可视化及操作
选择方案二:
3.10 设计模式应用
3.10.1 工厂模式
构造 Track、PhysicalObject 等对象时使用了工厂模式。
定义PhysicalObjectFactory作为轨道物体工厂类的接口,具体的轨道物体工厂类均实现此接口。
接口函数PhysicalObject fromString(String s);
用途是从字符串中创建轨道物体对象。
AthleteFactory、ElectronFactory、FriendFactory均实现此接口,并在重写的fromString函数中利用正则表达式解析字符串,并构造具体的轨道物体对象。
3.10.2 Iterator模式
使用 Iterator 设计模式,设计迭代器,客户端可在遍历 CircularOrbit 对象中的各 PhysicalObject 对象时使用,遍历次序为:从内部轨道逐步向外、同一轨道上的物体按照其角度从小到大的次序(若不考虑绝对位置,则随机遍历)。
使CircularOrbit<L, E>接口继承Iterable接口
public interface CircularOrbit<L, E> extends Iterable<E>
在ConcreteCircularOrbit<L,E>中实现Iterator iterator();方法:
3.10.3 strategy 设计模式
在 TrackGame 应用中,使用 strategy 设计模式实现多种不同的比赛方案编排策略,并可容易的切换不同的算法。
先将比赛编排分为两步:
- 给运动员分为不同的组别
- 给每一组组内的运动员排次序分配跑道
定义GroupStrategy接口作为分组策略类的接口,所有给TrackGame提供分组策略的类均需实现此接口。
接口中定义doGroup函数将运动员分组:
List<List<Athlete>> doGroup(List<Athlete> athletes);
NumberAscendGroup和NumberDescendGroup实现了GroupStrategy接口,分别提供按编号升序和降序进行分组的策略。
定义SortStrategy接口作为组内排次序策略类的接口,所有给TrackGame提供组内排次序策略的类均需实现此接口。
接口中定义doSort函数将组内运动员排序:
void doSort(List<Athlete> athleteGroup);
NumberAscendSort和NumberDescendSort实现了SortStrategy接口,分别提供按编号升序和降序进行组内排次序的策略。
TrackGame类中,定义organizeGame方法进行比赛编排,接收一个GroupStrategy参数和一个SortStrategy参数作为比赛编排的分组策略和组内排次序策略:
public void organizeGame(GroupStrategy groupStrategy, SortStrategy sortStrategy)
3.11 应用设计与开发
3.11.1 TrackGame
定义可变类型TrackGame,继承ConcreteCircularOrbit<Object, Athlete>,表示径赛运动。
定义字段athletes存储所有运动员,gameGroup存储比赛编排结果,groupSelected存储当前选中的比赛组别。
private final Set<Athlete> athletes = new HashSet<>();
private List<List<Athlete>> gameGroup = null;
private int groupSelected;
重写transit、addCentralObject、removeCentralObject函数,均直接抛出UnsupportedOperationException异常,因为TrackGame不支持这些操作。
重写removePhysicalObject函数,从athletes中移除运动员
super.removePhysicalObject(obj);
athletes.remove(obj);
实现organizeGame方法,实现对比赛的编排:
public void organizeGame(GroupStrategy groupStrategy, SortStrategy sortStrategy)
3.11.2 AtomStructure
定义可变类型AtomStructure,继承ConcreteCircularOrbit<AtomCore, Electron>,表示原子结构。
实现函数SeparateElectron,实现对各层电子的均匀排布:
遍历所有Track,遍历track上的所有电子,使用move函数移动
for (Electron electron : electrons) {
move(electron, Math.PI * 2 * i / electrons.size());
++i;
}
重写addPhysicalObject函数,自动均匀隔开电子
3.11.3 SocialNetworkCircle
定义可变类型SocialNetworkCircle,继承ConcreteCircularOrbit<CentralUser, Friend>,表示社交网络结构。
由于关系为双向,重写addRelation进行双向连边
super.addRelation(a, b, dist);
super.addRelation(b, a, dist);
由于关系为双向,重写removeRelation进行双向去边
super.removeRelation(a, b);
super.removeRelation(b, a);
定义函数updatePositions,用于根据题目要求,以距离CentralUser的远近更新Friend所处的轨道:
public void updatePositions()
3.12 应对应用面临的新变化
3.12.1 TrackGame
定义类型TrackGameNew,作为新的变化后的TrackGame类型,继承TrackGame进行扩展
public class TrackGameNew extends TrackGame
实现很简单,仅需调用父类的构造函数,将一个轨道最多四人的限制传入即可
super(4, game, numOfTracks, athletes);
定义类型TrackGameFactoryNew,作为新的工厂类,继承TrackGameFactory进行扩展
重写create函数,为重用代码,先调用父类的create函数创建TrackGame对象,再利用TrackGame对象的信息构造TrackGameNew对象:
TrackGame trackGame = super.create(s);
return new TrackGameNew(trackGame.getGame(), trackGame.getTracks().size(), trackGame.getAthletes());
3.12.2 AtomStructure
定义类型AtomicStructureNew,作为新的变化后的AtomicStructure类型,继承AtomicStructure进行扩展
public class AtomStructureNew extends AtomStructure
需要扩展两个新成员numOfNeutron, numOfProton,分别是原子核中质子和中子的数量
定义类型AtomicStructureFactoryNew,作为新的工厂类,继承AtomicStructureFactory进行扩展
重写create函数,为重用代码,先调用父类的create函数创建AtomicStructure对象,再利用AtomicStructure对象的信息构造AtomicStructureNew对象
3.12.3 SocialNetworkCircle
定义类型SocialNetworkCircleNew,作为新的变化后的SocialNetworkCircle类型,继承SocialNetworkCircle进行扩展
public class SocialNetworkCircleNew extends SocialNetworkCircle
需重写addRelation和removeRelation函数,将双向加边更改为单向加边
@Override
public void addRelation(Object a, Object b, double dist) {
addOneWayRelation(a, b, dist);
}
定义类型SocialNetworkCircleFactoryNew,作为新的工厂类,继承SocialNetworkCircleFactory进行扩展
重写create函数,由于SocialNetworkCircle为双向边,构造完成后失去原来的边的方向信息,因此无法重用代码,参考SocialNetworkCircleFactory::create重新实现即可。