城堡游戏(代码优化)
城堡中有多个房间,用户通过输入north, south, east, west等来确认去哪一个房间(此时窗口会有提示转到哪个房间),如果此时多出一个房间,需要使用up, down才能到达,修改代码则需要代码具有可扩展性,对原来的代码进行优化来实现这个功能。
优化前代码结构:
优化前.png
优化路程
将类中相同的代码抽离,转为一个公用的方法
如图将注释的代码抽离成一个方法,供调用:
private void nowRoom(){
System.out.println("现在你在" + currentRoom);
System.out.print("出口有:");
// 将出口信息封装在Room类中,降低程序之间的耦合
// if(currentRoom.northExit != null)
// System.out.print("north ") ;
// if(currentRoom.eastExit != null)
// System.out.print("east ");
// if(currentRoom.southExit != null)
// System.out.print("south ");
// if(currentRoom.westExit != null)
// System.out.print("west ");
System.out.println(currentRoom.getExitDesc());
System.out.println();
}
将 Room 类中的变量转为私有变量
Room 类和 Game 类有大量的代码出口相关,Game 类中大量使用了 Room 类中的成员变量;利用封装来降低类和类之间的耦合;便于代码后续的维护。
在将 Room 类的变量转为私有后,发现 Game 类中有几处报错;说明 Game 类中调用到了 Room 类的成员变量;
第一处是上一步中提取的方法,阅读代码功能;是显示当前 Room 的出口信息。
优化思想:调用者不做被调用类的逻辑处理
出口信息能否只在 Room 中做逻辑处理,Game 类中只负责调用就好。
做法:
在 Room 类中增加 getExitDesc 方法。
public String getExitDesc(){
// 使用 StringBuffer 是因为 String 对象是管理者,每次对 String 对象的修改都是新增一个 String 对象,
// 对系统开销比较大,但是 StringBuffer支持修改
StringBuffer sb = new StringBuffer();
if (northExit != null)
sb.append("north ");
if (eastExit != null)
sb.append("east ");
if (westExit != null)
sb.append("west ");
if (southExit != null)
sb.append("south ");
return sb.toString();
}
这样在 Game类中的 nowRoom 方法中,只需要调用 getExitDesc 方法就能返回出口信息。
第二处错误是在 Game 类中, goRoom 方法中,显示的是玩家进入下一个房间后,显示出口信息。
同样的,想办法让 Room 类中就处理其中的逻辑,直接返回给 Game 类下一个房间的出口信息。
在 Room 类中新增一个 getExit 方法,直接将 Game 类中的逻辑处理复制过来用。
public Room getExit(String direction){
Room ret = null;
if(direction.equals("north")) {
ret = northExit;
}
if(direction.equals("east")) {
ret = eastExit;
}
if(direction.equals("south")) {
ret = southExit;
}
if(direction.equals("west")) {
ret = westExit;
}
return ret;
}
减少代码中的硬编码
优化思想:使用容器来代替硬编码
Room 类有四个成员变量表示四个方向。使用 HashMap 容器来装载方向,这样方便以后新增方向变量,提高代码灵活性。
做法:
去掉原有变量,新增一个 HashMap 容器变量。
private HashMap<String,Room> exit = new HashMap<String,Room>();
同步修改类方法中的 setExits 方法。因为使用了容器的原因,所以只能为 Room 对象一个个出口添加。
// 录入房间空间位置的方式改变,由原来的对一个房间的四个方向分别定义,改为对一个房间
// 自定义方向以及该方向上的新房间
public void setExit(String dir, Room room){
exit.put(dir,room);
}
发现 Room类 中的 getExit 方法出现报错,可以通过容器直接返回房间的方向信息:
public Room getExit(String direction){
// Room ret = null;
// if(direction.equals("north")) {
// ret = northExit;
// }
// if(direction.equals("east")) {
// ret = eastExit;
// }
// if(direction.equals("south")) {
// ret = southExit;
// }
// if(direction.equals("west")) {
// ret = westExit;
// }
return exits.get(direction);
}
getExitDesc 方法也要同步做修改,var.keyset() 方法获取 map 的 key 的集合。
public String getExitDesc(){
// 使用 StringBuffer 是因为 String 对象是管理者,每次对 String 对象的修改都是新增一个 String 对象,
// 对系统开销比较大,但是 StringBuffer支持修改
StringBuffer sb = new StringBuffer();
for (String dir :
exits.keySet()) {
sb.append(dir+" ");
}
// if (northExit != null)
// sb.append("north ");
// if (eastExit != null)
// sb.append("east ");
// if (westExit != null)
// sb.append("west ");
// if (southExit != null)
// sb.append("south ");
return sb.toString();
}
因为修改了 Room 类中的房间出口信息设置方法,Game 类中的 creatRooms 方法报错。
private void createRooms()
{
Room outside, lobby, pub, study, bedroom;
// 制造房间
outside = new Room("城堡外");
lobby = new Room("大堂");
pub = new Room("小酒吧");
study = new Room("书房");
bedroom = new Room("卧室");
// 初始化房间的出口
// outside.setExits(null, lobby, study, pub);
// lobby.setExits(null, null, null, outside);
// pub.setExits(null, outside, null, null);
// study.setExits(outside, bedroom, null, null);
// bedroom.setExits(null, null, null, study);
outside.setExit("eastExit",lobby);
outside.setExit("southExit",study);
outside.setExit("westExit",pub);
lobby.setExit("westExit",outside);
pub.setExit("eastExit",outside);
study.setExit("northExit",outside);
study.setExit("eastExit",bedroom);
bedroom.setExit("westExit",study);
currentRoom = outside; // 从城堡门外开始
}
到这里,如果想要增加一个新的出口信息,只要在 Game 类中初始化一个新的房间,增加新的出口信息类型就可以了;
以框架+数据来提高可扩展性
优化思想:将硬编码改成 框架 + 数据
城堡游戏中 main 函数中的 go,help,bye 三个命令解析,依旧是硬编码的;优化成框架和数据的形式。
优化做法:
- 命令的解析脱离 if-else
- 定义一个 Handle 来处理命令
- 用 Hash 表来保存命令和 Handler 之间的关系
字符串对应关系用 HashMap[为什么字符串对应一个东西通常都是用 HashMap?]
定义一个 HashMap,key 和 value 都必须是对象,城堡游戏中有的是,String 和调用一个函数。因此需要创建一个中间类 Handle,通过 handle 类的对象调用对应的函数。
如下创建四个类。
//父类 Handler
public class Handler {
public void doCmd(String word){
}
public boolean isBye() {
return false;
}
}
// HandlerGo
public class HandlerGo extends Handler {
private Game game;
public HandlerGo(Game game){
this.game = game;
}
@Override
public void doCmd(String word) {
game.goRoom(word);
}
}
// HandlerHelp
public class HandlerHelp extends Handler {
@Override
public void doCmd(String word) {
System.out.println("迷路了吗?你可以做的命令有:go bye help");
System.out.println("如:\tgo east");
}
}
// handlerBye
public class HandlerBye extends Handler{
@Override
public boolean isBye() {
return true;
}
}
在 Gmae 类初始化 Handle 类的 HashMap 对象,并在构造器中构建 不同功能的 Handle 对象放入 map 中;
public class Game {
private Room currentRoom;
/*用Hash表保存命令与Handler之间的关系*/
private HashMap<String, Handler> handlers = new HashMap<String, Handler>();
public Game()
{
handlers.put("go", new HandlerGo(this));
handlers.put("bye", new HandlerBye());
handlers.put("help",new HandlerHelp());
createRooms();
}
...
将原来 main 函数中的循环体,提取成一个新的方法:
public void play(){
Scanner in = new Scanner(System.in);
while ( true ) {
String line = in.nextLine();
String[] words = line.split(" ");
// 利用Hash表<K, V>的特性,如果用户输入"bye",通过handler.get()得出handler
// 类型下面这句就相当于:Handler handler = new HandlerBye();
Handler handler = handlers.get(words[0]);
String value = " ";
if (words.length > 1){
value = words[1];
}
if (handler != null){
// 此时handler为HandlerBye型,没有value值
handler.doCmd(value);
// HandlerBye继承了Handler中的isBye()方法并将其覆盖,此时handler.isBye()返回true
if (handler.isBye()){
break;
}
}
// if ( words[0].equals("help") ) {
// game.printHelp();
// } else if (words[0].equals("go") ) {
// game.goRoom(words[1]);
// } else if ( words[0].equals("bye") ) {
// break;
// }
}
in.close();
}
优化完成,代码运行正常;优化完成后,当需要新增动作命令时,只需要新增一个 handler类型,并在构造器里新增新类型的 map 键值对。而不用对 play 函数做改动。这就是通过 框架 + 数据 实现的可扩展性。
博客地址:http://www.wengfe.win/2018/12/14/2018-12-14/#more
代码地址:https://github.com/wengfe/JAVA/tree/master/castle