前言

第四单元作业的主题是图书管理系统的构建,主要学习内容涉及到程序架构设计以及UML图的阅读与绘制。第一次作业主要实现基本的图书管理系统,服务单元主要是书架、借阅处、预约处,仅含有正式书籍,顾客可以获得borrowreturnqueryorderpick等服务,同时绘制程序的UML类图;第二次作业增加了图书漂流角服务单元(CornerOffice漂流角类),含有正式与非正式书籍(非正式书籍可以转正),顾客可以获得donaterenew等服务,同时绘制程序的UML类图以及书籍借阅流程的UML状态图;第三次作业增加了信誉积分,顾客可以查询自身信誉积分,借书还书的过程中各种情况会导致客户信誉积分变化,同时绘制程序的UML类图以及书籍预约获取流程的UML顺序图。第四单元并未明确规定代码的架构以及书籍的移动策略,自由度较高,主要练习了程序代码架构高内聚低耦合以及逻辑合理清晰的设计。各种UML图的绘制较为繁琐,反而是本单元中耗时最长的部分。本篇总结主要是记录一下三次作业的具体实现、各种优缺点分析,以及自身一点心得体会。

一、作业架构分析

Homework1

题目重点重述

第一次作业主要是实现简单的图书管理系统,包含borrowreturnqueryorderpick等顾客操作,以及图书馆在开馆闭馆时自行进行的调整操作。图书馆分区可分为三区,书架区、借阅处、预约处。当顾客进行借阅操作时,携带书籍前往借阅处,借阅处审批顾客是否可以借阅,若可以,则借与顾客,如不可以,则存入借阅处。归还书籍时同理,归还的书籍存入借阅处。当顾客进行查询操作时,书架区需要返回特定书籍编号的库存数量。当顾客进行预约操作时,预约处审批顾客是否可以预约,并在停止营业后进行书籍调整。当顾客进行取预约书籍操作时,预约处需要查询是否有预约信息,若存在,则进行借出。开馆闭馆停止营业时可以进行书籍在图书馆内调整操作,在三个分区转运书籍。书籍分为ABC三类,其中,A类书籍不可以借阅或预约(不可以移动),B类书籍同时只能拥有一本,C类书籍同一书号同时只能拥有一本。

程序UML图

hw4-1UML图

本次作业中使用RequestDealer类读入并处理需求LibraryRequest,主要功能实现类是Library类,主要交互是在LibraryCustomer类之间进行。在Library中存入客户名单,以及ShelfBorrowOfficeAppointOffice三个服务分区,在Library类中进行上层的统一操作。同时设计了BookBuffer类统一存储当前对象的书籍,并配有查询特定书籍数量、是否拥有特定书籍和增删书籍的基本操作。对于预约处的处理,使用预约信息AppointInf作为最小操作单元,每条预约信息均有bookIdcustomerdate等属性,以判断预约信息的有效性。

复杂度

类复杂度表格如图所示:其中OCavg指代平均操作复杂度,OCmax 指代最大操作复杂度,WMC 指代加权方法复杂度

Class OCavg OCmax WMC
AppointInf 1 1 5
AppointOffice 2 4 14
BookBuffer 1.88 3 15
BorrowOffice 1 1 3
Customer 1.6 4 8
Library 2.1 5 21
Main 1 1 1
RequestDealer 2.8 6 14
Shelf 1 1 6

可以观察到,本次作业设计架构还是较为合理的,整体复杂度水平较低。复杂度最高的是RequestDealLibrary类。这两个类中含有多种方法,且存在多项的分支选择,因此复杂度较高。同时,由于各项客户操作以及书籍转运主要在Library中实现,因此会出现Library复杂度远超其他类的现象。CustomerShelfBorrowOffice中方法均是对bookBuffer操作,主体部分在BookBuffer类中实现,因此复杂度较低。

重构

在重新思考后,发现不将书籍副本实例化为具体对象,将会遗失很多信息(将书本实例化后会更好存储),同时会造成三种Office较难抽象,因此选择进行重构。

hw4-1UML图改

Class OCavg OCmax WMC
AppointInf 1 1 4
AppointOffice 1 1 4
Book 1 1 6
BookBuffer 2.64 6 29
BorrowOffice 1 1 2
Customer 1.5 4 9
Library 1.9 4 19
Main 1 1 1
Office 1 1 6
RequestDealer 2.8 6 14
ShelfOffice 1 1 4

可以观察到,类复杂度略有上升。但是由于Book类中可以存储是否预约以及预约信息,因此可以将图书馆中appointOfficeborrowOfficeshelfOffice抽象出一个Office的父类,使得代码复用性更高,操作也更加顺遂。

Homework2

题目重点重述

本次作业在第一次作业基础上,新增了客户捐献、图书漂流角、正式书籍续借等功能。首先定义了正式书籍与非正式书籍,正式书籍可从初始化配置(存储于书架中)以及非正式书籍升级获得(非正式书籍被借阅两次,需要升级为正式书籍),仅存在于书架、预约处、借阅处、客户;非正式书籍可从捐献获得(存储于图书漂流角中),仅存在于图书漂流角、借阅处、客户。书籍的转移路径为书架到借阅处(正式书籍借阅失败)、书架到客户(正式书籍借阅成功)、书架到预约处(正式书籍预约整理)、借阅处到书架(正式书籍返还及非正式书籍升级)、借阅处到图书漂流角(非正式书籍返还)、预约处到书架(预约书籍超时未取返还)、预约处到客户(预约书籍取走)、图书漂流角到借阅处(非正式书籍借阅失败)、图书漂流角到客户(非正式书籍借阅成功)、客户到借阅处(客户归还借阅书籍)。在客户借阅到期的前五天内,通过图书馆预约信息判定后,客户可以续借30天。

程序UML图

hw4-2UML图

相比于第一次作业,本次作业整体架构并无较大变化。可以观察到,经过重构抽象后,非正式书籍相关操作实现较为简单。如图书漂流角可直接继承Office类,其全部功能均在父类中已经实现,因此可以直接使用。本次作业中,最大改变是将图书馆系统的操作全部提取出来建立Service接口,实例化各种服务类。在System类中建立Service[]数组储存各种服务,在系统使用各项功能时调用服务类即可。

hw4-2UML状态图

本次作业还要求制作书籍的UML状态图。本次画制较为简单,仅做出了初始状态InitState、在图书馆中状态inLibrary、在客户手中状态inCustomer。三种状态的转换方式如图所示,不再过多赘述。

复杂度

类复杂度表格如图所示:其中OCavg指代平均操作复杂度,OCmax 指代最大操作复杂度,WMC 指代加权方法复杂度

Class OCavg OCmax WMC
Main 1 1 1
books.Book 1.73 9 19
books.BookBuffer 2.73 4 41
informations.Inf 1 1 8
libraries.Library 1.87 3 28
libraries.System 3.17 9 19
offices.AppointOffice 1.4 3 7
offices.BorrowOffice 1 1 4
offices.CornerOffice 1 1 2
offices.Office 1 1 9
offices.ShelfOffice 1 1 2
services.BorrowService 2 3 4
services.ContinueService 1.5 2 3
services.DonateService 1 1 2
services.OrderService 2 3 4
services.PickService 2 3 4
services.QueryService 1.5 2 3
services.ReturnService 1.5 2 3
users.Customer 1.67 5 10

可以观察到,在增加功能后,本次作业复杂度有较高提升,其中SystemBookBuffer类复杂度最高。对于System类,其中的run()方法进行command轮询,requestDeal()方法有较多选项的switch语句,选择不同的服务进行调用,因此整体复杂度较高。其余类复杂度正常,得益于程序的模块化分离。

Homework3

题目重点重述

本次作业在第二次作业的基础上,新增了用户的信誉积分功能。用户初始分数为10分,最高为20分,最低无限制。当用户出现按时归还借阅书籍、捐赠书籍、捐赠书籍转正时获得相应加分;出现逾期还书、预约书籍逾期未取等情况时减去相应减分。当用户的信誉积分小于0时,将限制其借阅、预约、续借等服务。同时新增用户信誉积分查询功能,查询单个客户的信誉积分。

程序UML图

hw4-3UML图

UML类图与上一次作业变化不大,主要新增了CreditService服务类。在Book类中新增state属性,表示书籍当前状态,主要是NONE(代表处于图书馆内,没有被预约借阅)、APPOINTED(代表处于预约处,被某位客户预约)、BORROWED(代表被某位客户借阅,处于客户手中),通过新增方法changeState()变换状态。这种改变既可以去除isAppointedisBorrowed两个属性,整体更加抽象简洁,也可以在未来增加更多书籍状态时更好的拓展。

hw4-3UML顺序图

本次作业还要求制作书籍的UML顺序图,主要模拟客户预定并获取书籍收发消息的流程。本次画图较为简略,仅制出了两个lifeline作为收发消息的对象,分别是:System:Customer:后代表类)。当客户发出预约请求时,系统向客户发送消息orderNewBook()表示已接收到请求,并进行各项操作,将书籍变为可获取的状态;客户获取到书籍后,向系统发送消息getOrderedBook()表示已获得书籍。整体流程较为简单,是一个反馈机制。

复杂度

类复杂度表格如图所示:其中OCavg指代平均操作复杂度,OCmax 指代最大操作复杂度,WMC 指代加权方法复杂度

Class OCavg OCmax WMC
Main 1 1 1
books.Book 1.82 10 20
books.BookBuffer 2.73 4 41
informations.Inf 1 1 8
libraries.Library 1.87 3 28
libraries.System 2.88 9 23
offices.AppointOffice 2.33 7 14
offices.BorrowOffice 1 1 4
offices.CornerOffice 1 1 2
offices.Office 1 1 9
offices.ShelfOffice 1 1 2
services.BorrowService 2 3 4
services.ContinueService 1.5 2 3
services.CreditService 1 1 2
services.DonateService 1 1 2
services.OrderService 2 3 4
services.PickService 2 3 4
services.QueryService 1.5 2 3
services.ReturnService 1.5 2 3
users.Customer 2 5 20

可以观察到,相比于第二次作业,本次作业复杂度无显著变化。本次作业改动较少,并未产生过多新方法,同时新增部分中基本没有多项判断和循环,因此并未对复杂度造成太大影响。

二、架构重点实现分析

Homework1

第一次作业程序中主要有三个实现的重点:

  • 借阅流程的实现:当顾客进行借阅操作时,首先应该判断书籍是否可以移动,若可以移动则客户携带书籍前往借阅处,并进行进一步审批。借阅处审批顾客是否可以借阅,若可以借阅,则该书籍从shelf删除,并加入客户的books中;如不可以借阅,则该书籍从shelf删除,并加入借阅处的books中。借阅处的操作较为简单,关键在于对客户借阅请求的审批。设计中将该判断分为两部分,分别是书籍是否可以移动以及顾客是否可以借阅。书籍是否移动主要判断shelf中是否有对应剩余的副本,顾客是否可以借阅主要判断customer现在拥有的书籍和借阅请求是否矛盾。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //判断书籍是否可移动(有余本且不是A类书籍),在Shelf类中进行
    public boolean canMove(LibraryBookId bookId)
    {
    return books.haveBook(bookId) && !bookId.isTypeA();
    }

    //判断顾客是否可以借阅或预约书籍,在Customer类中进行
    public boolean canBorrowOrAppoint(LibraryBookId bookId)
    {
    if (bookId.isTypeB())
    {
    return !books.haveBTypeBooks();
    }
    else if (bookId.isTypeC())
    {
    return !books.haveBook(bookId);
    }
    else
    {
    return false;
    }
    }
  • 预约流程的实现:预约流程主要涉及预约操作、取书操作以及停止营业时书籍的内部调整。设计时为预约处设计了两个属性requestsappointsrequests主要存储审批通过的预约请求,appoints主要存储经过内部调整后运送至预约处的书籍的预约信息。在预约环节,仅审核客户的预约资格,如上所示,所有拥有预约资格的请求都应加入请求池中,等待处理;在转运环节,遍历requests,判断书籍是否可以移动以及当前该客户是否可以预约借阅(当客户在预约后进行其他操作并失去拥有书籍的资格时,如此可以保证当其再获得拥有书籍的资格时,预约请求仍能生效,提高客户picked的成功率),对于书籍可移动且当前该客户可预约借阅的请求,转运书籍,并以当前转运的bookIdcustomerdate创建新的预约信息,存入appoints中,等待客户取书。当客户取书时,遍历appoints找到为其准备的书籍并删除对应的预约信息。

  • 停止营业阶段的书籍转运:书籍转运主要有两部分组成,即借阅处与书架区、预约处与书架区的转运。请注意,转运操作无强制逻辑要求,可以自行设计转运方案。但最佳方案应有转运至书架区的操作在转运至预约处的操作前进行,以提高对应书籍转运的成功率以及客户获得书籍的成功率。

    • 借阅处与书架区间转运:主要是用户借阅失败以及归还的书籍返回书架区,操作较为简单,将借阅处的books清空,并将返回的集合转运添加至书架区的books中即可。该部分操作放在闭馆时进行,这样的好处是每天借阅处的书籍都能及时返回书架区。
    • 预约处与书架区间转运:主要是用户未取走因到时间而失效的预约书籍返回书架区以及因预约从书架区转运至预约处的书籍。对于预约处返回书架区的书籍,遍历appoints,根据预约信息的date,借助LocalDate类提供的isBefore()方法,判断超时需要返回书架区的书籍。对于因预约从书架区转运至预约处的书籍,遍历requests,判断当前需求对应书籍是否可以移动以及客户是否具备取书资质,符合的进行转运。该部分操作放在开馆时进行,这样的好处是当出现进行预约后数天不开馆的情况时,能空除这段时间延长预约有效周期,提高客户picked的成功率。

Homework2

第二次作业程序中主要有三个实现重点:

  • 时间判断:本次作业中,续借请求审核、还书是否超期等判断均需要进行时间判断。在程序中,主要借助于LocalDate类存储操作的时间日期,在时间判断中,主要借助该类中的plusDays()方法来获得截止日期,借助该类中的isBefore()isAfter()方法来比较日期大小(需注意isBefore()isAfter()方法不包含当天,即[2024-10-01].isBefore([2024-10-01])是不成立的)。在Inf类中,实现了不在操作时限内方法overOperationLimit()和不在续借时限内方法overContinueLimit(),如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //不在操作时限内,主要用于归还是否超时判定和预约是否超时判定
    public boolean overOperationLimit(LocalDate now)
    {
    //从opDate后一天开始计算,limit天后无效(不包含opDate+limit当天)
    return opDate.plusDays(limit).isBefore(now);
    }

    //不在续借时限内,主要用于是否处于续借时间判定
    public boolean overContinueLimit(LocalDate now)
    {
    //从opDate后一天开始计算,limit-4天内无效(不包含opDate+limit-4当天)
    //从opDate后一天开始计算,limit天后无效(不包含opDate+limit当天)
    return opDate.plusDays(limit - 4).isAfter(now) || opDate.plusDays(limit).isBefore(now);
    }
  • 正式书籍与非正式书籍:本次作业中,建立了正式书籍和非正式书籍两种书籍类型,具有不同操作,存在于不同位置。同时在借阅处归还后进行书籍返还整理时,应对非正式书籍进行升级,转变为正式书籍,仅改变书籍的类型,不改变书籍的序号。在官方包中,给出了LibraryBookIdtoFormal()方法。在该方法中,非正式书籍转为正式书籍时,会返回一个新的LibraryBookId对象。这时候会产生两本原本书籍类型序号均相同的非正式书籍(使用同一个LibraryBookId对象)经过转换后不相同(类型序号均相等,但使用两个不同的LibraryBookId对象)的问题,会对后续书籍存储、书籍类型判断产生影响,因此需要重写LibraryBookId类的equals()hashCode()方法。

    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
    //bookId的toFormal()方法,会产生新对象
    public LibraryBookId toFormal()
    {
    switch (this.type)
    {
    case AU:
    return new LibraryBookId(LibraryBookId.Type.A, this.uid);
    case BU:
    return new LibraryBookId(LibraryBookId.Type.B, this.uid);
    case CU:
    return new LibraryBookId(LibraryBookId.Type.C, this.uid);
    default:
    return this;
    }
    }

    //重写后的equals()方法
    public boolean equals(Object var1)
    {
    if (this == var1)
    {
    return true;
    }
    else if (var1 != null && this.getClass() == var1.getClass())
    {
    LibraryBookId var2 = (LibraryBookId)var1;
    return this.type == var2.type && Objects.equals(this.uid, var2.uid);
    }
    else
    {
    return false;
    }
    }

    //重写后的hashCode()方法
    public int hashCode()
    {
    return Objects.hash(new Object[]{this.type, this.uid});
    }
  • Service接口的实现:本次作业中,实现了Service的接口,实例化出各种服务,并将其储存在System类中进行调用。Service接口主要实现work()方法,每个实例化的Service内含library属性,并在work()方法中使用。

Homework3

第三次作业程序中主要有两个实现重点:

  • 借阅超期的扣分机制:用户每次成功获得书籍,在该书应归还日期的当日闭馆后,若用户仍未归还图书,该用户信用积分减2。请注意,应该在借阅期限当天扣除积分,而不是归还书籍当天扣除。因此在Customer类中新建属性returnBooks表示客户拥有的尚未扣除积分的书籍,在每日结束时对该属性遍历扣除积分,已扣除积分的书籍移除。
  • 整理流程的顺序安排:按照要求,在闭馆后,应先对借阅超期和预约超期的客户进行分数扣除,再对有捐赠非正式书籍转正的客户进行加分。因此,闭关后整理流程的顺序安排应是:遍历客户,对客户进行借阅逾期积分扣除整理预约处书籍,对客户预约逾期未取积分扣除,并将书籍返还书架整理借阅处书籍,对有捐赠非正式书籍转正的客户加分,并将书籍返换书籍和图书漂流角整理书架,按照预约请求将书籍运送至预约处。

三、Bug分析及程序优化

Homework1

Bug分析

本次作业暂无bug。

优化策略

本次作业对性能要求较低,暂无优化策略。

Homework2

Bug分析

本次作业暂无bug。

优化策略

本次作业对性能要求较低,暂无优化策略。

Homework3

Bug分析

本次作业暂无bug。

优化策略

本次作业对性能要求较低,暂无优化策略。

四、心得体会

在学习过前三个单元后,第四个单元迎来了完全自由的代码框架设计。不得不说,经过前三个单元的魔鬼训练,笔者学到了许多理念与思想,并运用在了本单元中,比如说本单元中的核心类BookBuffer类,就是参考第二单元中的RequestQueue类进行设计的。同时,按照相像性设计了Office类和Service接口,将一些类进行抽象,并将其存储在System的数组中,可以实现更进一步的方法抽象。

那么心得体会部分就主要谈谈这单元完成之后的一些收获和经验吧:

  • 架构设计的重要性。好的,这已经是第四遍了。相较于第三单元,本单元实现难度大幅降低,设计自由度大幅提高。好的设计逻辑更加鲜明,实现更加抽象,复用性更高。
  • 各类语言的阅读能力。 第三单元作业出现了JML语言,本单元作业中出现了UML语言。UML语言可视化程度高,阅读更加清晰明白。但是starUML从头构建UML图确实是一件繁琐的事情,耗费大量时间。同时,正常来说,UML图的设计应在程序设计最开始进行,但是在真正实现时,会发现很多细节都有瑕疵,需要不停修改,较为难办。
  • 测试的重要性。 在第三单元中,最重要的是程序测试,本单元中同样重要。本单元中,由于服务种类众多,组合出很多种情况,又存在如信誉积分这种影响全局的属性,因此需要进行详尽的测试。

至于对课程的建议,就留到期末总结中说吧。

最后,还是要感谢助教wwr学长的帮助。也希望在之后的学习中能再接再厉,继续进步。