面向对象的程序设计(2019)第二单元总结

I  对问题的初体验

在开始OO之旅前,对OO电梯早有耳闻。这一次终于轮到我自己实现OO电梯了。首先从顶层需求出发对电梯系统进行分析,对象包括电梯、任务和乘客。对于乘客而言,因为一个乘客由ID标识且仅会在一个生命周期中产生一个请求,因而可以和任务合并一体,作为一个输入线程实现。经过上述简化电梯调度模拟系统最核心的部分就落在了电梯模块和任务调度模块上。在每一次电梯作业的更迭里,慢慢寻找工程化和优化之间的微妙平衡。

II 三次的设计思路

A 单电梯 FCFS 调度算法实现

可以说这个作业算是电梯系统的开始。其本意可能是让我们分析出这个问题的对象并且实现基本的线程思想。在这个任务中,我将主函数线程和输入轮询线程合并,赋予其初始化与轮询获取输入的功能。对与调度器我认为没有必要使其成为一个独立的线程,而应该让他成为一个共享对象在各个电梯之间共享。这一次的目的选层式的电梯设计,输入输出接口的简化以及连续的正数楼层给了我们充分的思考和准备时间,让我们更合理的设计电梯。在线程的安全性方面,电梯需要访问调度器中的任务队列完成任务的分配,且任务队列还需要接受输入线程的输入请求,因而在每一次操作时都应加锁。

B 单电梯多楼层捎带调度算法实现

这一次的作业相较上一次,增加了调度算法的复杂度,也增加了地下楼层这一设定。在最开始就要牢记0层不能停的事实。对于捎带的实现,我使用一个任务队列存储所有当前上线的任务,并且定义了电梯内部正在执行的计划类。对于电梯内部的计划,包含电梯当前的运行方向,需要停靠的楼层,以及在每一个楼层上下电梯的乘客号。在电梯到达或经过每层时,会向调度器请求捎带任务。调度器负责过滤出可捎带任务,之后加入电梯计划中执行。在测试中发现很多时候因为评测样例喜欢在0秒钟塞入成吨的数据,使得没有来得及读入的请求不能被很好的捎带。多线程间的协同体现在输入模块读取输入,电梯线程获取相关计划,和第一次作业类似,只需要对任务队列加速保护即可。

B+ 单电梯多楼层捎带调度算法优化

在优化中,我选择在每个任务到达的时候,调度器会首先将未分配的任务按照一定的规则组合成电梯计划,当电器请求时一并交给电梯执行,这样可以保证等待队列的顺序是贪心的最优解,提高算法的效率。但是在实际强测过程中因为时间间隔较短、评测用例较为规律化导致这种算法的效率不算很高,甚至有时会弱于扫描算法。

调度器组合请求的顺序根据一个性能函数来判断贪心最优解:对于每一个电梯计划的可插入位置,计算该电梯计划因为新添加的计划所导致的额外的开销。若某一处加入后的开销最小,且小于任务本身的开销时间,则选择在该位置插入任务。否则,将任务单独作为新的电梯任务插入。

同时,在电梯运行过程中,也会不断查询调度队列寻找可以加入的新计划。新计划需满足:和当前电梯运行方向相同、电梯尚未到达起点楼层且计划间有楼层重叠。

当电梯执行好一个计划后,优先选择调度器队列中距离最长的反向任务执行。(受电梯扫描算法启发)当当前任务执行完毕时,电梯可以偷窥下一个对应的任务的起始楼层是否和当前电梯所在楼层相同,若相同则可以省去一次开门的时间。

C 多电梯多楼层捎带调度算法实现

第三次作业从体量和内容上都比第二次作业增加了不少。其中还最大的不同还属于电梯能够停靠的层数发生变化,且一个请求可能需要多个电梯之间的协作完成。对于这个问题,为了提供一个统一的解决方案,我决定使用一张图来描述整个电梯系统的状况。图中的节点为电梯系统所有可以到达的楼层,楼层间的边则代表可以在两层间运行的电梯。对于一个请求,只需要在图中计算最短路即可得到拆分后的任务队列。

在前两次作业的基础上,电梯类可以说完全沿用了第一次作业的设计。为了适配多电梯协作任务的完成,为计划队列增加待完成计划这个属性。从设计上来讲,我希望在调度队列中的所有任务均是待命状态,这就需要协同任务的后续请求需要在前序任务完成时出现在队列中。这样的设计可以极大地简化调度队列的维护和查询,提高代码简洁度。多线程之间的协同产生于输入线程为调度器提供输入,电梯向调度器请求任务执行。为了保证线程安全性,需要确保共享的调度器中的关键对象——调度队列在读写过程中加锁。

C+ 多电梯多楼层捎带调度算法优化

在完成基础图算法的基础上,开始探寻优化的空间。对于图算法,边权重的设计就值得考虑了。在优化版本中,我考虑为图的边赋予一定意义的数值。具体而言,对于可以直达的边,其时间开销为一次开门时间附加该电梯在两层之间的运行时间。对于不可达的边,其权值为中介可达路径的时间开销总和。此外,还需要额外附加电梯当前位置到任务起始位置的响应时间,以确保局部的贪心算法。这样,在图中运行 Floyd-Warshall 算法获得任意两点间权值最小的路径,即是在当前时刻最优的分配。

值得注意的是,图算法仅能够提供当前多个电梯协同任务的第一段分配。其他分配过程需要根据该任务完成时的电梯状况而定,不应该提前划分。这种优化方式也带来了一些潜在的问题。其中之一就是,不同的任务在不同的时间点可能被分配给不同的电梯来执行,这就要求当电梯在空闲状态是需要以一定的时间间隔检查是否有可以执行的任务来执行,而不能用通知的方式来实现。但是鉴于电梯运行时间较长,所以间隔查询的时间也不需要很长,所以这个过程并不过分消耗CPU时间。

Bug

明明知道 LinkedList 线程不安全但是还是鬼使神差的在程序里用了,可能是哪天脑子抽风了写进去的吧...哭晕,又一次错惨了。

III 解决方案的评估

A  自动化测试

这一次,鉴于不同作业要求的电梯输出和功能都略有差别,因而选择搭建一个较为灵活可变的框架实现三次电梯作业的自动化测试。多线程问题错误的出现不可复现,不便于调试,因此选择随机生成测试集,利用测试系统的形式是使用终端脚本运行多个协同的程序并最后检查结果。自动化测试的文件结构如下:

.

├── README.md

├── start.sh└── test_elevator

├── clean.sh├── comm.py

├── elevator-input-hw3-1.4-jar-with-dependencies.jar

├── elevator_tester.jar

├── gen.py

├── test.sh└── timable-output-1.0-raw-jar-with-dependencies.jar

自动化测试由命令 bash start.sh 开始,执行目录 ./test_elevator/test.sh 脚本。该脚本负责运行主要的 Java-Shell 交互程序 comm.py,由 gen.py 生成随机数量、随机间隔的请求数据并由 Python 作为桥梁输入给待测试的 Java 电梯程序,捕获输出并交给 elevator_tester.jar 检查结果,最终将运行结果返回给 test.sh 脚本。

为了方便不同参数下的自动测试,start.sh 被设计成可以将一些参数写入文件中作为 cache 的特性。在第一次指定必要参数后,之后的运行不必重复进行。

1 #!/bin/bash2 if [ ! -d "test_elevator" ]; then

3 echo "Dependency Directory test_elevator Not Found!"

4 exit 1

5 fi

6 if [ $# -gt 0 ]; then

7 echo "Setting Cached Parameters: Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name"

8

9 echo "$1" > test_elevator/num.cache10 if [ $# -gt 1 ]; then

11 echo "$2" > test_elevator/request.cache12 fi

13 if [ $# -gt 2 ]; then

14 echo "$3" > test_elevator/interval.cache15 fi

16 if [ $# -gt 3 ]; then

17 echo "$4" > test_elevator/project.cache18 fi

19 if [ $# -gt 4 ]; then

20 echo "$5" > test_elevator/package.cache21 fi

22

23 uname > test_elevator/system.cache24 else

25 if [ ! -f "test_elevator/num.cache" ]; then

26 echo "Parameters Test_Rounds Unset!"

27 echo "Try Setting Parameters By:"

28 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name"

29 exit 1

30 fi

31 if [ ! -f "test_elevator/request.cache" ]; then

32 echo "Parameters Max_Requests Unset!"

33 echo "Try Setting Parameters By:"

34 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name"

35 exit 1

36 fi

37 if [ ! -f "test_elevator/interval.cache" ]; then

38 echo "Parameters Max_Interval Unset!"

39 echo "Try Setting Parameters By:"

40 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name"

41 exit 1

42 fi

43 if [ ! -f "test_elevator/project.cache" ]; then

44 echo "Parameters Java_Main_Path Unset!"

45 echo "Try Setting Parameters By:"

46 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name"

47 exit 1

48 fi

49 if [ ! -f "test_elevator/project.cache" ]; then

50 echo "Parameters Java_Package_Name Unset!"

51 echo "Try Setting Parameters By:"

52 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name"

53 exit 1

54 fi

55 echo "Starting Elevator Autotest..."

56 cd test_elevator57 num=`catnum.cache`58 request=`catrequest.cache`59 interval=`catinterval.cache`60 project=`catproject.cache`61 package=`catpackage.cache`62 echo -e "Current Parameters:\n\tRounds :\t${num}\n\tRequests :\t${request}\n\tInterval :\t${interval}s\n\tMain : \t\"${project}\"\n\tPackage :\t${package}"

63 ./test.sh

64 fi

查看 start.sh 代码

./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name

随机请求的产生程序 gen.py 的实现基于 Python,最重要的是在输出之后一定要清空缓冲区才可以正确的按时间输出给电梯进程:

1 importrandom2 importtime, sys3 definput_generator(testNum):4 tests =[]5 realNum = random.randint(int(testNum*0.8), testNum)6 for i inrange(realNum):7 fromFloor = random.randint(-3, 20)8 while fromFloor ==0:9 fromFloor = random.randint(-3, 20)10 toFloor = random.randint(-3, 20)11 while toFloor == 0 or fromFloor ==toFloor:12 toFloor = random.randint(-3, 20)13 if fromFloor !=toFloor:14 inputString = str(i+1) + '-FROM-' + str(fromFloor) + '-TO-' +str(toFloor);15 tests.append(inputString)16 returntests17

18 with open("interval.cache","r") as file:19 interval = int(file.readline().strip('\n'))20

21 with open("request.cache","r") as file:22 total = int(file.readline().strip('\n'))23

24 tests =input_generator(total)25 for each intests:26 time.sleep(random.randint(0, 1000 * interval)/1000)27 print(each)28 sys.stdout.flush()

查看请求生成程序

该程序作为一个 Python 子程序在核心交互脚本中调用。这个脚本负责 Java 类的运行,输入输出记录和结果的返回。设计这个程序最复杂的一点就是如何在等待进程结束的过程中判断进程是否超过200秒的运行时间限制。经过查阅资料发现可以使用 os.WNOHANG 参数实现非阻塞的 wait 等待,加上轮询即可实现超时终止服务。具体代码如下:

1 from subprocess importPopen, PIPE2 importos3 importsignal4 importtime5 importre6

7 talkpipe = Popen(['python', 'gen.py'],8 shell=False, stdout=PIPE)9 with open("project.cache","r") as file:10 project = str(file.readline().strip('\n'))11 with open("package.cache","r") as file:12 package = str(file.readline().strip('\n'))13 with open("run.res","wb") as out, open("run.err","wb") as err: #, open('comp.res',"wb") as comp, open('comp.err',"wb") as comperr:

14 elevator_fast = Popen(['java', '-classpath', project + ':elevator-input-hw3-1.4-jar-with-dependencies.jar:timable-output-1.0-raw-jar-with- dependencies.jar', package], stdin=PIPE, stdout=out, stderr=err, shell=False)15 start =time.time()16 try:17 whileTrue:18 line =talkpipe.stdout.readline()19 ifline:20 elevator_fast.stdin.write(line)21 elevator_fast.stdin.flush()22 with open("run.check","ab+") as check:23 check.write(str.encode("[{:.1f}]".format(time.time() -start)))24 check.write(line)25 else:26 elevator_fast.stdin.close()27 break

28 with open("run.tst","ab+") as test:29 test.write(line)30 exceptKeyboardInterrupt:31 print("[!] ERROR:\t Terminating...")32 os.kill(talkpipe.pid, signal.SIGTERM)33

34 try:35 timeout = 200

36 t_beginning =time.time()37 seconds_passed =038 whileTrue:39 ef = os.wait4(elevator_fast.pid, os.WNOHANG)[2]40 if elevator_fast.poll() is notNone:41 break

42 seconds_passed = time.time() -t_beginning43 if timeout and seconds_passed >timeout:44 elevator_fast.terminate()45 print("[!] ERROR:\t Elevator Running Timeout!")46 raise TimeoutError("Elapsed For" + str(timeout) + "Seconds")47 time.sleep(0.1)48 elapsed = time.time() -start49 exceptChildProcessError:50 print("[!] WARNING:\t Real Time Limit Exceeded!")51 os._exit(0)52 exceptKeyboardInterrupt:53 print("[!] ERROR:\t Terminating...")54 os.kill(elevator_fast.pid, signal.SIGTERM)55 if elevator_fast.poll() !=0:56 print("[!] ERROR:\t Error Status On Exit Fast Elevator!")57 with open("run.res","r") as file:58 lines =file.readlines()59 time_fast = float(re.search(r"\d+\.?\d*", lines[-1]).group())60

61 time_max = 200

62 time_bse = 10

63

64 print("[i] Baseline Refer:\t Base :{0:>7.3f} | Upper:{1:>7.3f}".format(time_bse, time_max))65 print("[i] Fast Scheduler:\t Total:{0:>7.3f} | CPU :{1:>7.3f} | Kernel:{2:>7.3f}".format(elapsed, ef.ru_utime, ef.ru_stime))66 print("[-] Time Ratio:\t {0:>7.3f}".format((time_max)/(time_fast)))67

68 with open("summary.log","a+") as log:69 log.write(str(time_fast) + "\n")70 if (time_fast / time_max) > 1 or ef.ru_utime+ef.ru_stime >time_bse:71 with open("run.check","r") as test:72 print("[-] Bad Results:")73 for line intest.readlines():74 print(line.strip('\n'))

查看核心交互代码

在获取到程序输出后,还需要交还运行脚本来比对结果并在终端给予反馈:

1 #!/bin/bash2 num=`catnum.cache`3 for ((i=1;i<=num;i++))4 do

5 # current=`date +%d%H%M%S`6 test_file="run.tst"

7 result_file="run.res"

8 error_file="run.err"

9 catch1=$(rm run.*)10 # catch2=$(rm comp.*)11 python comm.py12 cat $test_file >>run.txt13 echo "END" >>run.txt14 cat $result_file >>run.txt15 echo "END" >>run.txt16 java_start_test="java -jar elevator_tester.jar"

17 success=$(cat run.txt |$java_start_test)18 if [ "$success" = 'Success!'];19 then

20 echo -e "[*] SUCCESS:\t $i/$num"

21 else

22 echo -e "$success"

23 echo -e "[!] ERROR:\t Fast Scheduler Failure!"

24 break25 fi

26 done

这样就可以保证在程序运行出现问题时将后续的测试停止,保留错误的输入结果供检查。

完整的工程可以参考 Github 仓库  https://github.com/BXYMartin/Java-Elevator/tree/test_multi

B 度量评估

a 类图绘制

这一次还是着重分析最后一次作业,基于 UML 度量工具进行类图的绘制:

从类图可以看出,这一次作业的体量和代码规模相较上一次的多项式作业有了显著的提升,尤其是多个对象共同享有的 Scheduler 调度者以及在多个电梯之间协同的 Plan、Route 类路径规划都是需要非常精心的构造和设计。我这种设计的优点在于,共享对象少,实现逻辑简单,代码出错的概率较低。但是同时也带来的缺点就是封锁粒度太大,某些时候将不得不采用轮询的方式为空闲的电梯分配最佳的任务,算是这种设计的缺陷吧。

b 经典度量分析

接下来分析经典的 OO 度量,分析 CK 度量组,基于类设计的六种度量:

可以看出,各个类的内聚程度较高,对象间的耦合度较低。部分类由于功能极为有限,仅仅用于输入输出,因而类的响应值较低,类内部的有权方法也较低。对于路径规划和电梯运行的类,对象的响应值和耦合程度都相对比较高。

之后来分析类内部的复杂度:

其中 Path 和 SmartElevator 类的平均类间、类内复杂度都较高,对其中的方法着重分析:

上表中省略了值较低或辅助功能的函数,仅保留复杂度较高的方法。着重分析复杂度,电梯的运行函数因为没有拆分成几个独立的阶段,所以内部复杂度较高,而对于调度器的分配函数,也有较高的方法间复杂度。再就是图中的规划路径函数具有较高的循环复杂度,也在情理之中。

接下来对类与方法的代码规模进行统计:

可以看到对于核心的路径规划类,类代码规模和属性个数都比较多,对于其他功能简单的类而言则并不复杂。

将上述数字可视化可以得到更直观的结果:

对于电梯类其核心的 run 方法是代码量最大的,应该考虑将其划分为几个功能较为分散的小函数执行,提升扩展性。仅次于电梯运行函数的就是关于图的计算函数等,这些函数的复杂性因其功能的专一性而变得很高,个人感觉也较为合理。代码评价工具在分析函数名的过程中存在错误,已在 Github 提交 Pull Request 并在 master 的最新版本中修复。

c 线程协作图

绘制线程间的协作图:

可以看到,各个模块之间的协作逻辑较为简单,Passenger 负责接受由标准输入读入的数据,经过 Plan 模块和 Elevator 模块的处理后输出结果。

d 设计原则检查

基于 S.O.L.I.D.原则(SRP 单一责任原则、OCP 开放封闭原则、LSP 里氏替换原则、ISP 接口分离原则、DIP 依赖倒置原则)进行评估:

1)SRP 原则:每一个类都各司其职。在程序设计中,电梯只负责简单的运送,规划模块负责路线规划,调度部分负责任务调度,最大化的分割了任务,做到了SRP 原则。

2) OCP 原则:在这一次作业中,电梯模块从始至终都没有发生重构,可以说最大程度的满足了 OCP 原则。但是对于任务规划类而言,则不可避免的进行了多次重构,但是也通过模块化的手段尽可能简化了重构流程。

3) LSP 原则:在本次作业中不涉及继承

4) ISP 原则:在本次作业中不涉及接口

5) DIP 原则:在这次作业中我抽象出多个交互类用来将复杂的信息抽象出本质,在不同类之间传递。我抽象了包括电梯计划(Plan),路径规划(Route)以及请求(Request)三类信息传递类用来简化模块和模块之间的耦合。但是对于路径规划类和电梯类,我还是硬编码了电梯的楼层信息,因为电梯和规划之间的实时通信限制了我对他们的抽象,应该维护一个公共的状态类去实现。

IV 总结

这一次电梯作业是一次代码量突飞猛进的增长,多线程的不可复现、不可调试的特性也让我在编码的过程中多加谨慎,遇到问题首先从顶层结构入手思考,而不是盲目调试,大幅度的降低了在修复漏洞阶段的时间,也让我认识到了架构设计对后期减轻返工次数的必要性。对于各种工具的使用也更加得心应手,是一次对自己的历练。

java面向对象模拟电梯_面向对象的程序设计-电梯调度系统的设计、优化与测试...相关推荐

  1. 基于java的ssm和微信小程序实现物业缴费系统的设计与实现【附项目源码+论文说明】

    基于java的ssm和微信小程序实现物业缴费系统的设计与实现 摘要 本论文主要论述了如何使用JAVA语言开发一个基于微信小程序的物业缴费系统的设计与实现,本系统将严格按照软件开发流程进行各个阶段的工作 ...

  2. Day814.电商系统表设计优化案例分析 -Java 性能调优实战

    电商系统表设计优化案例分析 Hi,我是阿昌,今天学习记录的是关于电商系统表设计优化案例分析. 如果在业务架构设计初期,表结构没有设计好,那么后期随着业务以及数据量的增多,系统就很容易出现瓶颈. 如果表 ...

  3. 面向对象的程序设计-电梯调度系统的设计、优化与测试

    面向对象的程序设计(2019)第二单元总结 I  对问题的初体验 在开始OO之旅前,对OO电梯早有耳闻.这一次终于轮到我自己实现OO电梯了.首先从顶层需求出发对电梯系统进行分析,对象包括电梯.任务和乘 ...

  4. 基于java互助平台设计_学生学习交流互助社区系统的设计与实现(MySQL)

    学生学习交流互助社区系统的设计与实现(MySQL)(任务书,开题报告,中期检查表,文献综述,外文翻译,毕业论文15000字,程序代码,MySQL数据库) 学生互助社区(Help) 使用Java 编程语 ...

  5. 基于java的网上花店销售系统_基于web的花店销售系统的设计与实现

    基于web的花店销售系统的设计与实现(论文13000字) 摘要:本系统是一个相对简单的基本应用系统,主要满足传统的花店运营需要,将自己的销售渠道扩展到网上,通过简单的管理,提供给最终消费者产品的展现. ...

  6. java 美发管理系统_基于安卓Android潮流美发系统APP设计(MySQL)

    基于安卓Android潮流美发系统APP设计(MySQL)(任务书,开题报告,中期检查表,文献综述,外文翻译,毕业论文16000字,程序代码,MySQL数据库) 本应用主要用于对手机上网用户,在线预约 ...

  7. Java毕设项目-商城管理系统-基于J2EE/SSM化妆品商城系统的设计与实现

    题目:商城管理系统-基于J2EE/SSM化妆品商城系统的设计与实现 重点作为毕设项目 1.开发环境 语言:Java       Spring+Springmvc+Mybatis[简称SSM] 数据库: ...

  8. p2p mysql 数据的拆分 案例_浅析: P2P网贷系统数据库设计

    许多人对于P2P网贷系统的技术环境并不熟悉,导致不少运营商被开发商忽悠的情况屡见不鲜.今天我们就从数据库出发,给大家讲一讲有关P2P数据库技术的知识.首先,数据库架构的设计是开发一套P2P网贷系统的前 ...

  9. jsp mysql火车票预定系统_火车票网络订票售票系统的设计与实现(NetBeans,MySQL)

    火车票网络预订售票系统的设计与实现(NetBeans,MySQL)(任务书,开题报告,中期检查表,文献综述,毕业论文16000字,程序代码,MySQL数据库) 本文针对火车站的售票实际情况,按照软件工 ...

最新文章

  1. mysql 基本操作
  2. asp网络编程:Web程序中网页间数据传递方法小结
  3. virt_to_page
  4. Hibernate教程——我的笔记
  5. sap新手学习第一天
  6. messagedigest 图片加密_MessageDigest的功能及用法(加密解密)
  7. Java 二叉树完整代码(递归迭代)
  8. 系统对接方案_劳务实名制管理系统解决用工问题
  9. Win7/Win8如何配置jdk环境变量(配置java环境变量)
  10. OpenCV-Python教程:图像金字塔
  11. PHP5.5 ~ PHP7.2新特性总结
  12. java充值卡号生成_java实现点卡生成
  13. 面试必备:Zack 大佬带您深入Spring MVC DispatchServlet 源码分析
  14. SyncToy 2.1
  15. HEU euler path
  16. 微信小程序怎么开通(自己申请开通微信小程序的方法)
  17. JS判断ios系统的版本号
  18. 计算机老师三年发展规划,信息技术专业教师个人发展三年规划汇总(全).doc
  19. 当编辑内容改变是后 选中内容 Range 的startOffset会被重置
  20. 【python】使用pip安装指定版本的模块,卸载、查看、更新包

热门文章

  1. SOD算法:PoolNet
  2. 对redis的keys方法替换
  3. LeetCode - 1705 吃苹果的最大数目
  4. 24个基本指标精粹讲解(18)--PSY
  5. 线上教育核心竞争力是什么?声网发布在线素质、职业教育解决方案
  6. 教你破解隔壁妹子wifi密码,成功率高达90%
  7. Autodesk 开源 3D 打印机
  8. 学历代表过去,只有学习力才能代表将来,尊重经验的人,才能少走弯路
  9. 关于HAL中的__weak详解
  10. 麦肯锡|稻盛和夫|麦肯锡方法与稻盛经营哲学