5.1:城堡游戏
导入代码,运行game主函数,即可开始游戏。
Game.java
package castle;
import java.util.Scanner;
public class Game {
private Room currentRoom;
public Game()
{
createRooms();
}//构造函数
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);
currentRoom = outside; // 从城堡门外开始
}//创建房间函数
private void printWelcome() {
System.out.println();
System.out.println("欢迎来到城堡!");
System.out.println("这是一个超级无聊的游戏。");
System.out.println("如果需要帮助,请输入 'help' 。");
System.out.println();
System.out.println("现在你在" + currentRoom);
System.out.print("出口有:");
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();
}//欢迎函数
// 以下为用户命令
private void printHelp()
{
System.out.print("迷路了吗?你可以做的命令有:go bye help");
System.out.println("如:\tgo east");
}//帮助函数
private void goRoom(String direction)
{
Room nextRoom = null;
if(direction.equals("north")) {
nextRoom = currentRoom.northExit;
}
if(direction.equals("east")) {
nextRoom = currentRoom.eastExit;
}
if(direction.equals("south")) {
nextRoom = currentRoom.southExit;
}
if(direction.equals("west")) {
nextRoom = currentRoom.westExit;
}
if (nextRoom == null) {
System.out.println("那里没有门!");
}
else {
currentRoom = nextRoom;
System.out.println("你在" + currentRoom);
System.out.print("出口有: ");
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();
}
}//去某个房间,并提示出口
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
Game game = new Game();
game.printWelcome();
while ( true ) {
String line = in.nextLine();
String[] words = line.split(" ");//命令区分
if ( words[0].equals("help") ) {
game.printHelp();
} else if (words[0].equals("go") ) {
game.goRoom(words[1]);
} else if ( words[0].equals("bye") ) {
break;
}
}//循环测试
System.out.println("感谢您的光临。再见!");
in.close();
}
}
Room.java
package castle;
public class Room {
public String description;
public Room northExit;
public Room southExit;
public Room eastExit;
public Room westExit;
//定义房间描述与四个出口
public Room(String description)
{
this.description = description;
}//构造函数,接收字符串作为此对象的描述
public void setExits(Room north, Room east, Room south, Room west)
{
if(north != null)
northExit = north;
if(east != null)
eastExit = east;
if(south != null)
southExit = south;
if(west != null)
westExit = west;
}设置出口,接收参数为Room对象
@Override
public String toString()
{
return description;
}//覆盖toString函数,返回此对象的描述
}
分析
- 这个应用程序的任务是什么?
构建几个房间及其相应的关系,接收用户输入的命令,调用相应的函数,控制相关的数据
- 这个应用程序接受什么样的命令?
go (a exit) , bye, help .
- 每个命令做什么?
修改当前房间的位置,并提示相应的出口;离开程序;显示帮助信息
思考:这个程序有什么问题?
- 代码复制严重
- 可维护性低,可拓展性低
5.2:消除代码复制
程序中存在相似甚至相同的代码块,是非常低级的代码质量问题。 代码复制存在的问题是,如果需要修改一个副本, 那么就必须同时修改所有其他的副本,否则就存在不一致的问题。这增加了维护程序员的工作量,而且存在造成错误的潜在危险。很可能发生的一种情况是,维护程序员看到一个副本被修改好了,就以为所有要修改的地方都已经改好了。因为没有任何明显迹象可以表明另外还有一份一样的副本代码存在,所以很可能会遗漏还没被修改的地方。 我们从消除代码复制开始。消除代码复制的两个基本手段,就是函数和父类
- 欢迎函数代码片段
- goRoom函数代码片段
很明显,整合成一个函数后,就可以消除这样的代码复制问题,需要的时候直接调用此函数即可
showPrompt():玩家提示
public void showPrompt(){
System.out.println("你在" + currentRoom);
System.out.print("出口有: ");
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();
}
5.3:封装
聚合与耦合
对于评价类的设计,有两个核心术语:耦合和聚合。
耦合:指的是类和类之间的联系,耦合度反映了这些类联系的紧密度。
在一个紧耦合的结构中,对一个类的修改也会导致对其他一些类的修改,为了减少类似的情况,我们需要努力获得低的耦合度,也称松耦合,在一个松耦合的系统中,常常可以修改一个类,但同时不会修改其他类,而且 整个程序还可以正常运作。
聚合:与程序中一个单独的单元(函数)所承担任务的数量和种类有关。
理想情况下,一个代码单元应该负责一个聚合的任务,一个方法应该实现一个逻辑操作,而一个类应该代表一定类型的实体。
聚合理论背后的要点是重用:如果一个方法或类是只负责一件定义明确的事情,那么就很有可能在另外不同的上下文环境中使用。
封装:降低耦合,增加可扩展性
可以运行的代码!=良好的代码,对代码做维护的时候最能看出代码的质量 如果想要增加一个方向,如down或up,如何做? 方向作为Room类的成员变量,game类却大量直接使用;新增一个方向,所有判断方向,使用方向的函数都要修改,game类与Room类都要大幅度修改。可以看出game类与Room类的耦合比较紧。
Game类为什么要拿到Room的(public 变量)成员变量(方向出口)?
showOut ():显示出口
//显示当前房间出口
public String showOut(){
StringBuffer ret= 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();
}
getExit():获取出口
//获取当前房间出口
public Room getExit(String direction){
Room nextRoom = null;
if(direction.equals("north")) {
nextRoom = northExit;
}
//如果方向和(东西南北)某一个相对应,则将currentroom的某方向出口对象赋值
if(direction.equals("east")) {
nextRoom = eastExit;
}
if(direction.equals("south")) {
nextRoom = southExit;
}
if(direction.equals("west")) {
nextRoom = westExit;
}
}
修改Game中的goRoom()
private void goRoom(String direction)
{
Room nextRoom = currentRoom.getExit(direction);
if (nextRoom == null) {
System.out.println("那里没有门!");
}
else {
currentRoom = nextRoom;
showPrompt();
}
}//去某个房间,并提示出口
修改Game中的showPrompt()
public void showPrompt(){
System.out.println("你在" + currentRoom);
System.out.print("出口有:");
System.out.println(currentRoom.showOut());
}
小节
到此,我们已经把Room的数据与操作封装起来了;所有的数据都由Room自己决定自己操作,game只负责接收数据,game需要什么,就由Room类将处理好的数据返回给Game。
5.4:可拓展性
我们已经实现的设计与其原始版本比较已经有了很大的改进,然而还可以有进一步的提高。 优秀的软件设计者的一个素质就是有预见性。什么是可能会改变的?什么是可以假设在软件的生命期内不会改变的? 可扩展性:代码的某些部分不需要经过修改就能适应将来可能的变化。
这个游戏会是一个基于字符界面的游戏,通过终端进行输入输出,会永远是这样子吗? 以后如果给这个游戏加上图形用户界面,加上菜单、按钮和图像,也是很有意思的一种扩展所以,我们需要提高其可拓展性
使用容器提高灵活性
我们使用容器来实现Room的成员变量(出口),随意增加出口,减少了硬编码,提高其灵活性。
private HashMap<String,Room> exits = new HashMap<String, Room>();
出口集合 = <方向 (String对象),房间(Room对象)>
setExit ():设置出口
public void setExit(String direction,Room room){
exits.put(direction,room);
}
showOut():显示出口
修改原来的显示出口函数,依次取出容器里的数据,结束循环后返回数据
public String showOut( ){
StringBuffer ret= new StringBuffer();
for(String items:exits.keySet()){
ret.append(items+" ");
}
return ret.toString();
}
getExit():获取出口
直接返回相应方向对应的Room对象
public Room getExit(String direction){
return exits.get(direction);
}
Game中修改房间初始化,改为一条一条地设置方向与对应房间
// 初始化房间的出口
outside.setExit( "east", lobby);
outside.setExit("south" , study);
outside.setExit("west",pub);
lobby.setExit("up", pub);
lobby.setExit("west",outside);
pub.setExit("east",outside);
pub.setExit("down", lobby );
study.setExit("north",outside);
study.setExit("east", bedroom);
bedroom.setExit("west", study);
小节
到此,程序的代码看起来优雅简洁了许多。原本是硬编码的成员变量(出口),使用容器来存储,某房间对象的出口已经非常灵活,想加入up
与down
出口是非常容易的事情了,可拓展性提高了许多。
5.5:数据与框架
从程序中硬编码的东西识别出框架和数据,以代码实现框架,将部分功能以数据的方式加载,这样能在很大程度上实现可扩展性。
Game类还存在什么问题?
Game类的主函数中,我们发现这样几行代码;其作用是输入命令后,执行相应的函数操作;这是一个很明显的硬编码的东西,命令就只能有三个;我们能否由此实现一个框架,以便后续新增命令呢?
通过Hashmap实现框架
当玩家输入某个字符串命令的时候,我们就通过Hashmap的get函数,将那个函数获取出来并执行,但是Hashmap里面只能存放对象,我们只能把函数放置在一个叫Handler的对象里面
- 创建Hashmap
容器,实现命令字符串与函数相对应。 - 将Handler设为父类,使用子类继承父类函数,一个命令对应一个子类,便于管理。
private HashMap<String,Handler> handlers = new HashMap<String,Handler>();
//新建一个命令哈希表,命令-函数
实现Handler父类
- 构造函数
public Handler(Game game)
(接收Game对象):某些函数只能在Game类中被调用,Handler没有此类函数处理命令,所以需要传递当前引用Handler的Game对象 protected Game game;
保证子类也可以使用Game对象public void docmd(String word)
;接收命令,做相应操作Boolean isBye()
;比较特殊的结束命令,不能放在docmd()中public class Handler {
protected Game game;
public Handler(Game game){
this.game = game;
}
public void docmd(String word){
}
public Boolean isBye(){
return false;
}
}
实现Handler子类
HandlerHelp.java
public class HandlerHelp extends Handler{
public HandlerHelp(Game game) {
super(game);
}
@Override
public void docmd(String help){
System.out.print("迷路了吗?你可以做的命令有:go bye help\t");
System.out.println("如:go east");
}
}
HandlerGo.java
public class HandlerGo extends Handler{
public HandlerGo(Game game) {
super(game);
}
@Override
public void docmd(String direction) {
game.goRoom(direction);
}
}
HandlerBye.java
public class HandlerBye extends Handler{
public HandlerBye(Game game) {
super(game);
}
public Boolean isBye( ){
return true;
}
}
将Handler对象放入Hashmap容器中
handlers.put("bye",new HandlerBye(this));
handlers.put("help",new HandlerHelp(this));
handlers.put("go",new HandlerGo(this));
修改Game类硬编码
将原主函数测试代码修改为Game类的play()函数 思路:
- while(true)循环实现可重复输入
- 将输入的命令使用split函数拆解,存入字符串数组words
- 声明变量order为Handler类型,可以管理Handler对象及其子类对象
- 使用get函数在Hashmap容器Handler,寻找到与命令相对应的那个子类对象,并赋值给order
- 判断word长度,go命令长度2,bye与help为1,如果为2,那么将words[1]交给value
- 如果order里面存在Handler子类对象,执行子类对象的docmd函数,并将value(初始值为空)传递给此函数。如果isBye返回的结果是true,结束游戏。
public void play(){
Scanner in = new Scanner(System.in);
printWelcome();
while ( true ) {
String line = in.nextLine();
String[] words = line.split(" ");
Handler order = handlers.get(words[0]);
String value="";
if(words.length >1)
value = words[1];
if(order!=null){
order.docmd(value);
if(order.isBye()){
break;
}
}
else {
System.out.println("输错啦,请重试一遍命令!\n 需要帮助?试试输入help");
continue;
}
}
System.out.println("感谢您的光临。再见!");
in.close();
}
小节
到此,我们新增一个命令就简单了许多
handlers.put("命令",new Handler命令(this));
命令集合里添加一个命令与一个相对应的Handler子类对象
- 新增一个Handler的子类对象,编写想要实现的功能