BUAA OO-Course 2022 Unit4 & Course Summary
BUAA OO-Course 2022 Unit4 & Course Summary
第四单元架构设计
UML类图与说明
由于代码中含有大量Association和Aggregation关系,上图中省略了表示这些关系的连线。
基本上以My
开头的类都是封装了官方包中给的以Uml
开头的对应的类,其意义是对UMLModel中一些类型的封装,以便于存放其他信息。剩下的两个类Parser
和Checker
分别用于解析输入的UMLElement... elements
和对9条规则进行检查。
数据读入与存储
选取合适的方法处理乱序输入的elements
是第一次作业的一个注意点。为此,我采用了先在第一次遍历中,将所有元素用多种容器变量分类存储进来,然后通过不同的操作依次对这些已经存储好的容器进行解析操作,流程如下代码所示:
// class Parser
public void parse(UmlElement... elements) {
storeElement(elements);
checkAttributeInClassDiagram();
distributeAttribute();
setBelongClassForOperation();
distributeParameterToOperation();
distributeOperationToClass();
setGeneralization();
setAssociation();
setInterfaceRealization();
/* ------SEQUENCE DIAGRAM------ */
setRepresentAttributeForLifeline();
distributeMessageToLifeline();
distributeLifelineToInteraction();
/* ------STATE DIAGRAM------ */
distributeEventToStateByTransition();
distributeAllState();
}
其中,storeElement(elements)
对传入的元素进行依次遍历并分类存储:
private void storeElement(UmlElement... elements) {
for (UmlElement e : elements) {
switch (e.getElementType()) {
case UML_CLASS:
storeClass(e);
break;
case UML_ATTRIBUTE:
storeAttribute(e);
break;
case UML_INTERFACE:
idToInterface.put(e.getId(), new MyInterface(e));
if (isNull(e.getName())) {
checker.setRule1();
}
break;
case UML_ASSOCIATION_END:
idToAssociationEnd.put(e.getId(), (UmlAssociationEnd) e);
break;
case UML_INTERFACE_REALIZATION:
storeInterfaceRealization(e);
break;
case UML_GENERALIZATION:
storeGeneralization(e);
break;
case UML_ASSOCIATION:
associations.add((UmlAssociation) e);
break;
case UML_PARAMETER:
storeParameter(e);
break;
case UML_OPERATION:
storeOperation(e);
break;
/* ------SEQUENCE DIAGRAM------ */
case UML_COLLABORATION: //TODO
// idToCollaboration.put(e.getId(), (UmlCollaboration) e);
break;
case UML_ENDPOINT:
umlEndPointId.add(e.getId());
break;
case UML_INTERACTION:
storeInteraction(e);
break;
case UML_LIFELINE:
storeLifeline(e);
break;
case UML_MESSAGE:
messages.add((UmlMessage) e);
break;
/* ------STATE DIAGRAM------ */
case UML_STATE_MACHINE:
storeStateMachine(e);
break;
case UML_REGION:
idToRegion.put(e.getId(), (UmlRegion) e);
break;
case UML_PSEUDOSTATE:
case UML_STATE:
case UML_FINAL_STATE:
storeAllState(e);
break;
case UML_TRANSITION:
storeTransition(e);
break;
case UML_EVENT:
storeEvent(e);
break;
default: //case UML_OPAQUE_BEHAVIOR: //TODO
}
}
}
出于"能存尽存"的想法,我在Parser
类中定义了如下容器,便于在存储完数据后建立各个层次UMLModel之间的关系:
private final Map<String, MyClass> nameToClass = new HashMap<>();
private final Map<String, MyClass> idToClass = new HashMap<>();
private final Set<String> duplicatedClassName = new HashSet<>();
private final Map<String, List<UmlAttribute>> parentIdToAttribute = new HashMap<>();
private final Map<String, UmlAttribute> idToAttribute = new HashMap<>();
private final Map<String, MyInterface> idToInterface = new HashMap<>();
private final Map<String, List<UmlInterfaceRealization>>
classIdToInterfaceRealization = new HashMap<>();
private final Map<String, List<UmlGeneralization>>
targetIdToGeneralization = new HashMap<>();
private final Map<String, List<UmlGeneralization>>
sourceIdToGeneralization = new HashMap<>();
private final Map<String, UmlAssociationEnd> idToAssociationEnd = new HashMap<>();
private final List<UmlAssociation> associations = new ArrayList<>();
private final Map<String, List<UmlParameter>> parentIdToParameter = new HashMap<>();
private final Map<String, List<MyOperation>> parentIdToOperation = new HashMap<>();
private final Map<String, MyOperation> idToOperation = new HashMap<>();
private int classCount = 0;
/* ------SEQUENCE DIAGRAM------ */
private final Map<String, MyInteraction> nameToInteraction = new HashMap<>();
private final Map<String, MyInteraction> idToInteraction = new HashMap<>();
private final Set<String> duplicatedInteractionName = new HashSet<>();
// private final HashMap<String, UmlCollaboration> idToCollaboration = new HashMap<>();
private final Map<String, MyLifeline> idToLifeline = new HashMap<>();
private final Map<String, List<MyLifeline>> parentIdToLifeline = new HashMap<>();
private final List<UmlMessage> messages = new ArrayList<>(); // use List: in order
private final Set<String> umlEndPointId = new HashSet<>();
/* ------STATE DIAGRAM------ */
private final Map<String, MyStateMachine> idToStateMachine = new HashMap<>();
private final Map<String, MyStateMachine> nameToStateMachine = new HashMap<>();
private final Set<String> duplicatedStateMachineName = new HashSet<>();
private final Map<String, UmlRegion> idToRegion = new HashMap<>();
private final Map<String, List<MyState>> parentIdToNormalState = new HashMap<>();
private final Map<String, MyState> parentIdToPseudoState = new HashMap<>();
private final Map<String, List<MyState>> parentIdToFinalState = new HashMap<>();
private final Map<String, MyState> idToAllState = new HashMap<>();
private final Map<String, List<MyTransition>> parentIdToTransition = new HashMap<>();
private final Map<String, MyTransition> idToTransition = new HashMap<>();
private final Map<String, List<UmlEvent>> parentIdToEvent = new HashMap<>();
private Checker checker;
可以发现,HashMap
类型容器的名字主要有三种形式:idToXXX、nameToXXX、parentIdToXXX。前两个是为了方便按id查找和按名查找,第三种主要是方便一次性存储。例如,将UMLAttribute
分配给UMLClass
、UMLInterface
或UmlCollaboration
时,根据属性的parentId
可以一次性取出所有在parent中的属性并存入。
指令实现和有效性检查
该部分为UserApi
规定要求实现的指令和对模型有效性检查的方法。由于评测时对算法复杂度几乎没有要求,该部分主要需要注意对指导书相关概念的理解。如:
-
某字段为空:
指该字段是空指针
null
,或仅包含空白字符(空格和制表符\t
)。即对于字段s
,判断s.matches("[ \t]*")
(注意这里空串也算)
同时,需要注意不同指令要求考虑的范围是不同的。如:
- 指令三——求类中操作的个数时,只考虑此类自己定义的操作,而不考虑父类中定义的操作;而指令六——求类属性耦合度时,需要考虑所继承的父类中定义的属性,但不考虑实现的接口中定义的属性。
最重要的,是要理解指导书说的意思。如:
- 有效性检查R006——Lifeline 必须表示在同一 Sequence Diagram 中定义的 Attribute。指导书说明了
attribute
的parentId
只可能表示UmlClass | UmlInterface | UmlCollaboration
这三种类型,而顺序图的范围其实对于了UmlInteraction
,因此需要通过UmlLifeline.getRepresent()
找到对应的attribute
,并判断attribute
的parentId
和UmlLifeline
所在的UmlInteraction
的UmlInteraction.getParentId()
是否指向同一个UmlCollaboration
。
流操作的使用
由于本单元涉及到存储较多容器以及对容器进行遍历操作,使用流操作可以化简代码逻辑。如:要将Set<MyInterface>
类型的容器implementedInterface
中所有接口所继承的所有父类都取出来,用下面流操作就显得简洁:
// Set<MyInterface> implementedInterface;
// ......
// 将所有接口即接口的"父接口"放进来
implementedInterface.addAll(implementedInterface.stream().flatMap(
fatherInterface -> fatherInterface.getElderInterface().stream()
).collect(Collectors.toList()));
上述代码在读取implementedInterface
的"同时"对其添加元素,实际上流操作的过程中这两个看似会导致并发修改异常的代码是先后分别执行的。
如果不使用流操作,则需要中间创建一个临时变量,代码需要改成如下:
final Set<MyInterface> temp = new HashSet<MyInterface>; // 临时变量,防止边遍历implementedInterface边修改造成并发修改异常
for (MyInterface interf : implementedInterface) {
temp.addAll(interf.getElderInterface());
}
implementedInterface.addAll(temp);
OO课程总结
四个单元中架构设计思维及OO方法的理解
四个单元的架构设计都用到了不同的侧重点的层次化设计。
第一单元
第一单元的架构设计是面向问题分解与归纳的层次化设计。面对解析运算表达式这一目标,我们需要将表达式拆解成项、因子(常量因子、变量因子、表达式因子)这几个类,在每个类中用容器存储相关相关属性,从而建立组合、继承、接口实现以及三者混合的层次关系,根据每个类对自身的管理设计类自身的方法。同时,通过继承、接口实现等连接上下层对象,从而能够在使用对象时用上层抽象无差别地引用和访问不同下层对象,实现对象的归一化管理。同时,在抽象层次中需要建立能够处理递归结构的层次关系,下层对象可能进一步引用上层对象。此外,除了对象管理上的层次化设计,第一单元在解析处理上也需要使用层次化的方式,采用自顶向下(一般输入模式)或自底向上(预解析模式)的方式逐步存储和构建分层的表达式模型。
第二单元
第二单元的架构设计是面向并发控制与安全的层次化设计。与第一单元继承、接口、多态形成的面向数据和行为抽象的层次设计结构不同,第二单元是在线程、共享、交互的基础上形成面向并发和协同抽象的层次设计结构。第二单元主要对线程这一对象进行一系列操作,由于线程的并发性,我主要采用生产者——消费者这一设计模式实现不同线程之间的互斥访问。通过设立共享对象并使用同步(synchronized
)保护不同线程对其的并发访问,完成了多个并发运行的线程进行数据交互或共用工具的业务需要。在不同线程交互的时候,需要使线程在无法获取资源时进入等待状态,释放cpu资源。除了生产者——消费者模式,也可以采用主从模式、事件驱动等其他模式涉及线程之间的交互方式,实现多个执行流的"有序"访问。
第三单元
第三单元的架构设计是面向JML规格的层次化设计。规格是开发人员需要实现的要求。就JML来说,数据规格是指类所管理的数据内容以及其需要满足的条件(有效性条件);方法规格是指类所提供的外界访问的操作,需要满足相应条件。在规格层面上,我们不关心具体实现方法,但要求程序运行产生的所有效果都在约束之内。在规格的规约下,代码具体实现并不是照搬规格,而是从方便用户的角度(考虑时空效率)设计数据结构和算法,并适当加上中间数据,在满足形式化要求的基础上尽可能地提高效率。此外,对于规格以外的输入信息,需要显示的抛出异常告诉调用者输入不合规约。
第四单元
第四单元的架构设计是面向复杂数据管理的层次化设计,具体是面对UML这一统一建模语言。基于UML本身就是纯面向对象的语言,首先需要明确UML中对象类型均是由UML元模型来规范定义的,而我们需要具体管理的对象则是UML元模型的实例化结构。根据UML的内部结构关系和相互间的逻辑关系(引用等)按照相应的方式解析并建立模型管理。在建立好层次化结构后需要根据根据相应的条件(指令)进行搜索和访问,因此在建模过程中就需要选取适合的图模型来封装类图、顺序图和状态图模型。同时,为了支持有效性检查,需要在建模过程中或建模完成后对相应的数据和对象之间的关系进行检查。
四个单元中测试理解与实践
第一单元
第一单元几乎没做测试,于是出现了很多:
基本上就是把指导书中的样例全部试一遍,在样例和中测都过了之后就万事大吉了。没有意识的测试的重要性和数据强度差异带来的测试结果差异。
第二单元
第二单元的测试主要分为两种。一种是刻意构造专门卡某种调度策略时间的策略(cpu时间或运行时间),另一种是通过自动化程序随机生成大量数据。随机大量数据的生成往往能找出代码中的主要问题,保证程序运行基本正确。而一些边界数据(或自动或手动生成)能够发现代码中不易察觉的漏洞。
第三单元
本单元的测试主要采用三种方法。除了使用自动化程序随机生成大量数据以外,一种是通过Junit对每个小模块进行单元测试,根据规格提供各种输入并检验输出与异常的抛出;另一种则是直接根据规格进行"瞪眼法"debug,只要满足规格程序就正确,因此对着每一个模块的规格和代码进行检查也能找出问题。除了正确性检查,本单元还需要选取合适的算法,保证算法复杂度不会太高。
第四单元
本单元的测试基本上为根据不同的UML图模型和不同的指令类型或检查类型,对每个类型进行多次重复测试。一种简便的方法是在starUML上手动画出各种情况的图,转换成输入数据,观察输出是否符合期望,以此检验模型正确性。
课程收获
四个单元各有收获。第一单元初次接触面向对象的建模,对类、接口以及它们之间的层次关系有了初步的理解和认识。第二单元在线程协作方面从零到有了初步的了解,理解了并发协作的便利性和多线程或多进程的重要性。第三单元学习了JML规格,理解了规格满足与具体实现,即策略与机制的分离。在清晰的规格下,每个人可以通过接口无需知道实现地调用相应方法进行操作。第四单元学习了纯oo的UML模型,进一步理解了面向对象的分层机制,同时学习了基本的顺序图和状态图模型。
改进建议
-
unit1 - unit4的课下任务与oo的关系似乎没有pre2和pre3与oo的关系大。第一单元的侧重点似乎跑到了递归下降上,第二单元则更注重线程之间的同步,不知道这算不算主题有所跑偏?
-
互测机制在后面几个单元中也许可以起到促使同学搭评测机的目的,但同时是否会导致同学亲自看别人代码并学习的动力下降?
-
希望多开一些实验课(如第二单元多线程的实验课,对将设计模式运用到本单元作业的帮助很大)
-
checkstyle可能需要有所改变,如下第十五次作业java代码能通过代码风格检查:
private void checkAttributeInClassDiagram() { try { idToAttribute.values().forEach(umlAttribute -> { if (isNull(umlAttribute.getName()) && ( idToClass.containsKey(umlAttribute.getParentId()) || (idToInterface.containsKey(umlAttribute.getParentId())))) { throw new RuntimeException(); } }); } catch (RuntimeException e) { checker.setRule1(); } }