面向对象课程Unit3总结
前言
第三单元作业的主题是社交网络的构建和维护,主要学习内容涉及到程序规格的确认以及JML
语言的阅读与书写。第一次作业主要实现基本的社交网络,内部单元仅有Person
,主要操作针对网络和个人,同时书写对于queryTripleSum()
方法的测试;第二次作业增加了Tag
标签类,将部分人标为同一群组,对群组进行整体操作,同时书写对于queryCoupleSum()
方法的测试;第三次作业增加了Message
信息类,实现人与人、人与群组间信息通讯,同时书写对于deleteColeEmoji()
方法的测试。第三单元源代码实现主要涉及JML
代码的阅读与实际功能的实现转化,对照着注释,功能的实现难度不大,主要是各自性能高要求导致需要对部分属性进行动态维护,引入多项图数据处理方法。测试代码套路较为统一,按照JML
描述即可完成测试,主要是练习参数化测试。本篇总结主要是记录一下三次作业的具体实现、各种优缺点分析,以及自身一点心得体会。
一、作业架构分析
Homework1
题目重点重述
第一次作业主要实现仅含Person
元素的无向图网络,以person
作为网络节点,acquaintance
作为网络连接边,并以value
作为边权重。程序要求实现对于网络的一系列功能,可分为对person
节点的操作与对acquaintance
边的操作。具体有网络增删节点、增加边、调整边权重等修改操作,查询块数量(所有相连节点构成一个块)、查询三角形数量(三个节点两两相连构成一个三角形)、查询两节点是否相连、查询边权重等查询操作。
程序UML图
本次作业含有四种异常处理,包括两节点不含关系、网络不含节点、获得同一节点、获得同一关系等异常。在网络操作发生错误访问时要抛出相应异常。Counter
作为计数器,记录各异常发生次数。主要功能类是MyPerson
、MyNetwork
类,并在其中进行各项修改和查询操作。DisjointSet
作为并查集类,用于简化块相关操作,提高程序性能。
复杂度
类复杂度表格如图所示:其中OCavg
指代平均操作复杂度,OCmax
指代最大操作复杂度,WMC
指代加权方法复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
Counter | 1.25 | 2 | 5 |
DisjointSet | 2.57 | 5 | 18 |
Main | 1 | 1 | 1 |
MyEqualPersonIdException | 1 | 1 | 2 |
MyEqualRelationException | 1.5 | 2 | 3 |
MyNetwork | 2.5 | 6 | 30 |
MyPerson | 1.38 | 2 | 18 |
MyPersonIdNotFoundException | 1 | 1 | 2 |
MyRelationNotFoundException | 1 | 1 | 2 |
可以观察到,相比前两单元的架构复杂度,本单元复杂度有所降低,证明笔者的架构设计还需进一步练习与学习。本次作业中,复杂度最高的是MyNetwork
、DisjointSet
两个类。在MyNetwork
类中,多个函数涉及到多分支判断,复杂度较高。DisjointSet
类主要有查找根节点、合并块、整合拆分块等操作,而这些操作涉及遍历操作,因此复杂度较高。
测试
本次作业要求对MyNetwork
中的queryTripleSum()
函数进行测试,测试库使用JUnit4
。主要测试了函数结果计算是否正确,及函数查询前后原属性是否改变。测试数据主要为仅有节点的网络和随机构建联系的网络。结果是否正确测试策略是调用函数获得测试结果,按照JML
构建正确的计算函数并获得正确结果,进行比对。该方法作为一个纯查询型的方法,不应改变网络的任何属性。属性是否改变测试策略是构建两个网络,一个始终不变,作为对照组;一个调用queryTripleSum()
方法,作为变量组。比对调用方法前后的两个网络,当两个网络属性相同时,证明函数调用没有改变网络属性。
Homework2
题目重点重述
本次作业在第一次作业的基础上,增加了Tag
标签类(同一标签可记为一个群组,在后续操作中也是对群组整体操作)。每个群组可以加入多个Person
,群组具有群组的各项操作。具体包含增删person
的基础群组维护操作,查询群组内value
数值、查询群组内person
的年龄方差特征等。对于Network
,新增了查询某人最佳关系、查询网络中的couple
数量、查询两个人之间的最短路径(即最短通过几人可以和对方取得联系)长度等操作。作业的主要难度在于群组value
数值、最佳关系等对于性能要求较高,需要动态维护;最短路径需要借助经典算法减少查询时间。
程序UML图
本次作业新增四种异常,无法找到路径、人物与任何人均无联系、人物不含群组、人物含有相同编号群组。同样使用Counter
作为计数器,记录各异常发生次数。新增主要功能类MyTag
,在其中进行群组的相关查询增删操作。对于网络,新增MyNetworkFeature
类,作为查询网络特征类,包含网络的多项查询功能,降低网络类复杂度。
复杂度
类复杂度表格如图所示:其中OCavg
指代平均操作复杂度,OCmax
指代最大操作复杂度,WMC
指代加权方法复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
Counter | 1.25 | 2 | 5 |
DisjointSet | 2.57 | 5 | 18 |
Main | 1 | 1 | 1 |
MyAcquaintanceNotFoundException | 1 | 1 | 2 |
MyEqualPersonIdException | 1 | 1 | 2 |
MyEqualRelationException | 1.5 | 2 | 3 |
MyEqualTagIdException | 1 | 1 | 2 |
MyNetwork | 3.05 | 8 | 64 |
MyNetworkFeature | 2.12 | 5 | 17 |
MyPathNotFoundException | 1 | 1 | 2 |
MyPerson | 1.71 | 4 | 41 |
MyPersonIdNotFoundException | 1 | 1 | 2 |
MyRelationNotFoundException | 1 | 1 | 2 |
MyTag | 2.18 | 4 | 24 |
MyTagIdNotFoundException | 1 | 1 | 2 |
可以观察到,相较于上一次作业,尽管增加了MyNetworkFeature
类降低复杂度,本次作业MyNetwork
复杂度仍然有所上升。这是因为增加了部分函数含有多分支选择,增加了复杂度。其余类复杂度较为正常。
测试
本次作业要求对MyNetwork
中的queryCoupleSum()
函数进行测试,测试库使用JUnit4
。主要测试了函数结果计算是否正确,及函数查询前后原属性是否改变。测试数据主要为仅有节点的网络和随机构建联系的网络。结果是否正确测试策略是调用函数获得测试结果,按照JML
构建正确的计算函数并获得正确结果,进行比对。该方法作为一个纯查询型的方法,不应改变网络的任何属性。属性是否改变测试策略是构建两个网络,一个始终不变,作为对照组;一个调用queryCoupleSum()
方法,作为变量组。比对调用方法前后的两个网络,当两个网络属性相同时,证明函数调用没有改变网络属性。相较于第一次作业,本次作业测试在思路架构上无较大变化。
Homework3
题目重点重述
第三次作业在第二次作业的基础上,增加了Message
类及关于信息的操作。Message
类有三个子类,分别为表情信息EmojiMessage
、通知信息NoticeMessage
、红包信息RedEnvelopeMessage
。对于每类信息,又有两种信息类型,单人信息(即单对单,单人向单人发送信息)和群组信息(即单对多,单人向群组发送信息)。所有信息存入网络属性messages
中,网络新增方法加入删除信息,发送信息,清除热度较低的表情,清除某人所收到的全部通知信息。
程序UML图
本次作业新增四种异常,未存储当前表情、存储表情序号相等、存储信息序号相等、无法获得待查询信息。同样使用Counter
作为计数器,记录各异常发生次数。新增功能类主要是Message
类,含有id
、socialValue
、type
等通用属性,三类信息存储有不同的特殊信息特殊属性:EmojiMessage
含有表情序号emojiId
(相同的emojiId
代表同一类表情),NoticeMessage
含有通知内容string
,RedEnvelopeMessage
含有红包价值money
。而对于每类信息的不同对单对群类型,又有不同操作:对于EmojiMessage
和NoticeMessage
,对单仅person2
接收信息,对群则tag
内全部成员都接收信息(EmojiMessage
需另外对当前emojiId
类表情的热度加一);对于RedEnvelopeMessage
,对单person1
减少红包等值的money
,person2
增加红包等值的money
,对群tag
内全部成员增加messageMoney / tagSize
等值的money
,person1
减少tagSize * (messageMoney / tagSize)
等值的money
(这里由于messageMoney
和tagSize
均是int
类型,因此person1
减少的money
可能不等于红包的money
,这也是本次作业设计中比较奇怪的一点)。对于网络,仅增加了messages
属性与整体网络内message
的操作方法。同时为降低MyNetwork
的复杂度和缩减长度,新增MyNetworkBehavior
类,内含不发生异常时网络的各项操作。
复杂度
类复杂度表格如图所示:其中OCavg
指代平均操作复杂度,OCmax
指代最大操作复杂度,WMC
指代加权方法复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
DisjointSet | 2.57 | 5 | 18 |
Main | 1 | 1 | 1 |
MyEmojiMessage | 1 | 1 | 3 |
MyMessage | 1.11 | 2 | 10 |
MyNetwork | 2.42 | 8 | 87 |
MyNetworkBehavior | 1.93 | 5 | 27 |
MyNetworkFeature | 2.33 | 5 | 21 |
MyNoticeMessage | 1 | 1 | 3 |
MyPerson | 1.62 | 4 | 52 |
MyRedEnvelopeMessage | 1 | 1 | 3 |
MyTag | 2.15 | 4 | 28 |
myexceptions.Counter | 1.25 | 2 | 5 |
myexceptions.MyAcquaintanceNotFoundException | 1 | 1 | 2 |
myexceptions.MyEmojiIdNotFoundException | 1 | 1 | 2 |
myexceptions.MyEqualEmojiIdException | 1 | 1 | 2 |
myexceptions.MyEqualMessageIdException | 1 | 1 | 2 |
myexceptions.MyEqualPersonIdException | 1 | 1 | 2 |
myexceptions.MyEqualRelationException | 1.5 | 2 | 3 |
myexceptions.MyEqualTagIdException | 1 | 1 | 2 |
myexceptions.MyMessageIdNotFoundException | 1 | 1 | 2 |
myexceptions.MyPathNotFoundException | 1 | 1 | 2 |
myexceptions.MyPersonIdNotFoundException | 1 | 1 | 2 |
myexceptions.MyRelationNotFoundException | 1 | 1 | 2 |
myexceptions.MyTagIdNotFoundException | 1 | 1 | 2 |
可以观察到,将behavior
从网络中提取出来后,MyNetwork
类复杂度有所降低,但仍因为存在多组多分支查询,因此复杂度仍较高。其余类复杂度正常。
测试
对于本次作业,笔者个人认为主要难度在测试环节。本次作业要求对MyNetwork
中的deleteColdEmoji()
函数进行测试,测试库使用JUnit4
。主要测试了函数结果计算是否正确,及函数查询前后原属性是否改变。表情信息主要有三种存储形式,分别是在emojiIds
中存储表情种类,每个emojiId
代表一类表情;在emojiHeats
中存储每个emojiId
对应的表情使用热度;在message
中存储信息,message
中属性 id
代表信息的序号,emojiId
代表这条表情信息对应的表情类型。deleteColdEmoji()
的主要实现逻辑是查询每种表情的热度,小于limit
的表情进行删除(这种删除包括三种存储形式,即删除emojiIds
和emojiHeats
中emojiId
对应的元素,删除messages
中所有对应emojiId
的EmojiMessage
),并返回删除后messages
的长度。
测试逻辑仍然是构建两个网络,一个始终不变,作为对照组;一个调用deleteColdEmoji()
方法,作为变量组。比对调用方法前后的两个网络,观察函数的返回结果是否正确,以及各项应该改变或不该改变的属性是否发生变化。主要步骤可以分为两步:
网络初始化:网络初始化需要为网络添加人物,添加群组,为人物添加关系,对网络添加信息,按照发送频率发送信息。为了保证测试的全面性,应保证在发送之后,所有
emojiId
均有一定热度,发送之后网络内存储的待发送信息messages
应包含全部6类信息。(请注意,下列代码中省略了应对调用各网络函数后可能产生的异常的处理方法,请自行补充完善)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109public void netInitial(MyNetwork oldNet, MyNetwork newNet)
{
int index = 0;
long seed = System.currentTimeMillis();
Random random = new Random(seed);
/*TODO:添加人*/
...
/*TODO:添加群组*/
...
/*TODO:添加emojiId*/
for (int i = 0; i < emojiIdSize; i++)
{
oldNet.storeEmojiId(i);
newNet.storeEmojiId(i);
}
/*TODO:添加emoji信息*/
for (; index < emojiSize; index++)
{
Message message1;
Message message2;
int emojiId = random.nextInt(emojiIdSize);
int id1 = random.nextInt(peopleSize);
int id2 = random.nextInt(peopleSize);
switch (index % 2)
{
case 0:
message1 = new MyEmojiMessage(index, emojiId,
oldNet.getPerson(id1), oldNet.getPerson(id2));
message2 = new MyEmojiMessage(index, emojiId,
newNet.getPerson(id1), newNet.getPerson(id2));
break;
default:
message1 = new MyEmojiMessage(index, emojiId,
oldNet.getPerson(id1), oldNet.getPerson(id1).getTag(id1));
message2 = new MyEmojiMessage(index, emojiId,
newNet.getPerson(id1), newNet.getPerson(id1).getTag(id1));
break;
}
oldNet.addMessage(message1);
newNet.addMessage(message2);
}
/*TODO:添加notice信息*/
for (; index < noticeSize + emojiSize; index++)
{
Message message1;
Message message2;
int id1 = random.nextInt(peopleSize);
int id2 = random.nextInt(peopleSize);
switch (index % 2)
{
case 0:
message1 = new MyNoticeMessage(index, "hello",
oldNet.getPerson(id1), oldNet.getPerson(id2));
message2 = new MyNoticeMessage(index, "hello",
newNet.getPerson(id1), newNet.getPerson(id2));
break;
default:
message1 = new MyNoticeMessage(index, "hello",
oldNet.getPerson(id1), oldNet.getPerson(id1).getTag(id1));
message2 = new MyNoticeMessage(index, "hello",
newNet.getPerson(id1), newNet.getPerson(id1).getTag(id1));
break;
}
oldNet.addMessage(message1);
newNet.addMessage(message2);
}
/*TODO:添加redElevator信息*/
for (; index < redElevatorSize + noticeSize + emojiSize; index++)
{
Message message1;
Message message2;
int wholeMoney = random.nextInt(100);
int id1 = random.nextInt(peopleSize);
int id2 = random.nextInt(peopleSize);
switch (index % 2)
{
case 0:
message1 = new MyRedEnvelopeMessage(index, wholeMoney,
oldNet.getPerson(id1), oldNet.getPerson(id2));
message2 = new MyRedEnvelopeMessage(index, wholeMoney,
newNet.getPerson(id1), newNet.getPerson(id2));
break;
default:
message1 = new MyRedEnvelopeMessage(index, wholeMoney,
oldNet.getPerson(id1), oldNet.getPerson(id1).getTag(id1));
message2 = new MyRedEnvelopeMessage(index, wholeMoney,
newNet.getPerson(id1), newNet.getPerson(id1).getTag(id1));
break;
}
oldNet.addMessage(message1);
newNet.addMessage(message2);
}
/*TODO:将信息进行发送*/
for (index = 0; index < emojiSize + noticeSize + redElevatorSize; index++)
{
if (index % sendFrequency == 0)
{
oldNet.sendMessage(index);
newNet.sendMessage(index);
}
}
}变量组对照组比对:属性比对主要针对
emojiIds
、emojiHeats
、messages
三个数组中的元素。对于emojiIds
,应确保原数组中对应热度大于等于limit
的序号新数组均含有,原数组中对应热度小于limit
的序号新数组均不含有,新数组中不含有相同元素;对于emojiHeats
,应确保原数组中对应热度大于等于limit
的热度新数组均含有,原数组中对应热度小于limit
的热度新数组均不含有,新数组中不含有相同元素;对于messages
,应确保原数组的NoticeMessage
、RedEnvelopeMessage
不发生变化,原数组中的EmojiMessage
应确保对应热度大于等于limit
的热度新数组均含有,对应热度小于limit
的热度新数组均不含有,新数组中不含有相同元素。
二、架构重点实现分析
Homework1
第一次作业程序中主要有三个实现的重点:
并查集
DisjointSet
构建与使用:在本次作业中,queryBlockSum()
与isCircle()
方法作为核心方法,对性能要求较高,因此引入并查集DisjointSet
类提高性能。并查集的核心在于时刻保证位于同一个块(即互相相连的节点)的根节点相同。find()
方法:查询节点的根节点;merge()
方法:合并两个块,将一个根节点的的父节点设置为另一个根节点;modify()
方法:调整一个块各点的根节点,在删除边时使用。对节点1进行深度遍历,记录全部可以到达的节点,并将所有可到达的节点的根节点变动为节点1。若节点2可以到达,则不需要继续进行任何操作;若节点2不可以到达,则对节点2进行深度遍历,并将所有可到达的节点的根节点变动为节点2。
计数器
Counter
构建与使用:在本次作业中,需要对各项异常发生次数进行统计,因此加入计数器Counter
类。每个异常中设置一个静态属性计数器counter
记录该类异常的发生次数。- 三角形数量
triNum
的动态维护:在本次作业中,queryTripleSum()
方法同作为核心方法,对性能要求也较高,因此对Network
设立属性triNum
记录三角形的数量。在加入联系时,查询联系两个端节点共同相连的节点数量,即是增加的三角形数量。删除练习时相同,减少两个端节点共同相连的节点数量即可。
Homework2
第二次作业程序中主要有三个实现重点:
- 群组
value
维护:在本次作业中,queryValueSum()
方法作为核心方法,对性能要求较高,因此对Tag
设立属性valueSum
记录特定群组的数值总和。维护情况主要分为对群组中增删人员和增删关系时对群组产生影响。当增删人员时,遍历群组内属性persons
,增加或删减群组人员与待增删人员之间的关系价值。当增删人员间关系时,遍历网络中全部人员的全部群组,对含有增删关系的两位成员的群组改变群组的valueSum
。 - 人员最佳关系维护:在本次作业中,
queryBestAcquaintance()
方法作为核心方法,对性能要求较高,因此对Person
设立属性bestId
记录与人员关系数值value
最高的成员编号。维护情况主要是在增加和调整整合关系时对人员产生影响。当增加关系时,将增加的关系的id
、value
与当前最佳关系的id
、value
进行比较,判断是否需要变化最佳关系。当调整关系数值时,分为两种情况:如果变化的关系是当前最佳关系且数值增加,或变化的关系不是当前最佳关系且数值减少,则不需要调整;如果变化的关系不是当前最佳关系且数值增加,或变化的关系是当前最佳关系且数值减少,则需要遍历所有现在的关系并找到最佳关系。当删除关系时,也分为两种情况:如果删除的不是最佳关系,则不需要调整;如果删除的是最佳关系,则需要遍历所有现在的关系并找到最佳关系。 - 寻找最短路径:在本次作业中,
queryShortest()
方法作为核心方法,对性能要求较高,但由于路径增删相关操作影响过多,不便动态维护,因此需要采用高效的寻路算法。笔者使用的是普通的广度优先遍历,首次遍历经过person2
后,清空队列,并返回相应长度。测试证明普通的广度优先算法完全够用。
Homework3
第三次作业程序中主要有两个实现重点:
同一类不同构造函数:本次作业中,对于三种信息
EmojiMessage
、NoticeMessage
、RedEnvelopeMessage
,每种信息均有两种类型对单对群,因此这三个类每个类应该有两种构造方法。而在一个类中使用多种构造方法过程较为简单,直接书写即可,最终产生新对象时会根据传入数据类型自动识别使用哪种构造方法。迭代器的使用:本次作业中涉及多个需要对集合进行遍历删除的操作,而在这个过程中,集合不应该发生改变,否则会抛出
ConcurrentModificationException
的异常。因此应该构造迭代器,使用迭代器对集合进行遍历删除操作。1
2
3
4
5
6
7
8
9Iterator<Object> iterator = objects.iterator();
while (iterator.hasNext())
{
Object object = iterator.next();
if (condition)
{
iterator.remove();
}
}
三、Bug分析及程序优化
Homework1
Bug分析
第一次作业出现的问题主要是性能问题。在最初的设计中,并未使用并查集,因此程序性能极差。在增添DisjointSet
类并将MyNetwork
类中相关方法优化后,就可以大幅提高程序性能。
优化策略
设计最初时,缺省了一定的程序鲁棒性以及对于性能的追求。现对作业过程中所做优化进行记录:
- 并查集优化函数:程序中最吃性能的部分是
isCircle()
、queryBlockSum()
两个方法。对于isCircle()
方法,直接调用并查集find()
查询两个节点的根节点,如果相同,证明来自同一个块,两节点相连。对于queryBlockSum()
方法,在DisjointSet
类中维护块组属性,在调用queryBlockSum()
方法时直接返回块组大小即可。
Homework2
Bug分析
第二次作业暂无bug
优化策略
在完成作业并通过强测后听到想到了一些有趣的优化方式,现记录如下:
- 使用小顶堆维护最佳关系:本次作业的人员最佳关系维护时,可以使用小顶堆的数据结构存储各关系数值。当需要获得最佳关系时,只需返回根节点即可。但增删关系时都需要重新调整构建小顶堆,这部分较占时间。
- 使用双向广度优先遍历寻找最短路径:已知路径的起点和终点,可以分别从起点和终点双向执行广度优先遍历,直到遍历的部分有交集。这种方法一定程度上可以减少从一边开始搜索的时候,随着层数的增加,搜索空间变大的情况。但由于程序中节点较少,构成的子图估摸较小,因此双向广度优先遍历在性能上并没有体现出较大水平的提高。
Homework3
Bug分析
第三次作业出现了在调用sendMessage()
函数抛出MyTagIdNotFoundException
时使用了messageId
的bug,本单元作业id
种类较多,在调用函数过程需要多加注意。
优化策略
第三次作业暂无优化策略
四、心得体会
本单元不需要发泄。(嘴脸!)
那么心得体会部分就主要谈谈这单元完成之后的一些收获和经验吧:
- 架构设计的重要性。好的,重要的事情说三遍。相较于前两个单元,本单元难度大幅降低,主要原因是去除了架构设计部分。同学们的身份从设计师转变为程序员,只需要按照给定的规格补全代码即可。但我不设计不妨碍我不学习。本单元的整体架构设计较为科学,从中也能看到高内聚低耦合的思想。美中不足的是
Network
中设计的方法过多,大大增加了该类的长度。 - 规格的重要性。 本单元作业中槽点最多的就是
JML
规格。诚然,这种规格双刃性极大,从好的方面讲,它可以更加精确地让程序员实现功能,详细描述了方法各项限制与前置后置条件;从坏的方面说,它的可读性较差,不能直观的反映要实现的功能。因此,真实设计时可能还是要使用JML
+自然语言的形式。 - 测试的重要性。 应该说,本单元最重要的一点就是测试,对于工业软件测试的基本概念方法有了一定了解。
- 数据构造:本单元三次作业
JUnit
的测试重点是参数化的随机数据构造。在构建测试前,应该详细考虑可能出现的全部问题,并保证设计出的数据可以涵盖这些方面。 - 黑箱测试白箱测试:黑箱即不知道真实代码的情况下直接构造数据进行测试,白箱即在知道真实代码的情况下针对代码进行攻击。两种在真实测试中均会使用到,一个是数据导向型的,一个是逻辑导向型的。
- 单元测试集成测试:单元测试即对每一个方法进行单独测试,集成测试即对程序整体进行各项功能测试。本单元使用
JUnit
进行的测试即是单元测试,而正常状况下对于程序整体运行测试即是集成测试。 - 功能测试压力测试回归测试:功能测试即是对程序的各项功能进行全面测试,压力测试即是使用各种临界数据测试程序的鲁棒性,回归测试即是修改代码后要再次进行全面测试。
- 数据构造:本单元三次作业
至于对课程的建议,说实话挺想吐槽一下作业描述的朝令夕改的。但是仔细想一想,想要使用JML
规格语言这种令人血压飙升的玩意正确全面地描述函数的功能与各项限制确实是一项极其困难的事,读的人都觉得困难,写的人就更觉得困难了,瞬间就能理解学长学姐们的难处了。架构设计方面,还是想说一句,Network
里面的方法太多了,很容易就超过checkstyle
的行数限制了。
最后,还是要感谢助教lmh学姐的帮助。也希望在之后的学习中能再接再厉,继续进步。