欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[HITSC]哈工大软件构造Lab3+Lab4实验总结

程序员文章站 2022-07-14 21:35:22
...

今天完成了Lab4的验收,由于Lab4是在Lab3的基础上的,所以我打算把这两个实验放在一起看一看这两个实验。

Lab3的目的是写一个航班的管理程序,以及尽可能多的可以为其他应用复用代码(我选择的其他两个应用为高铁车次管理应用和计划项管理应用)。这个实验给我最大的体会是:为了复用,就要尽可能地把所有的功能分割,把不相关的独立地作为一个ADT,然后通过委派机制,让对应的ADT去实现,而关联度小的功能就通过传参的方式把需要的数据传递给功能实现ADT的对应方法,本身的ADT只保留最核心的必不可少的功能。总之就是一个原则:能让别人做的事绝对不要自己动手!!!

delegation,delegation,delegation,重要的事情说三遍。

基于上面的原则,我的源代码目录树长成了下面的样子。

[HITSC]哈工大软件构造Lab3+Lab4实验总结

在这个目录结构成形之前,我的目录其实要比这个简单很多,至于它为什么会变成现在这么庞大的一个结构,就不得不说上面的原则的来历了。哎,这个说起来都是泪。在我写Lab3的3.14之前,我感觉上其实还好,虽然有部分没什么直接联系的功能直接放在了一个ADT中,比如TwoLocation这个类,我没有将它直接独立,因为这个东西只是保存了航班的起降机场,而这个类其实完全可以加入到FlightEmtry这个类的rep中,令两个Location直接作为航班计划项的一个私有rep来保存机场也不会有什么大问题,而且还方便使用,所以。。。我把它直接放了进去。而当看到3.14的时候,我后悔了,因为要求的增加经停功能导致我需要改我的FlightEntry的实现,增加rep,所以,我果断重构了这部分的之前的实现,将位置时间等信息作为了一个单独的接口,把他们分离了出来,在rep中只保留接口,

/**
  * 创建一个航班计划项
  * 
  * @param name     航班的名称,非空
  * @param twoLocations 航班的起止地点,非空
  * @param timeslot   航班的起止时间,非空
  */
public FlightEntry(String name, TwoLocationEntry twoLocations, PresetSingleTimeslotEntry timeslot) {
    super(name);
    this.twoLocations = twoLocations;
    this.timeslot = timeslot;
}

这样我改起来就非常方便了,直接在构造函数传入TwoLocation接口的不同实现就行了。

还有一个问题是我在写Lab4的时候发现的,我在三个应用的app中是把GUI和功能实现放在一起写的,没有分开(其实我在写Lab3的时候刚开始是分开的,后来觉得在功能实现里只是返回了一个表示操作成功失败的flag,所以我合并了。。。/大哭)。

/**
  * 删除不可用飞机资源
  */
public void removePlane() {
    String id = JOptionPane.showInputDialog("飞机的编号:");
​    String type = JOptionPane.showInputDialog("飞机的型号:");
​    String seats = JOptionPane.showInputDialog("飞机的座位数:");
​    String age = JOptionPane.showInputDialog("飞机的机龄:");
​    Plane plane = new Plane(id, type, Integer.parseInt(seats), Double.parseDouble(age));if (planes.contains(plane)) {
​      planes.remove(plane);
​      JOptionPane.showMessageDialog(null, "删除成功!");} else
​      JOptionPane.showMessageDialog(null, "此飞机不存在!");
  }

然后发现为了健壮性要抛异常再回到原来的执行轨迹上并且还要写日志,然后我就想捶死之前手贱合并了代码的我。于是。。。我又开始了分离功能和GUI的大业。修改之后变成了这样

/**
 * 删除不可用飞机资源
 */
public void removePlane() {
	String id = JOptionPane.showInputDialog("飞机的编号:");
	String type = JOptionPane.showInputDialog("飞机的型号:");
	String seats = JOptionPane.showInputDialog("飞机的座位数:");
	String age = JOptionPane.showInputDialog("飞机的机龄:");
	Plane plane = new Plane(id, type, Integer.parseInt(seats), Double.parseDouble(age));
	String planeInfo = "Code:" + id + ",Type:" + type + ",Seats:" + seats + ",Age:" + age;
	try {
		if (removePlane(plane)) {
			log.info("RemovePlane;" + planeInfo + ";success");
			JOptionPane.showMessageDialog(null, "删除成功!");
		} else {
			log.info("RemovePlane;" + planeInfo + ";fail");
			JOptionPane.showMessageDialog(null, "此飞机不存在!");
		}
	} catch (EntryUseResourceOrLocationException e) {
		log.info("RemovePlane;" + planeInfo + ";fail");
		if (JOptionPane.showConfirmDialog(null, e.getMessage() + "\n重新指定资源?") == JOptionPane.YES_OPTION) {
			log.warning("RemovePlane;EntryUseResourceOrLocationException:FlightScheduleApp.removePlane(plane);reselect resource");
			removePlane();
		} else
			log.warning("RemovePlane;EntryUseResourceOrLocationException:FlightScheduleApp.removePlane(plane);end");
	}
}

/**
 * 删除不可用飞机资源的功能实现部分
 * 
 * @param plane 待删除的资源
 * @return true 删除成功;false 不存在
 * @throws EntryUseResourceOrLocationException 资源被使用中
 */
public boolean removePlane(Plane plane) throws EntryUseResourceOrLocationException {
	if (planes.contains(plane)) {
		for (PlanningEntry<Plane> pe : board.getFlightEntries())
			if (((FlightEntry<Plane>) pe).getResource().equals(plane) && !pe.getState().equals("ENDED"))
				throw new EntryUseResourceOrLocationException("计划项" + pe.getName() + "正在使用该资源,不能删除");
		planes.remove(plane);
		return true;
	} else
		return false;
}

经过这一番折腾,我总结出了最开始你们看到的那条教(ti)训(hui)。如果你看到了这里,我以我血的教训建议你:delegation,delegation,delegation,重要的事情说三遍。 要学会以及如何更好的(这可能程序员的一个终极目标?)。

至于最开始我说的功能分割,其实我在Lab4的第一个任务中体会到了另一个方面的含义。

Lab4的3.1要求是从文件读入计划项然后在当读入的计划项出现错误时抛出相应的信息并且能够记录日志信息。所以,我发现我又有必要重构我Lab3中写的从文件读计划项的方法。因为我Lab3中匹配计划项是这个样子的:

String patten = "(Flight:\\d{4}-\\d{2}-\\d{2},[A-Z]{2}\\d{2,4}\\{DepartureAirport:[a-zA-Z]+"
				+ "ArrivalAirport:[a-zA-Z]+DepatureTime:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}"
				+ "ArrivalTime:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}Plane:[N|B]\\d{4}\\{Type:[a-zA-Z0-9]+)"
				+ "(Seats:[1-6]\\d{2}|[5-9]\\d)(Age:(\\d|[1-2]\\d|30)\\.?\\d?\\}\\})";
StringBuffer sb;
while (input.hasNext()) {
	// 读入并匹配
	sb = new StringBuffer();
	String in = "";
	while (!in.equals("}")) {
		in = input.nextLine();
		sb.append(in);
	}
	in = input.nextLine();
	sb.append(in);
	String s = sb.toString();
	if (!s.matches(patten)) {
		input.close();
		JOptionPane.showMessageDialog(null, "计划项格式错误!");
		return;
	}
}

可以看到我的读入是以一个计划项为整体匹配的,失配后也不知道在哪里失配,反正文件中某个计划项失配了。而由于在Lab4中要求能够定位错误位置,于是我大改了这部分,变成了这个样子:

// 拆分成数组匹配
String[] pattens = { "Flight:", "\\d{4}-\\d{2}-\\d{2}", ",", "[A-Z]{2}\\d{2,4}", "\\{", "DepartureAirport:",
				"[a-zA-Z]+", "ArrivalAirport:", "DepatureTime:", "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}", "ArrivalTime:",
				"Plane:", "[N|B]\\d{4}", "Type:", "[a-zA-Z0-9]+", "Seats:", "[1-6]\\d{2}|[5-9]\\d", "Age:",
				"(\\d|[1-2]\\d|30)\\.?\\d?", "\\}\\}" };
List<String> stringList;
int fileLineNum = 0;
while (input.hasNext()) {
	int entryLineNum = 0;
	stringList = new ArrayList<>(); //把每行存成数组
	String in = "";
	while (!in.equals("}}")) {
	in = input.nextLine();
	entryLineNum++;
		if (in.equals("}")) {
			in = in + input.nextLine();
			entryLineNum++;
		}
		if (!in.equals(""))
			stringList.add(in);
	}
	// 解析
	for (int i = 0; i < stringList.size(); i++) {
		String s = stringList.get(i);
		switch (i) {
		case 0:
		date = s.substring(7, 17);
		name = s.substring(18);
		if (!s.substring(0, 7).matches(pattens[0]) || !s.substring(17, 18).matches(pattens[2]))
			hrow new InherentFormatException("第" + (fileLineNum + 1) + "行固定格式有误");
		if (!date.matches(pattens[1]))
			throw new DateTimeFormatException("第" + (fileLineNum + 1) + "行日期格式有误");
		if (!name.matches(pattens[3]))
			throw new EntryNameFormatException("第" + (fileLineNum + 1) + "行航班号格式有误");
		break;
        ...
        }
    }
    ...
}

大手术啊大手术,这样一来我原来的逻辑基本上被完全推翻了,所以我又体会到了另一层的含义:千万千万不要为了一时的简便而偷懒,一定要把功能拆分开,把每个部分单独做一个功能块,即使是在方法内部也要遵循这个守则。

最后再来说一说Lab4吧,其实Lab4的任务其实不是很多,如果在Lab3中能够完全的遵守之前说过的要求编写各个ADT的spec以及AF、RI、Safety from rep exposure以及测试用例,将会很大程度上减轻你在这个实验中的任务负担。Lab4的任务负担主要来源于之前写的代码的结构问题,由于要添加异常抛出-处理机制、要添加日志功能,导致了原来的代码结构不适用于新的要求因而产生了大量的修改需求,所以,话题又回到了开始的那个体会,也就是我这篇文章想竭力传达的意思,希望后来者能够踩在我这个渣渣的脚印上避过这些我曾经踩过的坑,也谨以此文铭记教训。

试用例,将会很大程度上减轻你在这个实验中的任务负担。Lab4的任务负担主要来源于之前写的代码的结构问题,由于要添加异常抛出-处理机制、要添加日志功能,导致了原来的代码结构不适用于新的要求因而产生了大量的修改需求,所以,话题又回到了开始的那个体会,也就是我这篇文章想竭力传达的意思,希望后来者能够踩在我这个渣渣的脚印上避过这些我曾经踩过的坑,也谨以此文铭记教训。

(向老师提个建议:多线程的实验还是让做一做吧,简单点用一次课的时间占个一两分也行啊,光上课没做实验总感觉少点什么)

相关标签: 软件构造