一、UML解析器设计
先看下题目:第四单元实现一个基于JDK 8 带有效性检查的UML(Unified Modeling Language) 类图,顺序图,状态图分析器 MyUmlInteraction ,实际上我们要建立一个有向图模型,UML 中的对象(元素)可能与同级元素连接,也可与低级元素相连形成层次关系
输入:上述三UML 图的边集(非实体元素)与点集,上下层级元素通过_parent 隐式邻接。值得注意的是,不同于前两个单元,我们终于回归了离线算法OFFLINE ,I/O 通过一行END_OF_MODEL 分隔,处理难度减小了
输出:针对给出的_name 索引到相应的UML 元素查询相关对应图中信息,复杂的problem 是hw15 中对循环继承的判断,其他的都能不使用高级算法解决掉
下图是我对本单元作业设计的类图,因为三次作业都是这个架构,下文只按解析器构造流程顺序分析。btw感谢肖同学的排雷帖,可见我对UML 的理解不够深入,UMLAttribute 可以出现在顺序图中,不应该简单归入类图元素的子类,不过我这样还是能活过本单元的
信息转换——适配器模式
我构建了抽象类MyEle ,以及三个子抽象类对应三种UML 图中的元素,因为接口给出的结点(对应实体的元素)没有邻接结点的信息,我有必要自定义类来保存这些信息。能将这种“一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的”问题解决的是适配器模式
public class MyAdapt {
//适配器用于转换的类
public static MyEle as(UmlElement e) {
switch (e.getElementType()) {
case UML_CLASS: {
return new MyClass((UmlClass) e);
}
case UML_ASSOCIATION: {
return new MyAssc((UmlAssociation) e);
}...
//
public abstract class MyEle {
//My*类对UmlElement对象有一个依赖,MyEle实现基本信息接口,其他由子类分别考量
private UmlElement ele;
public MyEle(UmlElement e) {
this.ele = e;
}...
在UmlInteraction 构造函数中,我首先将传入的UmlElement 通过MyAdapt “转换”成My* 对象,后者可以从前者中只存储我们关心的信息,一些可以忽略的元素UmlEvent / UmlEndPoint 等直接在这里过滤掉
建图
有了这些准备工作开始建图,以类图为例(另两类图如法炮制),通过.mdj 解析,边集通过_parent / _reference / UML 元素表示,提取后完成建图。我对作业中涉及的所有的UmlElement ,包括边集UMLAssociation / UMLGeneralization 等都适配自己的类,当然这可有可无,我这样做是为了遍历一遍MyEle 对象就完成建图,看起来清晰一点。类图中的顶级元素是类与接口,都管理着方法与属性,StarUML 中可以对二者进行很多相同操作,有必要为二者创建父类ClassLike (类实体)来复用代码
根据输入模式,我们在解析器中需要管理按_name 索引的Class / StateMachine / Interaction 等集合,并且索引查询中要进行有效与重复检查。我为了复用代码采用了泛型容器,分别用Hashmap / Hashset 实现有效_name 查找表与重复_name 池
public class MySet<T> {
private HashMap<String, T> na2ele = new HashMap<>();
private HashSet<String> dupName = new HashSet<>();
public void add(T t) {
if (t instanceof MyEle)
if (na2ele.containsKey(t.getName())) dupName.add(t.getName());
else na2ele.put(t.getName(), t);
}
public boolean contains(String s) { return na2ele.containsKey(s); }
public boolean isDup(String s) { return dupName.contains(s); }
...
根据输入,查询请求关心ClassLike 自身的层次或直接关联关系或ClassLike 间接继承/实现的关系,后者如“类的顶级父类 / 实现的全部接口”需要ClassLike 继承链上的信息。没有循环继承时继承链为树形结构,由此,我在MyClassLike 定义addFather / getFather / update 继承相关抽象方法。update 将extends 或implements 的ClassLike 进行update 后再合并信息,置为更新过的状态,是递归式搜索
输出方面,通过与同学交流可以记忆化搜索(查询时更新),但是这是个OFFLINE 问题,同时考虑到题目的数据规模1e2.6 ,小,在完成建图之后直接对所有类update ,全部元素更新后再进行查询也没问题,这样不用每个查询方法都先update 一下了
PreCheck
hw15 要求进行有效性检查,上文中查询前update 显然要放到R002 / R003 之间,有效性以_id / _name 重复,_visibility 等检查为主,很简单;R002 检测循环继承,输出继承环路上所有元素,当然不用求出环路,只要求出所有环路上的元素。MyEle 处在继承环路上 iff 其处于某阶数大于1 的强连通分量上或本身存在自环或一次以自身为源点的搜索经过本身1 次以上,故可使用Tarjan 或BFS 来检查
二、OOP心路历程
谈到架构设计及OO方法理解的演进,我认为自己本学期确实有长进,起码前两个Unit 次次重构,但后两个Unit 都是先好好分析设计结构再写的,而且都不很复杂,所以都没有重构
Unit1 为层次化设计,hw1 / hw2 是纯纯的面向过程,hw3 中利用工厂模式根据实际中的实体为表达式中的对象建立了具有继承层次关系的类,通过将求导以及化简操作作为抽象方法,使得因子,表达式啥的都能按照自己的求导法则各自计算导数、化简,输出通过覆写toString() ,这是我首次在作业中体会到面向对象中多态的优势。
Unit2 为多线程设计,代码结构很简单,采用简单的生产-消费者模式都行,主要还是通过Java 库及保留字实现Java 线程的同步与互斥,这块一直理不太清又很难调试,所以又重构了好多次,可以说我也是初次接触在线算法问题,电梯调度的优劣是跟数据相关的,其实有点玄学,在这里我初步了解了Java 的异常机制,这是保证鲁棒性的方法之一,Java 中还有封装好的ReentrantLock 可重入锁等锁,我也做了了解
Unit3 为规格设计,需要根据JML 封装官方包中的接口,首先得说JML 表意很准确,不用像指导书般反复研究,这个Unit 大家实现的接口功能都是一样,但方法与效果不尽相同,面向对象中就是通过这样的封装实现代码的模块化,调用者很难为封装好的方法优化,作为实现者有义务实现高效的封装。面向过程和面向对象肯定不是对立的,我们实现的代码对外是屏蔽掉的,所以在作业中我在底层函数用了很多面向过程风格的代码
Unit4 为模型化设计,我们接触的UML 脱离了Java ,描述的是对象、属性、状态等,更加接近面向对象的本质,作业方面空间很大,由于hw13 就理好了层次,后续轻松拓展,还第一次使用了泛型容器,节约了很多代码量,充分封装各个层次的操作以模块化
三、测试理解与实践
当然我印象最深刻的是多线程程序测试,程序运行结果与环境相关,在测试出现问题后很难通过程序本身找到病因,我使用JProfiler + println() 才能逐步确定程序卡在了哪里
测试分两方面,一是验证程序的基本功能,二是对极限数据或异常输入测试,前者通过随机生成样例数据或JUnit 单元测试法或对拍实现,后者如电梯换乘调度测试,特殊表达式求导,给定数据规模下复杂度最高的数据。我认为注重后者或许对我们更有价值,反正我的Bug 都出在极限情况...还有一种测试就是对优化过的代码进行测试,一定得用修改前版本对拍一下,属实对这一点有ptsd 了
JUnit 对我而言很好用,@Before / @Test / @After 完整构建了数据生成、断言测试、错误分析的测试流程,还在同一项目下就能用,舒服啊
四、收获总结
首先我在本学期通过Java 认识到面向对象的印象深刻的特征:封装与多态
封装定义为隐藏对象的属性和实现细节,仅对外公开接口,我最开始对每个属性几乎都写了get / set ,但这样属性的隐藏原则就没意义了,我理解的封装是没有需求就不开放属性的查询或方法的调用,所以属于对象“份内事”的方法也尽量用private 修饰。封装的优势显然,首先封装隐藏了属性,保证了程序的数据安全,封装方法,可以使程序设计更加模块化,比如Unit4 中可将三种图的查询分派给三种管理类,这样也能降低类的平均复杂度,还有一点是封装的代码拓展性强,需要改变程序功能时,调用者往往不用改写,只需对封装的对象中的数据结构与方法进行修改,这也是课程迭代开发模式下的要求。当然,既然有这个信息隐藏原则,调用者就很难根据实际问题对封装对象作出调整,导致封装对象已经很难有优化的空间,所以使用封装时,调用者得对对象有一个大体的了解,比如在Unit3 中我因为对Hashmap 的底层实现不了解,初始容量过大引发了隐患;实现者则需合理利用时空资源保证程序的整体良性
多态,我理解的是同一类型对象引用在运行时可以使用不同的方法以实现“多种形态”,一种形式是子类Override 方法后使用父类类型引用,还可以是由不同类实现的接口。通过多态在Unit1 实现表达式中不同因子的求导,在Unit4 中可以让数据结构不同的Class / Interface (单/多继承)共享一份Tarjan 代码,多态巧妙在我们无需去if-else 所引用的对象实际是什么类型,调用的方法采用对应继承链上最近的版本,所以多态中想要扩展新子类很简单,只需要符合引用者的行为逻辑,引用新子类的对象则无需修改代码
当然一些别的特征也很酷,我们可以通过继承复用父类的代码,也可以通过泛型类来复用代码,并且实现类似多态的效果,不过泛型类在构造传入类型时,其所管理的类型就确定了,所以我觉得泛型倒有一点c/c++ 中#define 的意思
不难看出,上面这些特征,无论是类也好,接口也好,其共性是对对象行为范式的一种抽象,OO 语言不同于PO 语言的一点就是它给了一套这种抽象的机制,所以面向对象编程更贴近实际,我们代码的结构往往对应实际中的逻辑关系,可能这也是Java 的优势,虽然不精炼,但很直观易懂
在理论网课 / 实验课中,我也学到了OO 设计模式与原则,OO 的可维护性强,很适合于迭代开发与复杂系统的整体设计,学习OO 编程思想,更深入地理解Python / C++ 等的OO 机制了
五、我的改进意见
-
看到这么多同学吐槽实验课我就放心了,我认为实验课有必要给同学们一定的反馈,具体的形式可以是编程性实验之后可以在平台上像作业一样了解测试情况并且进行Bug 修复
-
感觉后两Unit 对可维护性要求降低了,Unit1 中嵌套的出现打破了原本简单的表达式到因子的层次关系,Unit2 中多部电梯的出现改变了控制类的数据结构,也改变了线程同步的条件,而Unit3 / 4 在迭代开发中基本是跟随指导书在上一次作业中多写些方法增加功能就okay了,反正就是对原先的作业架构的冲击力不够大,应该提高难度,btw程序高效性也很重要,希望后两个Unit 能开启性能分
-
Unit3 可以放到前面,体现出类规格,层次间的规格
-
互测本意应该是驱动大家学习其他同学代码中的设计策略吧,建议发动Hack 的时候得提交自己发现的Bug 的位置,防止互测变成纯黑箱互测
六、网课学习体会
条件艰苦的线上网课,颇怀念线下的课堂氛围和peer pressure呢
感谢仍离同学们最近的老师与助教们~
|