【Unit4】UML解析器(模型化设计)-作业总结 & 【BUAA-OO】课程总结

第四单元作业总结

1.题目概述

UML类图建模与查询(8) + UML顺序图/状态图建模与查询(3+3) + 模型错误检查(9),三次迭代共23条命令

2.构架设计

一开始以为和第三单元差不多,稍微用点容器,用官方包的解析函数填填接口函数就好。后来发现还是有所区别。

原因在于,本单元模型为静态模型,所有查询指令不会对模型本身进行影响;同时课程组提供的UML解析函数不足以很好的处理各种查询功能。

思来想去,应该尽情地用空间换时间。再想想,应该用课程解读出的一摞UmlElement重新搭建一个UML的图景。这不就是将UML模型解构后再按Java重构嘛,初想很是麻烦(还得自己设计类!属于是被上单元惯坏了)。但写起来就好多了。

2.1 重建模型结构

image
大体上只需要容器配上官方包的各类型(UmlXxx)就可以了,对于想进一步建模的类型设计了新类(见图中各XxxModel),嫌累赘没有继承官方包的类型,第三次作业用到的UmlClassOrInterface接口就直接假实现了。

类中按需设计属性,选择了应存尽存

public class MyImplementation implements UserApi {
    /* ============================ ClassDiagram ============================ */
    private final HashMap<String, ClassModel> name2Class = new HashMap<>();
    private final HashMap<String, ClassModel> id2Class = new HashMap<>();
    private final HashSet<String> dupNameClasses = new HashSet<>(); //<name>
    private final HashMap<String, Integer> name2ClassAttCouplingDegree = new HashMap<>();
    private final HashMap<String, InterfaceModel> id2Interface = new HashMap<>();
    private final HashMap<String, ArrayList<String>> className2InterfaceList = new HashMap<>();
    private final HashMap<String, OperationModel> id2Operation = new HashMap<>();
    private final HashMap<String, UmlAssociationEnd> id2AsEnd = new HashMap<>();
    /* ============================ StateMachine ============================ */
    private final HashMap<String, StateMachineModel> name2Machine = new HashMap<>();
    private final HashMap<String, StateMachineModel> id2Machine = new HashMap<>();
    private final HashSet<String> dupNameMachines = new HashSet<>();
    private final HashMap<String, StateMachineModel> rid2Machine = new HashMap<>();
    private final HashMap<String, TransitionModel> id2Trans = new HashMap<>();
    /* =========================== SequenceDiagram ========================== */
    private final HashMap<String, CollaborationModel> id2Collaboration = new HashMap<>();
    private final HashMap<String, InteractionModel> name2Interact = new HashMap<>();
    private final HashMap<String, InteractionModel> id2Interact = new HashMap<>();
    private final HashSet<String> dupNameInteracts = new HashSet<>();
    /* =========================== GrammarChecker ========================== */
    private boolean umlR001 = false;
    private final HashSet<UmlClassOrInterface> umlR003 = new HashSet<>();
    private boolean umlR005 = false;
	...
}
public class StateMachineModel {
    private final String name;
    private final String id;
    /* ======= State ======= */
    private String startId = null;
    private final HashSet<String> endIds = new HashSet<>();
    private final HashMap<String, UmlState> name2State = new HashMap<>();
    private final HashMap<String, UmlState> id2State = new HashMap<>();
    private final HashSet<String> dupNameStates = new HashSet<>();
    /* ======= Transition (include critical point judge) ======= */
    private final HashMap<String, ArrayList<TransitionModel>> srcId2Trans = new HashMap<>();
    private final HashMap<String, Boolean> name2Critical = new HashMap<>(); // <stateName, bool>
    private boolean globalCritical;
    private HashSet<String> hitEndMark;
	...
}

2.2 数据读入

由于模型的建构有数据依赖采用了多次循环读取的方式。理论上应该统一引用,使用过不再需要的内容扔掉;或者第一轮读取时就分门别类的存放。不过看在数据条数很少(300-),就直接循环了4轮。

public MyImplementation(UmlElement... elements) {
        storeArterialEntity(elements);
        completeEntity(elements);    // after: UMLOperation has its complete information
        storeRelations(elements);
        name2Machine.values().forEach(StateMachineModel::setGlobalCritical);
}
  • storeArterialEntity(): 包含一轮遍历,对独立性高的主干实体进行存储(UmlClass、UmlInterface、UmlOperation、UmlAssociationEnd;UmlCollaboration、UmlInteraction;UmlStateMachine)
  • completeEntity(): 包含两轮遍历(可能没有好好规划),目的在于补全实体。首轮是有些许特殊处理的类型(UmlParameter、UmlRegion),次轮是向已有顶层主干实体中加入次级实体(UmlAssociation;UmlPseudoState、UmlFinalState、UmlState、UmlTransition;UmlInteraction、UmlLifeline、UmlEndpoint)
  • storeRelations(): 包含一轮遍历,主要将各实体的关系按需存入相应的实体或位置中,以便查询(UmlAttribute、UmlOperation、UmlGeneralization、UmlInterfaceGeneralization;UmlEvent;UmlMessage)

最后一行是处理为了某命令,希望在模型建完后更新一次全体状态机的某值

2.3 继承实现相关查询命令

还是少不了类似:查询所有继承的类/实现的接口、检查是否循环继承/重复实现等。看在复杂度无忧的情况下,就直接dfs好了。目前习惯写的dfs都会传一个记录容器或全局设一个更迭容器,例如:

private void dfs4Interface(String stepIn, ArrayList<String> path) {
        int index = path.indexOf(stepIn);
        if (index >= 0) {
            for (int i = index; i < path.size(); ++i) {
                umlR003.add(id2Interface.get(path.get(i)));
            }
            return;
        }
        path.add(stepIn);
        for (String nextId: id2Interface.get(stepIn).getFatherInterfaceIds()) {
            dfs4Interface(nextId, path);
        }
        path.remove(stepIn);
}

不知道有没有更好的写法。

2.4 关于名字重复

处理方式大体上形成模式了,用了三个容器:

  • HashMap<String, XXXModel> id2XXX : 用id标识,作为一切操作的使用容器(不管重不重名)
  • HashMap<String, XXXModel> name2XXX : 用name标识,用于作是否重名的依据
  • HashSet<String> dupNameXXXs : 记录所有因name2XXX而找到的重名XXX,可用于各类需求命令

2.5 关于缩行

写完第三次作业后,MyImplement达到了580行,超过了一个类<500行的规范。所以进行了一些缩行操作:

  • 觉察出类图、状态图、顺序图中相关命令都有对“找不到名字/名字重复”的异常抛出处理,将它们提出合并成三个xxxNameExceptionThrower()方法,在各命令函数中调用。
  • 删除了所有自动生成的@Overide编译注释
  • 感觉这第一点的三个方法仍很独立,干脆独立出一个专门抛名字异常的类叫做NameExceptionThrower
public class NameExceptionThrower {
    public static void detectClassNameException(String className,
                                                HashMap<String, ClassModel> name2Class,
                                                HashSet<String> dupNameClasses)
            throws ClassNotFoundException, ClassDuplicatedException {
        if (!name2Class.containsKey(className)) {
            throw new ClassNotFoundException(className);
        } else if (dupNameClasses.contains(className)) {
            throw new ClassDuplicatedException(className);
        }
    }

    public static void detectInteractNameException(String interactionName,
                                                   HashMap<String, InteractionModel> name2Interact,
                                                   HashSet<String> dupNameInteracts)
            throws InteractionNotFoundException, InteractionDuplicatedException {
        if (!name2Interact.containsKey(interactionName)) {
            throw new InteractionNotFoundException(interactionName);
        } else if (dupNameInteracts.contains(interactionName)) {
            throw new InteractionDuplicatedException(interactionName);
        }
    }

    public static void detectMachineNameException(String stateMachineName,
                                                  HashMap<String, StateMachineModel> name2Machine,
                                                  HashSet<String> dupNameMachines)
            throws StateMachineNotFoundException, StateMachineDuplicatedException {
        if (!name2Machine.containsKey(stateMachineName)) {
            throw new StateMachineNotFoundException(stateMachineName);
        } else if (dupNameMachines.contains(stateMachineName)) {
            throw new StateMachineDuplicatedException(stateMachineName);
        }
    }
}

现在再想想,如果还要继续肢解,就按类图、顺序图、状态图三个不太相关的数据和命令独立出来。


构架类图
image

3.测试与记录

  • 第一次:进行了数据生成+猛烈的对拍,找到了很多同学的bug;
  • 第二次:想用JUnit4,但发现好像构造MyImplementTest有点困难像是作茧自缚,应该到官方包某处去构造Test,但官方包稍微读了下一知半解就搁置了,最后就按自己的想法手动构造了一个整合数据,还有找出一个HashMap的NullPointException的情况
  • 第三次:认为没啥好测的,就自己构造了下r002、r003、r004、r009的数据,大致都没问题;结果问题出在r001,某片段文字理解反了导致了灯下黑的错误,喜提81.81

这个单元自测时最容易出的bug是HashMap的NullPointException,就是get失败得到了null了,还继续对它使用既定的方法。

ps.一直记不清聚合、组合,还有相应的图形标记,放一张别人的图
image

课程总结

1.关于构架设计和OO方法

第一单元

架构设计:刚上手肯定称不上舒服,是Experiment1中的Parser和Lexer类给了我思路,开始了构架设计之旅。之后该单元的设计主旨转到了“预处理”-“解析”两者的权衡上,随着解析的比重上升,构架的清晰度和可拓展性也逐步上升。

OO方法:继承并不是这一单元的主要方法,而应是组合或者聚合关系(不同因子的类嵌套聚合);类的主体性逐渐分明

第二单元

构架设计:同样是做了本单元的Experiment后,采取了相同的生产者-消费者模式。当电梯增多/横向运输出现后,虽听得自由竞争的说法,但并未想清,情急下便沿用生产者-消费者的调度架构多套了几层。但调度算法以其没有最优解而难以抉择,没有了第一单元明确的优化方向,让权衡更加困难,再辅以各种bug和较难的测试方法,总体效果一般。第一/三次作业性能上似乎不错,但都由于存在未查出的bug而或小或大的崩盘。

OO方法:第三次作业的迭代中,终于好好体会到了继承多态的美妙之处;认识了一些面向对象的设计模式:生产者消费者模式,流水线模式,单例模式;在多线程编程方面,逐渐掌握了synchronize的使用,不过没有运用重入锁reentrantlock

第三单元

构架设计:几乎没有什么构架设计,就是按接口写函数。由于构建命令和查询命令的缘故,以部分数据结构的空间换时间,达到降低复杂度的效果。同时也体会到了规格化设计谓何。

OO方法:体验了在java中相关算法的实现

第四单元

架构设计:重新将UML图的结构在java中还原,取所需而用,设计上更加自然了些,也用包来视觉优化了代码结构。这便是UML自动解析转换的目的所在吧

2.关于测试

一贯来沿用的主要测试手段便是python生成数据+数据检验(函数/自行检验,对拍)。肉眼debug得看心情,似乎写完了就不太愿意细细回看一遍了。后来接触了JUnit4单元测试,只是玩过,但没有成品的测试

第一单元开头新鲜感强烈,亲手从数据生成到数据检验搭过简单的测评姬,接触了subprocess、xeger、random等模块;逐渐开始犯懒,觉得有些数据生成繁琐,测试也开始偷懒,用用别人的数据生成模块啊,自己随手造几个数据看看啊什么的,后两次作业都有bug未查出。

第二单元开始被当头棒喝,又开始重视测试,然而写了数据生成模块后觉得检验挺难写,加上时间精力不够,也作罢了。第一次作业和第三次作业的bug四两拨千斤,令人心疼而无奈。

第三单元时强调了单元测试,其实这时候是最适合尝试JUnit的;不过最初的python测评逻辑也很顺,况且群友提供了初版(第二单元得了超大教训想着拉个群交流交流debug事项)测评姬,便修修补补用了一整个单元,每周六猛力对拍。所以强测互测结果不错,但其实过程除了改测评姬部分,都很无聊,也没有太多能力上的提升。想想要是搞一套ci会不会很有意思,但知识也不足。

关于JUnit,其实有时编写时还是跳不开写代码的主主观思路,自己怎么理解的就是怎么测的,这样会有灯下黑的认知错误无法查出;从头的自动化测试就能规避这点

第四单元的第一次作业同上一单元,第二第三次时又变回手造数据了,其实到和单元测试没啥区别了。但发觉JUnit写不来,类中的容器太多太杂,直接写MyImplementTest吃不消也没意义,就又作罢了。

总的没有太多认知差,但态度端正了一些。

3.关于收获

码代码的能力提升是最显著的。遥想数据结构那会儿,几十行代码都能写出bug还de不出考试干着急,到最近几次作业写下来基本上就直接能跑,bug也不再是些莫名其妙的代码缺陷,可以更多的关注构架设计本身。一些算法的实现似乎挺流畅的,之前还没啥概念。(也有Idea的功劳)

其次就是面向对象本身。真要说了解了面向对象编程,未必。基本的概念知识是了解了,究竟有没有构架设计能力呢,不好说,要继续实践才行。读代码和理解能力还是偏弱了,线上实验有两次没来得及。很多设计模式没有好好实践过,更多OO知识没有接触过,应该在工程实践中进一步理解。

曾经觉得OO又难又神圣,写起来后又觉得不过如此,再往后觉察到其背后浓缩的庞大工业体系而叹服,这种看山是山和看山不是山的变换,甚是奇妙。冰山一角,具体而微。

4.关于建议

  • 后两个单元的课程似乎是略显空泛,没听进去太多,作业也没用上就不管了;设计模式的讲解挺少的,希望可以专门讲讲并配上相应的练习。
  • 第二个单元的中测太弱不是好事,测试方式和测评姬的搭建希望提供台阶
  • “第四单元手册”可以好好调整调整,挺粗糙的;第三单元作业的重心似乎和规格化有所片偏差,希望多见识实际工程中规格化的应用。
posted @ 2022-06-28 16:42  Xlucidator  阅读(41)  评论(0编辑  收藏  举报