BUAA OO 2024Unit2总结

BUAA OO 2024Unit2总结

00前言

写在前面

第二单元的作业难度可谓是直线上升,并且三次作业迭代的难度都呈现不减的趋势。第二单元的核心是Java多线程程序设计,而多线程程序的最大特点之一就是不可再现性,这导致编写代码和debug的难度相比第一单元更难。关注并发行为的线程管理线程安全是这一单元的核心要点。另外本单元我了解了上一单元的心愿,成功搭建了测评机。

但是成功送出19杀。。。(还是想说一句peace and love)

顺便一提,如果新主楼的电梯系统是一个能够操控乘客上指定电梯、乘客没有正确登上指定电梯便会离奇失踪、到个别楼层就把乘客赶下来、时不时会陷入重置状态,甚至会突然有一天自我改造成双轿厢电梯的系统——那我大概率一辈子都不会去坐的。

你需要一定了解的:

  • 创建多线程和重写run方法:

    public class myThead extends Thead { // 或许可以采用实现Runnable接口的方式创建新线程
        public void run() {
            try {
                while (true) { 
                    if (/*线程结束*/) {
                        return;
                    }
                    else if (/*无新任务*/) {
                        wait(); // or sleep();
                    }
                    // do the task
                }
            } catch (InterruptedException e) {
                // 异常处理
            }
        }
    }
    
  • synchronized关键字:

    /* 1.对象锁 */
    // 1.1 锁非静态变量 或 this对象
    synchronized (obj) { /*do something*/ } //只允许一个线程访问obj
    synchronized (this) { /*do something*/ } //只运行一个线程访问this对象
    // 1.2 锁非静态方法
    synchronized void method() { /*do something*/ } //只允许一个线程调用方法method
    /* 2.类锁 */
    // 2.1 锁静态变量 或 静态方法
    synchronized (static obj) { /*do something*/ }
    synchronized static void method() { /*do something*/ }
    // 2.2 锁类
    synchronized (myThread.class) { /*do something*/ }
    
  • wait()notify()notifyAll():

    • wait():将当前线程放入等待队列并释放锁
    • notify():随机唤醒一个等待队列中的线程
    • notifyAll():唤醒所有等待队列中的线程
    • wait() 和 notify() /notifyAll()方法都必须搭配 synchronized同一个锁对象。如果wait()和notify()/notifyAll()作用于不同的锁对象,是没有任何作用的。
  • 读写锁ReadWriteLock

    public class MyReadWriteLockExample {
        private int data = 0;
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
        public int readData() {
            lock.readLock().lock(); // 获取读锁
            try {
                return data; // 读取共享资源
            } finally {
                lock.readLock().unlock(); // 释放读锁
            }
        }
    
        public void writeData(int newValue) {
            lock.writeLock().lock(); // 获取写锁
            try {
                data = newValue; // 写入共享资源
            } finally {
                lock.writeLock().unlock(); // 释放写锁
            }
        }
    }
    
  • 死锁:

    当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁:这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁

    // 一种常见的死锁的例子:线程1和2都因为等待对方释放锁而陷入死锁
    //线程1
    synchronized (objA) {                    // 线程1先拿到了objA的锁,线程2先拿到了objB的锁
        synchronized (objB) {                // 由于线程2没有释放objB的锁所以线程1等待
            //do some thing                  
        }                                    
    }                                        
    //线程2                                    
    synchronized (objB) {                    // 线程2先拿到了objB的锁,线程1先拿到了objA的锁
        synchronized (objA) {                // 由于线程1没有释放objA的锁所以线程2等待
            // do some thing            
        }                                    
    }                                       
    
  • 生产者消费者模式:
    在这里插入图片描述

01第五次作业

本单元作业目标是模拟一个多线程实时电梯系统。情景为一座1-11层的大楼内有6台电梯,系统会从输入中读入乘客请求信息(起点层、终点层),然后通过分配电梯并调度电梯运行、开门、关门实现每位乘客的请求。电梯初始默认位于1层,移动、开门、关门需要时间,限乘人数为6。

类图分析

UML类图
在这里插入图片描述
协作图
在这里插入图片描述

架构设计

多线程设计

本次作业我设计了三个线程:输入线程(InputHandler),调度器线程(Scheduler),电梯控制线程(ElevatorController)。三个线程关系如下:
在这里插入图片描述

  • 输入线程:从官方提供的输入包内获取请求,并放入主请求列表RequestTable类。
  • 调度器线程:从RequestTable中拿取请求,并且分配给各个电梯的请求表WaitTable。由于第五次作业的输入是指定电梯的,因此本次作业不需要编写调度策略。
  • 电梯控制线程:共6个,从WaitTable中拿取请求然后根据请求执行电梯操作。

不难看出,这样的设计实际上是实现了两个生产-消费者模式InputHandler-(RequestTable)-SchedulerScheduler-(WaitTable)-ElevatorController

结束线程条件如下:

  • 当从官方输出包读取到null时,requestTable设为end,同时return
  • requestTablerequestTableend,则将waitTable设为end,同时return
  • waitTablewaitTableend电梯内没有乘客时,结束return

在设计线程时,我有意的将线程方法单一化,即线程中只包含重写的run方法。这样的设计思路可以理解成把线程当做一个MainClass(主线程本身也是一个线程),只起到判断有无任务执行任务结束进程三个基本操作。具体任务的执行则交给其他类协作完成。

同步块设计

本次作业的共享对象(如requestTablewaitTable)均使用synchronized将读写方法上锁。为了避免死锁,我尽量选择对方法上锁,并且避免出现不安全的互斥锁嵌套,避免死锁。本次作业中我也没有出现线程不安全的问题。

电梯运行策略设计

本次作业的电梯运行策略采用的是Look算法:

规定:乘客请求方向是指目标楼层 - 出发楼层的符号,若为-则表示向下,+表示向上。

  • 首先判断电梯内是否有乘客要下电梯。若有则open
  • 判断waitTable.isEmpty() && elevator.isEmpty()
    • 若是,且waitTable.isEnd(),则电梯线程结束over
    • 若不是,则电梯线程等待wait
  • 接着判断当前电梯运行方向上有无乘客的出发楼层在当前楼层及前方楼层上:
    • 若前方楼层上有,则保持方向不变;
    • 若前方楼层上没有但当前楼层有乘客请求且乘客请求方向电梯运行方向一致,则也保持方向不变;
    • 否则,电梯转向reverse
  • 再判断当前楼层有无乘客需要上电梯:
    • 若乘客运行方向与电梯运行方向一致,则上电梯。
  • 最后电梯关门close然后移动move

具体实现上我是创建了一个Elevator类用于模拟电梯各种行为(如openclosemove)和一个策略类Strategy用于给出电梯运行状态(overwaitreverse)。这两个类作为ElevatorController线程的成员变量。值得一提的是,先完成第三步电梯运行方向的判断再判断有无乘客上电梯可以减少个别情况下多开关一次门的问题。

look策略的核心实现放入Strategy类是一种很好的高内聚低耦合的设计思路,这样如果后续策略需要变更运行策略,只需要修改Strategy类即可。

bug分析和hack思路

本次作业我在强测和互测中均未发现bug。但在设计过程中我曾出现过一个严重bug

// 根据策略类获得下一步
Strategy.Type action = strategy.getAction(elevator, waitTable, openFlag);
switch (action) {
	case OVER:
		return;
    case WAIT:
        synchronized (waitTable) {
            waitTable.wait();
        }
        continue;
}
  • 问题的表现是有请求到来时电梯并没有移动去完成这个请求,一直维持在wait
  • 后来我发现是因为我在Strategy类中判断出要电梯要over或者wait后执行了close!这条close出现的理由是,如果此时送走电梯中最后的乘客后电梯就可以结束运行或者等待,那么就需要关门。
if (elevator.isEmpty() && waitTable.isEmpty()) {
    if (waitTable.isEnd()) { //进程结束
        if (openFlag) {
            sleep(200);
            TimableOutput.println(String.format("CLOSE-%d-%d",elevator.getCurFloor(), elevator.getId()));
        }
        return Type.OVER;
    }
    if (openFlag) {
        sleep(200);
        TimableOutput.println(String.format("CLOSE-%d-%d",elevator.getCurFloor(), elevator.getId()));
    }
    return Type.WAIT;
}
  • 你可能会觉得这好像没什么问题呀,但问题就在这:close操作需要sleep0.2秒。当我已经判断完需要wait之后就会因为关门而sleep0.2秒。如果这0.2秒来了一条请求,我的电梯是不知道的。自然而然电梯继续wait,而那个人请求永远不会被执行。这简直就是灾难,试想一下在德国无限速公路上狂奔着几十辆时速超过200的汽车,而你在大马路上睡了0.2秒,这0.2秒足以让你的程序生命灰飞烟灭……
  • 问题的解决十分之简单,把关门的0.2秒跟开门的0.2秒合并在一起,放在开门阶段0.4秒就好了。对,就是这么简单

总结下来的经验教训就是不要在分析策略阶段消耗太多时间,这个时间即包括CPU时间,也包括线程等待的时间。否则会出现很多冲突和线程不安全的问题。

接下来讲一下hack思路。本次作业我成功hack他人一次。

在很多人的设计里,当有乘客要上电梯时,则从请求表中移除该乘客。而很多人采取的look策略需要根据当前请求表来判断电梯下一步状态。这个时候可能会出现ConcurrentModificationException,出现原因就是没有在遍历请求表时对请求表上锁,导致出现一边遍历一边删除的异常。构造的hack样例就是针对这一情况,在某一时间戳大量涌入相同出发楼层、相同出发电梯、相同目标楼层的请求。

最后简单说一下搭建测评机策略。本单元的数据生成器相比上一单元简单,所以测评难点在于搭建正确性检验函数。由于多线程的输出不确定性,因此本单元的测评机主要采取正确性检验的评价思路,即根据输出模拟电梯和乘客行为,当电梯和乘客出现不合理行为时认为输出有误(如抵达不存在楼层,超载,开关门顺序等)。

02第六次作业

本次作业新增一类请求和一类约束。新增RESET请求,接收到重置指令的电梯需要尽快停靠后重置运行速度和载客上限,再投入电梯系统运行。电梯重置时内部不可以有乘客,且重置动作需要时间1.2s。新增RECEIVE约束,本次作业乘客请求将不再指定电梯,因此需要编写电梯调度策略,当电梯接收到请求时输出RECEIVE,未输出RECEIVE时电梯移动是不合法的行为(因此电梯调度策略不能选择自由竞争!)。

类图分析

uml类图
在这里插入图片描述

协作图
在这里插入图片描述

架构设计

本次作业我并没有新增线程或是改变原有的设计模式,核心点是增加了电梯调度策略以及电梯RESET请求。

多线程设计(更新)

本次作业我没有增加新的线程,唯一更改的是调度器线程的结束条件。不仅要判断requestTable是否为空且结束,还有确保此时没有正在重置的电梯。

if (requestTable.isEmpty() && requestTable.isEnd() && ResetCount.isZero()) { /*结束*/ }
同步块设计(更新)

由于新增RESET请求,需要接收到重置请求的电梯主动把电梯中的乘客赶出去!!(什么都市传说)被赶出去的请求只能重新扔回主请求队列,这样就在原来基础上新增了Elevator-(RequestTable)-Scheduler模式。
在这里插入图片描述

为了保证线程安全,从requestTable接收电梯线程的请求的方法也要上同步锁。

为了防止调度器线程被占用而导致的RESET请求不能及时发送给电梯(课程组要求RESET请求发出后电梯必须在两次移动内开始重置),同时也是为了让调度器只起到调度电梯分配的功能,我将输入线程获取到的reset请求直接发送给了电梯线程。相关的方法我也全部用synchronized上锁。

电梯调度策略(新增)

想必提前对本单元作业有了解的同学应该听说过一种大名鼎鼎的调度方法——影子电梯,这种方法有着局部最优解的美名,但其实现思路以及实际意义实在不合我的口味,因此我本次作业并没有采用性能或许最优的影子电梯,也没有采取随机性极大的random或模6法,而是采取了赋权评价函数的方法,想法在于根据当前电梯状态乘客候乘状态为当前电梯计算一个评价分数,然后将乘客分配给分数更优的电梯。这种思路看似只是一种模拟,但如果能够成功抓住事物主要矛盾,在绝大多数强测点性能上完全不输影子电梯!

首先先关注当前电梯与乘客的位置关系。首先假定一栋6层的大楼,一个乘客请求是从4楼向上走(在我的模拟思路中不需要关注乘客的目标楼层),那么运行中的电梯相对于这名乘客的位置关系大概有如下四种:

在这里插入图片描述

  • 第一种,电梯运行方向恰好与乘客请求方向相等,且电梯楼层位于乘客出发楼层的后方,即满足最优捎带情况。在不考虑超载情况下此时无疑是最优情况。
  • 另外三种情况均不能达成最优捎带的情况,电梯都需要进行一次折返
  • 实际上还有第四种情况,那就是电梯处于等待状态,这种情况下电梯始终能满足最优捎带。但考虑电梯总耗电量,都能达成最优捎带的前提下,运行中的电梯往往优于等待中的电梯。

于是乎形成了权重的第一部分的计算方法:

double weight;
Elevator elevator = elevators.get(i);
WaitTable waitTable = waitTables.get(i)
double moveTIme = elevator.getMoveTime();    //以ms为单位
int elevatorDirect = elevator.getDirection();
if (elevator.isWait()) {
	weight = moveTIme * Math.abs(elevator.getCurFloor() - request.getFromFloor()) + 1;
	//为减小耗电量,等待中的电梯不如运行中的可捎带电梯
} 
else if (passengerDirection == elevatorDirect
           &&(request.getFromFloor() - elevator.getCurFloor()) * passengerDirection >= 0) {
    weight = moveTIme * Math.abs(elevator.getCurFloor() - request.getFromFloor());
} 
else {
    int goal = waitTable.getGoalFloor(elevator.getCurFloor(), elevatorDirect);
    weight = moveTIme * (Math.abs(elevator.getCurFloor() - goal)
                         + Math.abs(goal - request.getFromFloor()));
} 

最后一个else中的临时变量goal表示电梯开始折返的楼层,是根据当前电梯的请求队列来计算的。

int i = curFloor; //电梯当前楼层
int goal = i;
int floor;
while (i > 0 && i < 12) {
    if (!waits.get(i).isEmpty()) { 
        for (Passenger passenger: waits.get(i)) { //遍历位于i楼层的乘客请求
            if (/*电梯方向向上*/) {
                goal = max(/*反方向请求的出发楼层,同方向请求的目标楼层*/);
            } else {
                goal = min(/*反方向请求的出发楼层,同方向请求的目标楼层*/);
            }
        }
    }
    i = i + direction; //只判断在电梯运行方向上的请求
}

再关注电梯的乘客数量和请求数量。

weight += (waitTables.get(i).getSize() > 10) ? 4000 : 0;            //电梯请求表数量不能过多
weight += elevator.isFull() ? elevator.getLimitNumber() * 100 : 0;  //电梯乘客数量不能过多

最后如果电梯正在重置,也不能分配请求。

if (elevator.isResetting()) { weight += 100000; }

bug分析和hack思路

我本次作业出现了两个bug,一类是wrong answer,一类是RTLE。

  • wrong answer的原因是当六台电梯全部重置的时候我仍然会分配给一台电梯。解决方案是判断一下当六台电梯全部RESET时就等1.2秒等电梯重置完成再分配。(顺便一提这个显而易见的错误使我在互测中被hack了16次。。。)
  • RTLE是中了围师必阙策略的毒害。大概意思是先一开始将一台电梯重置成最慢载重最小的电梯,再将另外五台电梯全部重置,重置过程中涌入海量的11楼到1楼请求。如果调度策略没有多加思索,那么很可能会把所有请求分配给此时没有在重置的那台最糟糕的电梯,造成超时。解决方案也很简单,一旦可用电梯过少就不再分配,等待电梯重置结束再继续。

hack思路主要参考“围师必阙”策略,以及构造六台电梯全部重置的同时提供请求的样例,成功刀中6次。

03第七次作业

本次作业新增了第二类重置请求,可以把电梯重置成双轿厢电梯。要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。当重置完成后,轿厢 A 默认初始在换乘楼层的下面一层,轿厢B默认初始换乘楼层的上面一层。双轿厢电梯的耗电量是普通电梯的1/4!

类图分析

uml类图
在这里插入图片描述

协作图
在这里插入图片描述

架构设计

多线程设计(新增双轿厢电梯控制)

对于新增的第二类重置请求,我的处理是结束原有电梯控制线程,再新运行两个电梯控制线程代表双轿厢的两个轿厢。两个轿厢之间用Shaft类共享换乘楼层资源,并且通过Shaft保证两轿厢不同时位于换乘楼层。除此之外两个双轿厢线程彼此独立,拥有各自的电梯和电梯请求表,都采用look算法运行。双轿厢电梯控制线程DoubleCarElevatorController与普通电梯控制线程功能上无太大区别,并且由于双轿厢电梯不会再接收重置请求,因此每个轿厢的行为更接近第五次作业。

每个轿厢的移动范围将不再是1-11层,因此双轿厢电梯可能涉及到乘客的换乘,即把乘客从换乘楼层扔出电梯,用类似第一类重置请求的办法回传给requestTable,再由调度器分配给能完成请求的电梯。

调度器线程的结束判断需要在原来基础上加上所有乘客请求都已完成,然后同时结束将所有电梯请求表setEnd。如果不同时结束电梯的话,也要至少保证同一电梯井内的两个双轿厢电梯同时结束,防止需要换乘的请求得不到解决。因此最方便也是最合理的设计是记录未完成的请求数量,请求增加时加1,请求完成时减1;当满足原来的结束条件且未完成请求数量==0时,所有电梯全部setEnd

同步块设计(新增双轿厢电梯控制)

这里主要提一下我是如何实现保证两轿厢不同时位于换乘楼层的。

  • 首先需要一种类似信号量的机制,当没有电梯位于换乘楼层时,运行一个轿厢运行至换乘楼层(空闲让进);
  • 只能有一个双轿厢电梯拿到这个信号量,另一个若也想到达换乘楼层则只能等待忙则等待);
  • 同时如果轿厢需要到达换乘楼层,那么在有限时间内必须能让电梯到达换乘楼层(有限等待);
  • 当某一轿厢长时间不能到达换乘楼层,需要赶走另一个轿厢避免忙等(让权等待)。

我的Shaft类存储轿厢A和轿厢B的当前楼层和换乘楼层。每次轿厢移动的同时改变Shaft类中记录的当前楼层。接下来将电梯的移动行为改写move方法:

public void move() throws InterruptedException {
	int nextFloor = elevator.getCurFloor() + elevator.getDirection();
	synchronized (shaft) {
        // 如果要运行到换乘楼层,且无法到达换乘楼层
        if (nextFloor == shaft.getTransferFloor() && !shaft.canMoveToTransferFloor(type)) {
            shaft.wait();             //等待另一轿厢移动
            Thread.sleep(100);        //防止另一轿厢处于换乘楼层且未到达另一楼层时,本轿厢先到达换乘楼层
        }
        // 非换乘楼层或者换乘楼层可达
        shaft.setCurFloor(type, floor); //每次移动都尝试唤醒等待的线程
    }
    super.move() //继承普通电梯控制线程的move方法。
}

其中Shaft.canMoveToTransferFloor()方法是判断换乘楼层处有没有轿厢停靠,需要上锁。setCurFloor()用于写轿厢位于的楼层,因此也要加锁。这样的设计实现了空闲让进和忙则等待。

为了解决有限等待和实现让权等待,我在电梯wait条件判断上限制了双轿厢电梯不能在换乘楼层陷入等待,这样天然解决了有限等待和让权等待的问题:你根本不必要去赶走另一台电梯,因为它不会在那里睡下;你也不必去焦虑等待的时间,因为另一台电梯总会离开。

另外实现第二类重置请求的过程中,我需要新增电梯和电梯对应的请求表,还需要创建Shaft类,并启动两个双轿厢控制线程。这里我没有把新增电梯和新增请求表添加进Scheduler类原本的电梯列表和请求表列表,而是新增了一个couple电梯和请求表队列。这样可以避免一些线程安全问题。

private static HashMap<Integer, WaitTable> waitTables;
private static HashMap<Integer, WaitTable> coupleWaitTables;
private static HashMap<Integer, Elevator> elevators;
private static HashMap<Integer, Elevator> coupleElevators;

普通电梯线程中,解决第二类请求的代码块被我上了锁(锁waitTable),防止重置阶段给电梯分配请求。

电梯调度策略(更新双轿厢电梯)

新增双轿厢电梯后,我依然采取之前的赋权法,只是做了一些调整。

  • 如果电梯为普通电梯,则计算方法不变。这回为了让代码结构看起来更清晰,我把之前计算权重的方法封装成了normalWeight()方法。

  • 如果电梯为双轿厢电梯,先判断请求该分配给哪个轿厢:

    if (/*出发楼层位于下区*/ || /*出发楼层位于换乘楼层且请求方向向下*/) { /*给A*/ }
    else { /*给B*/ }
    

    再讨论只调用被分配的轿厢能否完成请求,如果可以则权重为调用normalWeight得到的值;如果不能,则权重大小为被分配电梯的normalWeight(出发楼层为乘客的出发楼层)+另一轿厢的normalWeight(出发楼层为换乘楼层)。考虑到双轿厢电梯的耗电量优势,因此我在最后的权重*0.7

    //以分配给A厢为例,normalWeight传入参数为电梯、出发楼层、电梯请求表、乘客请求方向
    weight = normalWeight(elevator, fromFloor, waitTable, direction);
    if (goalFloor > transferFloor) {
        weight += normalWeight(couple, transferFloor, coupleWaitTable, direction);
        weight *= 0.7;
    }
    
  • 为了应对看不见的敌人——线程安全的问题,为了防止分配给无法完成任务的双轿厢电梯,为了防止分配乘客时将电梯的厢号甚至种类判断错误,我添加了一道保险——evaluateAndAdd()。其目的是在分配给电梯前的最后时刻检查这条RECEIVE是否合理。如果分配给了不存在或是无法完成任务的电梯,该方法帮助我修正请求并给出正确的分配。

bug分析和hack策略

本次作业最后遗憾落幕,是因为我错了一个强测点。另外也被一条RESET请求成功hack。最让我不能接受的是错误的原因竟然跟第五次作业一模一样!(说好的人不会两次踏入同一条河流呢555)我在第二类重置请求的同步锁块里执行了1.2秒的sleep然后启动两个双轿厢线程,这期间内发送过来的进程结束请求(setEnd)可能会因为双轿厢线程正在执行wait判断而错过。我将线程的睡眠和新线程启动扔出同步块之后就解决了这个问题。

本次作业我hack别人三次,hack成功的样例还是第二次作业的两类样例。

顺便吐槽一句:当我把没有bug修复过的代码直接提交bug修复若干次,挂掉的强测点居然一次都没再测出过问题。一句话——多线程的事儿你少打听!交就完了!

04心得体会

闻风丧胆的第二单元结束了,回想起来其实满是遗憾。上一单元还可以略微得意,这一单元就给我当头一棒。记得第二次作业是在清明放假,去南京的火车上完成的,为了设计一个好的调度策略绞尽脑汁,闲暇之余也是在女朋友陪伴下debug。事实证明面向对象编程并不能提高面向对象编程的能力。第二天进入互测房后我就被hack了13次,最后被hack了18次。然后周一晚上十一点一查互测成绩,真是成也萧何败也萧何,调度策略让我出现了不少100分的测试点,但也导致我强测wa了两个点。第三次作业则出现了更为严重的线程安全问题,尽管正确点的性能分都十分优秀,但仍然掩盖不了这是一坨随时可能停不下来的屎山

这不禁让我思考起这一单元的最本质的问题:为什么要学习这一单元?显然这一单元的任务核心是多线程设计,在这一目标上显然我做的还不够好,经常出现各种各样莫名其妙的线程安全问题,只能靠打补丁似的修正来弥补多线程漏洞。多线程的不可再现性也让debug难度骤增,经常出现评测机报错但是本地无法复现的情景。如果现在让我重新设计这台电梯,我可能更多的时间会花在多线程设计上而不是调度策略设计上。

从层次化设计的角度上思考,本次作业一是熟悉了生产-消费者模式,二是熟悉了线程设计。设计线程功能时我有意的把线程当做一种MainClass来看待,其中只包含run方法。后来为了使代码阅读性提升和通过checkStyle,我把run方法中的一些代码块包装成了方法,但其本质上也只是在run中被调用,仍然不失(我所认为的)设计的职能单一性。此外包括电梯控制线程与电梯类的解离:我并没有将电梯与电梯线程合二为一,电梯只是电梯线程的一个成员变量,除此之外还有请求表等。这样的设计是将电梯控制视为线程的职能,而电梯运行是由电梯方法所实现的。再包括策略类与电梯类的分离,requestTable直接分发重置请求给电梯等,都是从单一职能的设计思路来实现的。

最后感谢oo这一单元对我的历练,希望有一天我们写的电梯系统不要变成新主楼的电梯系统。

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值