5.1:城堡游戏

导入代码,运行game主函数,即可开始游戏。

Game.java

  1. package castle;
  2. import java.util.Scanner;
  3. public class Game {
  4. private Room currentRoom;
  5. public Game()
  6. {
  7. createRooms();
  8. }//构造函数
  9. private void createRooms()
  10. {
  11. Room outside, lobby, pub, study, bedroom;
  12. // 制造房间
  13. outside = new Room("城堡外");
  14. lobby = new Room("大堂");
  15. pub = new Room("小酒吧");
  16. study = new Room("书房");
  17. bedroom = new Room("卧室");
  18. // 初始化房间的出口
  19. outside.setExits(null, lobby, study, pub);
  20. lobby.setExits(null, null, null, outside);
  21. pub.setExits(null, outside, null, null);
  22. study.setExits(outside, bedroom, null, null);
  23. bedroom.setExits(null, null, null, study);
  24. currentRoom = outside; // 从城堡门外开始
  25. }//创建房间函数
  26. private void printWelcome() {
  27. System.out.println();
  28. System.out.println("欢迎来到城堡!");
  29. System.out.println("这是一个超级无聊的游戏。");
  30. System.out.println("如果需要帮助,请输入 'help' 。");
  31. System.out.println();
  32. System.out.println("现在你在" + currentRoom);
  33. System.out.print("出口有:");
  34. if(currentRoom.northExit != null)
  35. System.out.print("north ");
  36. if(currentRoom.eastExit != null)
  37. System.out.print("east ");
  38. if(currentRoom.southExit != null)
  39. System.out.print("south ");
  40. if(currentRoom.westExit != null)
  41. System.out.print("west ");
  42. System.out.println();
  43. }//欢迎函数
  44. // 以下为用户命令
  45. private void printHelp()
  46. {
  47. System.out.print("迷路了吗?你可以做的命令有:go bye help");
  48. System.out.println("如:\tgo east");
  49. }//帮助函数
  50. private void goRoom(String direction)
  51. {
  52. Room nextRoom = null;
  53. if(direction.equals("north")) {
  54. nextRoom = currentRoom.northExit;
  55. }
  56. if(direction.equals("east")) {
  57. nextRoom = currentRoom.eastExit;
  58. }
  59. if(direction.equals("south")) {
  60. nextRoom = currentRoom.southExit;
  61. }
  62. if(direction.equals("west")) {
  63. nextRoom = currentRoom.westExit;
  64. }
  65. if (nextRoom == null) {
  66. System.out.println("那里没有门!");
  67. }
  68. else {
  69. currentRoom = nextRoom;
  70. System.out.println("你在" + currentRoom);
  71. System.out.print("出口有: ");
  72. if(currentRoom.northExit != null)
  73. System.out.print("north ");
  74. if(currentRoom.eastExit != null)
  75. System.out.print("east ");
  76. if(currentRoom.southExit != null)
  77. System.out.print("south ");
  78. if(currentRoom.westExit != null)
  79. System.out.print("west ");
  80. System.out.println();
  81. }
  82. }//去某个房间,并提示出口
  83. public static void main(String[] args) {
  84. Scanner in = new Scanner(System.in);
  85. Game game = new Game();
  86. game.printWelcome();
  87. while ( true ) {
  88. String line = in.nextLine();
  89. String[] words = line.split(" ");//命令区分
  90. if ( words[0].equals("help") ) {
  91. game.printHelp();
  92. } else if (words[0].equals("go") ) {
  93. game.goRoom(words[1]);
  94. } else if ( words[0].equals("bye") ) {
  95. break;
  96. }
  97. }//循环测试
  98. System.out.println("感谢您的光临。再见!");
  99. in.close();
  100. }
  101. }

Room.java

  1. package castle;
  2. public class Room {
  3. public String description;
  4. public Room northExit;
  5. public Room southExit;
  6. public Room eastExit;
  7. public Room westExit;
  8. //定义房间描述与四个出口
  9. public Room(String description)
  10. {
  11. this.description = description;
  12. }//构造函数,接收字符串作为此对象的描述
  13. public void setExits(Room north, Room east, Room south, Room west)
  14. {
  15. if(north != null)
  16. northExit = north;
  17. if(east != null)
  18. eastExit = east;
  19. if(south != null)
  20. southExit = south;
  21. if(west != null)
  22. westExit = west;
  23. }设置出口,接收参数为Room对象
  24. @Override
  25. public String toString()
  26. {
  27. return description;
  28. }//覆盖toString函数,返回此对象的描述
  29. }

分析

  1. 这个应用程序的任务是什么?

构建几个房间及其相应的关系,接收用户输入的命令,调用相应的函数,控制相关的数据

  1. 这个应用程序接受什么样的命令?

go (a exit) , bye, help .

  1. 每个命令做什么?

修改当前房间的位置,并提示相应的出口;离开程序;显示帮助信息

思考:这个程序有什么问题?

  1. 代码复制严重
  2. 可维护性低,可拓展性低

5.2:消除代码复制

程序中存在相似甚至相同的代码块,是非常低级的代码质量问题。 代码复制存在的问题是,如果需要修改一个副本, 那么就必须同时修改所有其他的副本,否则就存在不一致的问题。这增加了维护程序员的工作量,而且存在造成错误的潜在危险。很可能发生的一种情况是,维护程序员看到一个副本被修改好了,就以为所有要修改的地方都已经改好了。因为没有任何明显迹象可以表明另外还有一份一样的副本代码存在,所以很可能会遗漏还没被修改的地方。 我们从消除代码复制开始。消除代码复制的两个基本手段,就是函数和父类


  • 欢迎函数代码片段

image.png

  • goRoom函数代码片段

image.png
很明显,整合成一个函数后,就可以消除这样的代码复制问题,需要的时候直接调用此函数即可

showPrompt():玩家提示

  1. public void showPrompt(){
  2. System.out.println("你在" + currentRoom);
  3. System.out.print("出口有: ");
  4. if(currentRoom.northExit != null)
  5. System.out.print("north ");
  6. if(currentRoom.eastExit != null)
  7. System.out.print("east ");
  8. if(currentRoom.southExit != null)
  9. System.out.print("south ");
  10. if(currentRoom.westExit != null)
  11. System.out.print("west ");
  12. System.out.println();
  13. }

5.3:封装

聚合与耦合

对于评价类的设计,有两个核心术语:耦合聚合

耦合:指的是类和类之间的联系,耦合度反映了这些类联系的紧密度。
在一个紧耦合的结构中,对一个类的修改也会导致对其他一些类的修改,为了减少类似的情况,我们需要努力获得低的耦合度,也称松耦合,在一个松耦合的系统中,常常可以修改一个类,但同时不会修改其他类,而且 整个程序还可以正常运作。

聚合:与程序中一个单独的单元(函数)所承担任务的数量和种类有关。
理想情况下,一个代码单元应该负责一个聚合的任务,一个方法应该实现一个逻辑操作,而一个类应该代表一定类型的实体。

聚合理论背后的要点是重用:如果一个方法或类是只负责一件定义明确的事情,那么就很有可能在另外不同的上下文环境中使用。

封装:降低耦合,增加可扩展性

可以运行的代码!=良好的代码,对代码做维护的时候最能看出代码的质量 如果想要增加一个方向,如down或up,如何做? 方向作为Room类的成员变量,game类却大量直接使用;新增一个方向,所有判断方向,使用方向的函数都要修改,game类与Room类都要大幅度修改。可以看出game类与Room类的耦合比较紧。

Game类为什么要拿到Room的(public 变量)成员变量(方向出口)?

  1. 显示当前房间出口
  2. 获取当前房间出口

    为什么不可以是Room自己做相应的操作呢?

  3. 将变量关键字改为private,私有化变量

  4. 在Room中定义函数,显示当前房间出口,返回当前房间出口

showOut ():显示出口

  1. //显示当前房间出口
  2. public String showOut(){
  3. StringBuffer ret= new StringBuffer();//可以原地修改字符串的对象
  4. if ( northExit != null )
  5. sb.append("north") ;
  6. if ( eastExit != null )
  7. sb.append("east");
  8. if ( westExit != null )
  9. sb.append("west" ) ;
  10. if ( southExit != null )
  11. sb.append("south");
  12. return sb. toString();
  13. }

getExit():获取出口

  1. //获取当前房间出口
  2. public Room getExit(String direction){
  3. Room nextRoom = null;
  4. if(direction.equals("north")) {
  5. nextRoom = northExit;
  6. }
  7. //如果方向和(东西南北)某一个相对应,则将currentroom的某方向出口对象赋值
  8. if(direction.equals("east")) {
  9. nextRoom = eastExit;
  10. }
  11. if(direction.equals("south")) {
  12. nextRoom = southExit;
  13. }
  14. if(direction.equals("west")) {
  15. nextRoom = westExit;
  16. }
  17. }

修改Game中的goRoom()

  1. private void goRoom(String direction)
  2. {
  3. Room nextRoom = currentRoom.getExit(direction);
  4. if (nextRoom == null) {
  5. System.out.println("那里没有门!");
  6. }
  7. else {
  8. currentRoom = nextRoom;
  9. showPrompt();
  10. }
  11. }//去某个房间,并提示出口

修改Game中的showPrompt()

  1. public void showPrompt(){
  2. System.out.println("你在" + currentRoom);
  3. System.out.print("出口有:");
  4. System.out.println(currentRoom.showOut());
  5. }

小节

到此,我们已经把Room的数据与操作封装起来了;所有的数据都由Room自己决定自己操作,game只负责接收数据,game需要什么,就由Room类将处理好的数据返回给Game。


5.4:可拓展性

我们已经实现的设计与其原始版本比较已经有了很大的改进,然而还可以有进一步的提高。 优秀的软件设计者的一个素质就是有预见性。什么是可能会改变的?什么是可以假设在软件的生命期内不会改变的? 可扩展性:代码的某些部分不需要经过修改就能适应将来可能的变化。

这个游戏会是一个基于字符界面的游戏,通过终端进行输入输出,会永远是这样子吗? 以后如果给这个游戏加上图形用户界面,加上菜单、按钮和图像,也是很有意思的一种扩展所以,我们需要提高其可拓展性

使用容器提高灵活性

我们使用容器来实现Room的成员变量(出口),随意增加出口,减少了硬编码,提高其灵活性。

private HashMap<String,Room> exits = new HashMap<String, Room>();

出口集合 = <方向 (String对象),房间(Room对象)>

setExit ():设置出口

  1. public void setExit(String direction,Room room){
  2. exits.put(direction,room);
  3. }

showOut():显示出口

修改原来的显示出口函数,依次取出容器里的数据,结束循环后返回数据

  1. public String showOut( ){
  2. StringBuffer ret= new StringBuffer();
  3. for(String items:exits.keySet()){
  4. ret.append(items+" ");
  5. }
  6. return ret.toString();
  7. }

getExit():获取出口

直接返回相应方向对应的Room对象

  1. public Room getExit(String direction){
  2. return exits.get(direction);
  3. }

Game中修改房间初始化,改为一条一条地设置方向与对应房间

  1. // 初始化房间的出口
  2. outside.setExit( "east", lobby);
  3. outside.setExit("south" , study);
  4. outside.setExit("west",pub);
  5. lobby.setExit("up", pub);
  6. lobby.setExit("west",outside);
  7. pub.setExit("east",outside);
  8. pub.setExit("down", lobby );
  9. study.setExit("north",outside);
  10. study.setExit("east", bedroom);
  11. bedroom.setExit("west", study);

小节

到此,程序的代码看起来优雅简洁了许多。原本是硬编码的成员变量(出口),使用容器来存储,某房间对象的出口已经非常灵活,想加入updown出口是非常容易的事情了,可拓展性提高了许多。


5.5:数据与框架

从程序中硬编码的东西识别出框架和数据,以代码实现框架,将部分功能以数据的方式加载,这样能在很大程度上实现可扩展性。

Game类还存在什么问题?

Game类的主函数中,我们发现这样几行代码;其作用是输入命令后,执行相应的函数操作;这是一个很明显的硬编码的东西,命令就只能有三个;我们能否由此实现一个框架,以便后续新增命令呢?
image.png

通过Hashmap实现框架

当玩家输入某个字符串命令的时候,我们就通过Hashmap的get函数,将那个函数获取出来并执行,但是Hashmap里面只能存放对象,我们只能把函数放置在一个叫Handler的对象里面

  1. 创建Hashmap容器,实现命令字符串与函数相对应。
  2. 将Handler设为父类,使用子类继承父类函数,一个命令对应一个子类,便于管理。
    1. private HashMap<String,Handler> handlers = new HashMap<String,Handler>();
    2. //新建一个命令哈希表,命令-函数

实现Handler父类

  1. 构造函数 public Handler(Game game)(接收Game对象):某些函数只能在Game类中被调用,Handler没有此类函数处理命令,所以需要传递当前引用Handler的Game对象
  2. protected Game game;保证子类也可以使用Game对象
  3. public void docmd(String word);接收命令,做相应操作
  4. Boolean isBye();比较特殊的结束命令,不能放在docmd()中
    1. public class Handler {
    2. protected Game game;
    3. public Handler(Game game){
    4. this.game = game;
    5. }
    6. public void docmd(String word){
    7. }
    8. public Boolean isBye(){
    9. return false;
    10. }
    11. }

实现Handler子类

HandlerHelp.java

  1. public class HandlerHelp extends Handler{
  2. public HandlerHelp(Game game) {
  3. super(game);
  4. }
  5. @Override
  6. public void docmd(String help){
  7. System.out.print("迷路了吗?你可以做的命令有:go bye help\t");
  8. System.out.println("如:go east");
  9. }
  10. }

HandlerGo.java

  1. public class HandlerGo extends Handler{
  2. public HandlerGo(Game game) {
  3. super(game);
  4. }
  5. @Override
  6. public void docmd(String direction) {
  7. game.goRoom(direction);
  8. }
  9. }

HandlerBye.java

  1. public class HandlerBye extends Handler{
  2. public HandlerBye(Game game) {
  3. super(game);
  4. }
  5. public Boolean isBye( ){
  6. return true;
  7. }
  8. }

将Handler对象放入Hashmap容器中

  1. handlers.put("bye",new HandlerBye(this));
  2. handlers.put("help",new HandlerHelp(this));
  3. handlers.put("go",new HandlerGo(this));

修改Game类硬编码

将原主函数测试代码修改为Game类的play()函数 思路:

  1. while(true)循环实现可重复输入
  2. 将输入的命令使用split函数拆解,存入字符串数组words
  3. 声明变量order为Handler类型,可以管理Handler对象及其子类对象
  4. 使用get函数在Hashmap容器Handler,寻找到与命令相对应的那个子类对象,并赋值给order
    1. 判断word长度,go命令长度2,bye与help为1,如果为2,那么将words[1]交给value
    2. 如果order里面存在Handler子类对象,执行子类对象的docmd函数,并将value(初始值为空)传递给此函数。如果isBye返回的结果是true,结束游戏。
  1. public void play(){
  2. Scanner in = new Scanner(System.in);
  3. printWelcome();
  4. while ( true ) {
  5. String line = in.nextLine();
  6. String[] words = line.split(" ");
  7. Handler order = handlers.get(words[0]);
  8. String value="";
  9. if(words.length >1)
  10. value = words[1];
  11. if(order!=null){
  12. order.docmd(value);
  13. if(order.isBye()){
  14. break;
  15. }
  16. }
  17. else {
  18. System.out.println("输错啦,请重试一遍命令!\n 需要帮助?试试输入help");
  19. continue;
  20. }
  21. }
  22. System.out.println("感谢您的光临。再见!");
  23. in.close();
  24. }

小节

到此,我们新增一个命令就简单了许多

  1. handlers.put("命令",new Handler命令(this));

命令集合里添加一个命令与一个相对应的Handler子类对象

  1. 新增一个Handler的子类对象,编写想要实现的功能