面向对象程序设计——Java语言 3

本文探讨了面向对象程序设计中的重要原则,以Java语言为例,通过城堡游戏实例展示了如何识别和解决代码设计问题。文章强调了消除代码复制、封装和可扩展性的重要性,并详细介绍了如何通过重构代码降低类之间的耦合度,提升代码的可维护性和可扩展性。此外,文章还提及了抽象与接口的概念,解释了抽象类在程序设计中的作用,以及如何通过抽象类来表达共同属性的抽象集合。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

设计原则

即使类的设计很糟糕,也还是有可能实现一个应用程序,使之运行并完成所需的工作。一个已完成的应用程序能够运行,但并不能表明程序内部的结构是否良好。当维护程序员想要对一个已有的软件做修改的时候,问题才会浮现出来。比如,程序员试图纠正已有软件的缺陷,或者为其增加一些新的功能。显然,如果类的设计良好,这个任务就可能很轻松;而如果类的设计很差,那就会变得很困难,要牵扯大量的工作。在大的应用软件中,这样的情形在最初的实现中就会发生了。如果以不好的结构来实现软件,那么后面的工作可能变得很复杂,整个程序可能根本无法完成,或者充满缺陷,或者花费比实际需要多得多的时间才能完成。在现实中,一个公司通常要维护、扩展和销售一个软件很多年,很可能今天在商店买到的软件,其最初的版本是在十多年前就开始了的。在这种情形下,任何软件公司都不能忍受不良结构的代码。 既然很多不良设计的效果会在试图调整或扩展软件时明显地展现出来,那么就应该以调整或扩展软件来鉴别和发现这样的不良设计。本周将使用一个叫作城堡游戏的例子,这个例子很简单,基本实现了一个基于字符的探险游戏。起初这个游戏并不十分强大,因为还没全部 完成。不过,在本章结束的时候,你就可以运用你的想像力来设计和实现这个的游戏,让它更有趣更好玩。

5.1 城堡游戏

下载要用到的代码castle.zip,仔细的阅读一下这里的两个类,一个Game,一个Room,理解一下这里每一行要做的事情,,然后思考一下,这里有什么问题?这些代码存在什么样的问题?接下来要做的事情是去找出这里面的问题,然后去解决这里面的问题。

我们从main开始对吧,我们要去理解一个代码,从他的main开始,在main里面做了一个game的对象Game game = new Game();,让game去printWelcome。在printWelcome()这个函数上上鼠标右键选择Open Declaration 或者F3,就到了 private void printWelcome()这里。

Open Declaration是打开该方法的接口文件

 

然后就输出了欢迎来到什么,然后他告诉你说现在你在加上一个currentRoom,currentRoom是什么?然鼠标停留在这里,他也会告诉你说这是一个Room对象。

 

 

就进入一个死循环while ( true ) { },处理死循环,死循环里面每次都读语句,读了语句进来以后,对一个字符串做split。因为我们输进去的可能比如说go south,那么它要根据中间空格,把它分成两个单词,第一个单词如果是help做什么,如果是go做什么,如果是bye做什么,是吧?那么如果是help就输出一个help,如果是by就结束,这都这很容易理解。如果是go它调用了一个叫做goRoom的函数。

5.2 消除代码复制

程序中存在相似甚至相同的代码块,是非常低级的代码质量问题。

代码复制存在的问题是,如果需要修改一个副本,那么就必须同时修改所有其他的副本,否则就 存在不一致的问题。这增加了维护程序员的工作量,而且存在造成错误的潜在危险。很可能发 生的一种情况是,维护程序员看到一个副本被修改好了,就以为所有要修改的地方都已经改好 了。因为没有任何明显迹象可以表明另外还有一份一样的副本代码存在,所以很可能会遗漏还 没被修改的地方。

我们从消除代码复制开始。消除代码复制的两个基本手段,就是函数和父类。

这个程序其实问题是非常多,这里面暴露出来的问题,正好就是我们今天要说的在面向对象程序设计当中的一些非常基本的原则。原则第一条,其实我们之前见过的代码复制,我们要消除代码复制,代码复制是设计不良的一种表现,在这个程序当中有非常明显的代码复制。

 

有一段,现在你在什么地方,然后出口有什么?然后往下看goRoom。当我们选择了要去某一个房间的时候,又来了你在什么地方?出口有什么?这就是非常明显的代码复制,所以修改的方案也非常的简单,我们可以把这一段提取出来,把这一段作为是进入到一个房间以后,给玩家看的那么一段提示,所以把它 ctrl +x提取出来,然后加一个函数,public void showPrompt ()。ctrl+v把这个放进来。你在什么地方?然后在这个地方去调一下这个函数,在前面把这一段也去掉,换上这个函数。

 

 

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;
    }

    @Override
    public String toString()
    {
        return description;
    }
}
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();
        showPrompt();
    }

    // 以下为用户命令

    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;
            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();
    }
    
	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();
	}

}

5.3 封装

要评判某些设计比其他的设计优秀,就得定义一些在类的设计中重要的术语,以用来讨论设计的优劣。对于类的设计来说,有两个核心术语:耦合和聚合。耦合这个词指的是类和类之间的联系。之前的章节中提到过,程序设计的目标是一系列通 过定义明确的接口通信来协同工作的类。耦合度反映了这些类联系的紧密度。我们努力要获得 低的耦合度,或者叫作松耦合(loose coupling)。

耦合度决定修改应用程序的容易程度。在一个紧耦合的结构中,对一个类的修改也会导致 对其他一些类的修改。这是要努力避免的,否则,一点小小的改变就可能使整个应用程序发生改变。另外,要想找到所有需要修改的地方,并一一加以修改,却是一件既困难又费时的事情。 另一方面,在一个松耦合的系统中,常常可以修改一个类,但同时不会修改其他类,而且整个程序还可以正常运作。

本周会讨论紧耦合和松耦合的例子。 聚合与程序中一个单独的单元所承担的任务的数量和种类相对应有关,它是针对类或方法 这样大小的程序单元而言的理想情况下,一个代码单元应该负责一个聚合的任务(也就是说,一个任务可以被看作是一个逻辑单元)。一个方法应该实现一个逻辑操作,而一个类应该代表一定类型的实体。聚合理论背后的要点是重用:如果一个方法或类是只负责一件定义明确的事情,那么就很有可能在另外不同的上下文环境中使用。遵循这个理论的一个额外的好处是,当程序某部分的代码需要改变时,在某个代码单元中很可能会找到所有需要改变的相关代码段。

我们的这个城堡游戏的代码是能运行的,能正常运行,一切功能都正常,也没有发现有bug,你怎么输入他反正也都能有一些正确的结果。但是一个能够正常运行的没有bug的代码,不等于它就是一个好的代码,评价一个代码是否好,标准是多元的,并不是只有唯一一个能运行没有bug就是标准。我们还有很多标准

,尤其是这个代码是否适用于将来的需要,什么是将来的需要?将来只有一种需要,就是维护,代码写出来,不是跑一次。不是现在用就好了。你要考虑他一年以后,2年以后5年以后甚至10年以后,这个代码还要有,其他人当然也可能是你自己要继续做下去,需求有了变更,要进一步发展下去了。这个时候你拿之前写的代码,你的代码能不能在今后还能够继续起作用,是不是让今后做维护的人,无论是你还是其他人,能够比较容易的在这个代码基础上做事情。

这是我们去考察一个代码质量非常重要的原则,或者叫做指标。就是代码是不是适合于扩展?比如说在我们现在代码基础上,如果我们想要给他增加新的方向怎么办?我们现在有东南西北4个方向,城堡,他可能有好几层楼的,那么如果我们想要up down,要增加这两个方向,在现在这个代码上面我们要怎么做呢?我们现在的城堡的方向在什么地方体现出来?你看对于room来说,他有4个成员变量,这4个成员变量是别的Room、北南东西4个成员变量。

 

在构造他的时候,我们给的是一个描述字符串。

    public Room(String description)

    {

        this.description = description;

    }

但是后面我们有一个set exists设每一个房间的出口,在设出口的时候我们要给出4个方向的其他room的变量,然后才能够把这些4个变量给它初始化起来。

然后不光是这样子,这个代码问题挺大的,不只在Room里头,在Game里头,,比如在goRoom这个函数里头,我们会要用到当前那个房间的4个出口,显示提示符的时候,再到了一个房间以后,我们要显示他的信息的时候,也要用到它的出口的信息。

我们在很多地方用到了出口信息,还有我们在初始化这个房间场景的时候,初始化城堡的时候,我们是去做了5个房间出来,做了5个房间出来,然后每一个房间去设了他的这些出口的对应的这些房间,那么如果我增加了,所有这些地方都要改。那么这种代码的设计,就是一种不良的体现。你的代码没有可扩展性。

可扩展性的意思是说将来有什么新的东西要加进来的时候,你的那些代码是不是可以很大程度上保持不变?以不变应将来的万变,这叫做可扩展性。要实现这里的良好的扩展性。

 

Room类和Game类紧密的结合在一起。我要增加一个出口,不仅是Room类也要增加一些东西,在Game类里头也要增加一些东西。

我们首先看到的是这两个类,他们的耦合非常的紧,耦合是什么?耦合指的是类和类之间的关系,类和类之间的关系越松越好,不要这么紧紧的咬在一起,保持距离很重要。对象和对象之间类和类之间尽量的保持距离,隔开,对彼此的认识越浅越好。

我们现在深入到什么程度,你看在这个代码里面,从Room来看,我们的这些东西全都是public的,全都是public的。我们在一开始讲类的设计的时候,提到过一个很基本的原则,,首先想到的是让你的所有的成员变量都是私有的,private。万不得已的时候才去做public。现在好我们Room类的设计一上来通通都是public,结果就是在Game里面,我们不断的在使用这些东西。

Ok,我们先来做这第一件事情,设法让他们的耦合松一点。我们把Room里面所有的这些public东西都改成private。

 

所以我们这样来改,在Room里面需要有一个对房间的描述的东西,能够说明白房间里面有什么出口的东西。所以我们在后面增加一个函数getExitDesc()。他可以返回一个用来表达现在有什么房间的那么一个描述的字符串。

 

做一个stringBuffer,然后我们每一次让 stringBuffer去append这么一个字符串,最后我们让 stringBuffer给我们产生一个string就好了。这个理由主要是string是一个immutable的对象,就string这种类型的对象,你没有任何办法去对它做修改。而stringBuffer是一个会可以不断修改的一种对象,所以通常当我们需要采用很多复杂的字符串的操作去产生一个结果字符串的时候,我们都会用stringBuffer,而不用string。

好,这里有了一个getExitDecs() { }以后,我们Game这边showPrompt就变了。showPrompt的这些东西就都没有了。

而变成是去输出System.out.print( currentRoom.getExitDecs() )。于是这一个地方就解开了。也就是说Room和Game之间这里的一个耦合解开了。

还有在这里是说当玩家输入了一个方向,然后我们要从Room那边得到说那个方向上面他有没有东西?那么同样的我们也让Room自己来做这个事情。

是说我们有一个函数,它会返回一个Room叫做getExit。它需要有一个字符串是一个direction,然后他要根据 direction决定返回一个什么?同样的我们先有一个先把这个框架搭好。public Room getExit(String direction) {Room ret=null; return ret;}接下来就是要怎么去填 ret的问题,那么我们可以把这一段代码给他拿过来。

然后这里就变了就变成是 currentRoom的 getExist(direction);

 

Ok这代码改造完成,所有的错误都完都没有了,现在 game和room之间松了很多,game不再会去直接用到room的任何成员变量了,room所有的成员变量都已经是私有了。

我们实现了两个接口,来帮助game和room之间的解耦,这是我们做的第一件事情,用封装来降低耦合。原本Game是直接使用Room面的那4个成员变量,来掌握它的每个方向的出口。现在我们把Room的这些成员变量都做成私有了,提供了两个接口,一个接口给出文字描述,一个接口可以根据方向来返回在那个接口在出口方向上的对应的房间。

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();
        showPrompt();
    }

    // 以下为用户命令

    private void printHelp() 
    {
        System.out.print("迷路了吗?你可以做的命令有:go bye help");
        System.out.println("如:\tgo east");
    }

    private void goRoom(String direction) 
    {
        Room nextRoom = currentRoom.getExit(direction);
        
        if (nextRoom == null) {
            System.out.println("那里没有门!");
        }
        else {
            currentRoom = nextRoom;
            showPrompt();
        }
    }
	   
    public void showPrompt() {
    	System.out.println("你在" + currentRoom);
        System.out.print("出口有: ");
        System.out.print(currentRoom.getExitDesc());
        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();
	}

}

package castle;

public class Room {
    private String description;
    private Room northExit;
    private Room southExit;
    private Room eastExit;
    private 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;
    }

    @Override
    public String toString()
    {
        return description;
    }

    public String getExitDesc() {
    	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();
    }
    
    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;
    }
}

5.4 可扩展性

       我们已经实现的设计与其原始版本比较已经有了很大的改进,然而还可以有进一步的提高。 优秀的软件设计者的一个素质就是有预见性。什么是可能会改变的?什么是可以假设在软件的生命期内不会改变的? 在游戏的很多类中硬编码进去的一个假设是,这个游戏会是一个基于字符界面的游戏,通过终端进行输入输出,会永远是这样子吗? 以后如果给这个游戏加上图形用户界面,加上菜单、按钮和图像,也是很有意思的一种扩展。如果这样的话,就不必再在终端上打印输出任何文字信息。还是可能保留着命令字,还是会在玩家输入帮助命令的时候显示命令字帮助文本,但是可能是显示在窗口的文字域中,而不是使用System.out.println?

可扩展性的意思就是代码的某些部分不需要经过修改就能适应将来可能的变化。

       对于Room以外的其他地方来说,Room内部如何去表达每一个出口,如何去建立起出口方向的字符串和它在出口方向上面的对应的房间之间的关系,就是Room自己说了算,Room就可以做调整。我们现在在Room里面用4个成员变量来表达4个方向上对应的房间的。如果希望做到尽可能的灵活性,我们就不应该这样硬编码,我们就应该采用容器来表达每一个方向上对应的房间。这样一来,方向就和Room本身没有关系了,这不是硬编码的,我可以任意往里面增加新的方向。也就是说对于Room来说,我们不应该有这样的4个东西,我们应该有的一个东西是 一个 private HashMap<String, Room>它的key是String,它的 value是Room。

设出口的时候就不是一次性需要把东南西北都给出来,给一个字符串表明说这个方向上是那个房间,,所以我们需要的是另外一个接口,叫做setExit(String dir,Room room)。然后我们要做的事情非常简单,我们在 exits里面put direction和那个Room进去。

 

描述就变成是说对于在这个出口里面的 keySet(),拿到每一个key,然后呢我们把它加进去,加这个dir进去。sb.append(dir);当然这里头我们还需要加一个空格sb.append(' ');。

for (String dir: exits.keySet()) {

        sb.append(dir);

        sb.append(' ');

    }

 

在getExit时候,变成说ret就等于 exits 的get那个direction就好了 ret = exits.get(direction);。直接从容器里面拿到那个东西就可以了,

 

对于Game来说,初始化房间的出口这段是唯一受到影响代码,原来是 setExits,现在我们变成要一个一个去设了。

up和down的加入和Room就没有任何关系了。我们只要在外面给他恰当的字符串和恰当的Room之间的关系,就能把这个事情建起来。因此Room它就对于出口体现出了可扩展性。

 

package castle;

import java.util.HashMap;

public class Room {
    private String description;
    private HashMap<String, Room> exits = new HashMap<String, Room>();
    
    public Room(String description) 
    {
        this.description = description;
    }

    public void setExit(String dir,Room room){
    	exits.put(dir, room);
    }
 
    @Override
    public String toString()
    {
        return description;
    }

    public String getExitDesc() {
    	StringBuffer sb = new StringBuffer();
    	for (String dir: exits.keySet()) {
    		sb.append(dir);
    		sb.append(' ');
    	}
    	return sb.toString();
    }
    
    public Room getExit(String direction){
//    	Room ret = null;
//    	ret = exits.get(direction);
//      return ret;
    	return  exits.get(direction);
    }
}

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.setExit("east" , lobby);
        outside.setExit("south" , study);
        outside.setExit("west" , pub);
        lobby.setExit("west", outside);
        pub.setExit("east", outside);
        study.setExit("north", outside);
        study.setExit("east", bedroom);
        bedroom.setExit("west", study);
        lobby.setExit("up", pub);
        pub.setExit("down", lobby);
        currentRoom = outside;  //	从城堡门外开始
    }

    private void printWelcome() {
        System.out.println();
        System.out.println("欢迎来到城堡!");
        System.out.println("这是一个超级无聊的游戏。");
        System.out.println("如果需要帮助,请输入 'help' 。");
        System.out.println();
        showPrompt();
    }

    // 以下为用户命令

    private void printHelp() 
    {
        System.out.print("迷路了吗?你可以做的命令有:go bye help");
        System.out.println("如:\tgo east");
    }

    private void goRoom(String direction) 
    {
        Room nextRoom = currentRoom.getExit(direction);
        
        if (nextRoom == null) {
            System.out.println("那里没有门!");
        }
        else {
            currentRoom = nextRoom;
            showPrompt();
        }
    }
	   
    public void showPrompt() {
    	System.out.println("你在" + currentRoom);
        System.out.print("出口有: ");
        System.out.print(currentRoom.getExitDesc());
        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();
	}

}

5. 5 框架加数据

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

 

我们对Room做了改造,我们把出口从原来以成员变量做硬编码,变成了用容器,用Hash表,这给我们一个启发。原本我们是硬编码的东西,我们把它做成框架,在Room里面有那么一个框架,这个框架表明说建立一个数据结构,数据结构,名字和对应的那个地方的房间之间的一个对应关系。实现了这样一个框架之后,当然围绕这个框架不仅仅是这个HashMap,我们还有那些接口,比如说get exit等等这样的一些接口函数。这些东西形成了一个框架。在这个框架里头,如果要增加新的出口,很容易。所以这是一种思路。把程序的硬编码尽可能的解成框架和数据的结构。

 

还有一个地方是硬编码的,命令解析里边有三个命令,go help bye。在 Game里头有那么一个循环,在这个循环里面,我们会用if else去判断输入的字符,那个字符串命令是哪一种?这是一种硬编码,它能不能被变成框架?我们知道字符串对应一个什么东西,这事情HashMap能做,但是对应什么?我们在这要对应的是函数。输入go的时候去调一个什么函数,输入help的时候去调另外一个什么函数,这个函数怎么能够变成在HashMap里面能够放的东西,HashMap放的是一个key和一个value,key和value都必须是对象,函数不是对象。

定一个新的类,Handler。Handler里面有 public的函数,叫做doCmd(String word),他接受一个String的 word作为输入。

 

我们先把它定义成这个样子。当然作为这个Handler来说他什么都不做,它只是提供一个基础,接下来我们会要有比如说做help的Handler,比如说做 go的Handler和做bye的 Handler。当然bye有一定的特殊性,我们可以以特殊的方法来处理。我们先来看,如果我们把它定义成这个样子,接下来我们要做的事情是我们需要在Game里面有一个数据结构,它保存了字符串和Handler的对象之间的那么一个对应关系,所以在这儿我们要有一个HashMap,他的 key是String,而他的 value是Handler。

 

那么在Game构造的时候,我们需要给它填进去,Handler里面要put一些东西,比如说对于 go我们需要 put一个叫做HandlerGo的东西。我们要制造一个叫做HandlerGo的对象出来,把它放进去。需要有一个叫做HandlerGo的类,这一次我们需要把这个 HandlerGo这个类给它定义出来。

 

在做这些事情之前,我们还得做一个调整,我们需要把这个while(true)循环给他放到Game里头去,我们需要Game本身有一个叫做play的函数。在这个函数里面来做这样的一个死循环

 

做了这一系列的改动的目的是什么呢?因为我们在这儿要用到我们的Handle handler=handlers.get (word[0])。我们要用它去get。 word[0]得到了一个handler。如果 handler不是null,说明它是一个有效的命令,接下来我们要做的事情就应该是让 handler去做doCmd给他word[1]就可以了。先把下面这些代码注释起来。

 

会看到我们有两个问题没有解决,第一个问题是 bye怎么办?我们显然是需要让 handler在doCmd之后,如果这个handler是 bye那个handler,他能够给我们一个bye。

 

我们就需要这个handler有一个特殊的东西说如果 handler能够告诉我们说isBye,那么我们再来break。所以handler需要有另外一个函数,说他会告诉我们isBye(),默认的是返回false。

于是我们就可以做一个专门用来做bye的 handler。因此我们会有另外一个类说有一个类叫做HandlerBye。

 

 

我们在这儿先放了一个 bye的Handler。Bye 的Handler里面new一个HandlerBye的对象。

 

另外我们在这里也不能直接去使用 words[1],因为如果输入比如像bye这样的命令,它是没有words[1]的。另外有一个叫做value的变量String value = "";如果 words的length大于1的话,那么 value等于words[1]。

这样的话就直接用的是value,而不是用words[1]。

 

在Game构造的时候制造一个叫做HandlerHelp的对象。

 

HandlerGo的类,对于help来说,我们需要@Override的是doCmd。而这个doCmd(String word) 做什么事儿呢?原来在Game里面有private void printHelp() { }把这一段给拿过来。

 

还没有解决go的问题,go要做的事情其实是在Game里面。Game里边的private void goRoom(String direction) { }

在Handler构造的时候记下game,也就是说Handler里面有个game对象的管理者,然后 Handler的构造,就需要记下 game。this.game=game,,

 

 

Game里边的private void goRoom(String direction) { }要改为protected void goRoom(String direction)

做完这个之后,我们的代码今后如果想要增加新的命令,我们要做的事情只有两个地方,第一,要有新的Handler的类型。第二,要在Game的构造器把这些东西放进去,把新的一个Handler放进去,下面在play的这段代码不需要做任何的改动,这段代码对于任何新的命令都是不变,这就是可扩展性。

抽象与接口

6.1 抽象

有一个Shape类的例子。这个类有很多的子类,每个子类也都实现了父类的方法。实际上父类Shape只是一个抽象的概念而并没有实际的意义。如果请你画一个圆,你知道该怎么画;如果请你画一个矩形,你也知道该怎么画。但是如果我说:“请画一个形状,句号”。你该怎么画?同样,我们可以定义Circle类和Rectangle类的draw(),但是Shape类的draw()呢?

Shape类表达的是一种概念,一种共同属性的抽象集合,我们并不希望任何Shape类的对象会被创建出来。那么,我们就应该把这个Shape类定义为抽象的。我们用abstract关键字来定义抽象类。抽象类的作用仅仅是表达接口,而不是具体的实现细节。抽象类中可以存在抽象方法。抽象方法也是使用abstract关键字来修饰。抽象的方法是不完全的,它只是一个方法签名而完全没有方法体。

如果一个类有了一个抽象的方法,这个类就必须声明为抽象类。如果父类是抽象类,那么子类必须覆盖所有在父类中的抽象方法,否则子类也成为一个抽象类。一个抽象类可以没有任何抽象方法,所有的方法都有方法体,但是整个类是抽象的。设计这样的抽象类主要是为了防止制造它的对象出来。

shape程序的最根上的类Shape:

 

Shape类在public class中间多了一个单词,叫做abstract。在这个类里头的函数唯一的函数draw(Graphics g)函数,在public void中间也有一个 abstract。abstract的意思是抽象。

 

从Shape类我们派生出了Rectangle、Ellipse、Square、Circle等等各种各样的形状。然后在Shape里面我们定义了叫做draw的这么一个函数。这个函数定义出来之后,所有的Shape类的子类都有一个叫做draw的函数。

像Shape这样的一个类,在程序中它的作用是什么?它提供了Rectangle、Ellipse、Square、Circle这么一些东西的一个公共的概念,一个公共的父类。

我们最合适的方式让类成为一个抽象的类。Shape类的子类都具有一个draw函数,这个函数本身也应该是抽象的。从语法上来说,由于draw是抽象的,所以 draw在这里是没有括号的。抽象的函数不能有括号{}。抽象的类还有另外一层意思,就是这个类是不能够产生对象的。如果试图做一个Shape的对象s等于new一个shape。错误提示:Cannot instantiate the type Shape。 不能实例化Shape这个类型。

这是一个细胞自动机的程序,运行后网格上面好多黑的点点动来动去动,到底在干什么呢?它基本概念是这样,在棋盘上面,在网格上面,每一个点都是一个细胞,黑点表示这是一个活着的细胞,白点表示死了的细胞。那么每一轮我们看到的一每一步他做的事情就是检查扫描,检查所有的细胞,看细胞和它周围细胞的情况,每一个细胞都在一个九宫格里面,所以它周围有8个细胞,周围的8个细胞,去数活着的细胞的数量。如果说活着的细胞是2个或者3个,不动。但是如果这个细胞周围的活着的细胞小于两个或者大于三个,那么这个细胞就要死掉。而如果一个死掉的细胞周围正好有三个细胞活着,那么它就又活了,又活回来了。黑的不断的在变,白的有可能会变黑。

那么我们来看一下这个细胞自动机的程序,拿到一个陌生的程序,怎么去看懂这个程序?一种看别人程序的办法是去找main。因为程序总是要从main开始的,我们就去找main,找到main以后,想办法去读懂他的每一句,看他每一句都是干什么的,这一句里面提到一些什么样的类,然后再展开去看那个类。

在main里面先做了一个field,有一个Field的对象30 30,Field field = new Field(30,30);大概可以猜得到,因为我们把它运行起来以后,它是一个网格,所以大概这个field是用来表达一个30×30的网格的,然后我们做了一个两重的循环,在两重循环里面, row表示行,从0到它的getHeight(),col表示列,列从0到它的getWidth(),所以两重循环应该去遍历了整个field,遍历 field做了一个place,在row和col这个位置上面去放一个new Cell()。field.place(row, col, new Cell());

        到目前我们已经看到了很多类,我们的CellMachine是入口,在CellMachine的main里面,我们先去做了一个field,在field里面放进了很多的Cell。然后下面我们再来了一个循环,同样的又去做了一遍整个的 field的遍历,在遍历里面我们用field的get函数说我要拿你在row和col那个位置上的一个cell出来,然后我们判断如果随机数小于0.2,因为这个Math.random()函数它会返回的是0~1之间的一个随机数,所以小于0.2。让cell去reborn(),大概猜得出来,reborn()是重生,所以也许我们new的Cell它并不是活着,然后我们要在这个时候去让当中1/5的细胞活过来,可以认为说这一段我们在准备数据。

 

        接下来我们做了一个view, 这个view呢,做的时候告诉它,相应的 field,View view = new View(field);然后我们做了一个JFrame,我们看到的那么一个图形的窗口,在Java来说是叫做一个JFrame,我们让这frame设置一个默认的关闭的操作,叫做EXIT_ON_CLOSE。frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);这句话的作用是为了保证将来,当我在图形窗口上面去按点叉叉按钮的时候,能够把这个程序给结束掉,做了一堆的设置,比如说不可以改变大小,frame.setResizable(false);它的Title是"Cells",frame.setTitle("Cells");在 frame里面把刚才的 view给加进去,frame.add(view);然后让他自己决定一下大小, frame.pack();然后frame.setVisible(true); 那么东西就画出来了。

如果程序到此为止,把下面的代码注释掉。来运行一下这个程序看,就做出了一个静态的一个画面。

 

有这样一个窗口,不能改变大小,可以拖动。有关闭按钮。然后上面有了网格,网格上面有些是黑的,有些是白的。

一段代码准备了一个叫做field的东西,有数据了。另一段代码准备了一个叫做view的东西,view是干什么的?显然看起来view像是我们看见的窗口,那么我们来看view,打开View类。

 

public View(Field field) { theField = field; }

View构造的时候记录了一个field,所以view知道field。然后其实它整个的 View只有一个paint函数,有用的就是这个paint函数,paint函数做的事情是说paint函数是@Override,是继承来的。这个paint函数,因为view本身是一个JPanel,是 Java的图形库当中用来表达一块画面的。所以这个paint函数是什么呢?每一次当我的窗口要被显示出来的时候, paint函数就会被调用,被调用的时候它会得到一个Graphics g这个对象,public void paint(Graphics g)这是当前我要画的那个对象,然后做了一个两重循环,从 field里面得到了每一个cell,得到了 cell以后,如果这个cell是有的,那么我们就让 cell去draw,在view里面做的事情是去遍历 field,让 field里面的每一个单元拿出来,如果那个格子是在的,那个细胞是有的,那么这个细胞你自己去把自己给画出来,这是 view。

再看Field类,Field里面有一个sell的二维数组,rivate Cell[][] field;

就管维持好这个二维数组,要place,就把它放进去。要get就拿出来。

 

然后我们看到 View和Field之间的互动是什么?下面这一段代码在做的事情是什么呢?

 

for ( int i=0; i<1000; i++ ) {  }

做了1000步,每一次要遍历同样的两重循环,要去遍历整个field,找到 field里面每一个单元取出来,取出来以后要field去找到说这个单元它的邻居,把格子周围的8个找出来,找出来之后,要遍历所有的邻居如果某一个邻居是活着的,那么活的邻居的数量要++,所以这一个做完以后,就知道了自己周围有多少个活着的邻居。那么如果自己是活着的,根据前面说的那些规则,如果是活着的,邻居数量是小于2个或者大于3个的话就要死掉。如果是死的,但是邻居的数量有3个了,就要重生。所以其实核心的代码就在这。每一轮去取出相应的一个格子来,取出一个格子来以后,用field的一个方法去得到所有的邻居,然后根据邻居的活着的数量来决定自己该怎么办。

    比如说cell.die();到底做什么事了呢?Cell也是一个简单的类,它有一个alive的变量表明自己是不是活着的,die()就是 alive false;reborn()就是alive true; isAlive()就返回它是不是alive的,draw的时候就先画个方框,如果是活着的,那么这个方框是要填起来的,这就是需要做的事情。

 

现这个程序里面这4个类 Cell、Field、View和CellMachine。

CellMachine实现的是最终的业务逻辑,他把 Field和View连接在一起,通过在他的main里面实现的业务逻辑,所以CellMachine并不代表任何的东西,有价值去看的就是 Cell、Field和View这三者之间的关系,他们是怎么样的?

        这个程序里面非常重要的设计理念是什么?这个程序里面非常重要的一些设计理念是什么?我们看程序看什么?学习编程,不仅仅是学那些语法,我们更重要的是学习在这些程序当中,在这些例子当中,他们体现出来一些什么样的设计理念,在这个细胞自动机里面非常重要的一件事情,就是我们看到数据和表现是分离的,表现是View,数据是那个Field,以及Field里面放的那些Cell,他们是分开的。

 

package cell;

import java.awt.Graphics;
 
public class Cell {
	private boolean alive = false;
	
	public void die() { alive = false; }
	public void reborn() { alive = true; }
	public boolean isAlive() { return alive; }
	
	public void draw(Graphics g, int x, int y, int size) {
		g.drawRect(x, y, size, size);
		if ( alive ) {
			g.fillRect(x, y, size, size);
		}
	}
}
package cellmachine;


import javax.swing.JFrame;

import cell.Cell;
import field.Field;
import field.View;

public class CellMachine {

	public static void main(String[] args) {
		Field field = new Field(30,30);
		for ( int row = 0; row<field.getHeight(); row++ ) {
			for ( int col = 0; col<field.getWidth(); col++ ) {
				field.place(row, col, new Cell());
			}
		}
		for ( int row = 0; row<field.getHeight(); row++ ) {
			for ( int col = 0; col<field.getWidth(); col++ ) {
				Cell cell = field.get(row, col);
				if ( Math.random() < 0.2 ) {
					cell.reborn();
				}
			}
		}
		View view = new View(field);
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setResizable(false);
		frame.setTitle("Cells");
		frame.add(view);
		frame.pack();
		frame.setVisible(true);
		
		for ( int i=0; i<1000; i++ ) {
			for ( int row = 0; row<field.getHeight(); row++ ) {
				for ( int col = 0; col<field.getWidth(); col++ ) {
					Cell cell = field.get(row, col);
					Cell[] neighbour = field.getNeighbour(row, col);
					int numOfLive = 0;
					for ( Cell c : neighbour ) {
						if ( c.isAlive() ) {
							numOfLive++;
						}
					}
					System.out.print("["+row+"]["+col+"]:");
					System.out.print(cell.isAlive()?"live":"dead");
					System.out.print(":"+numOfLive+"-->");
					if ( cell.isAlive() ) {
						if ( numOfLive <2 || numOfLive >3 ) {
							cell.die();
							System.out.print("die");
						}
					} else if ( numOfLive == 3 ) {
						cell.reborn();
						System.out.print("reborn");
					}
					System.out.println();
				}
			}
			System.out.println("UPDATE");
			frame.repaint();
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}
package field;

import java.util.ArrayList;

import cell.Cell;

public class Field {
	private int width;
	private int height;
	private Cell[][] field;
	
	public Field(int width, int height) {
		this.width = width;
		this.height = height;
		field = new Cell[height][width];
	}
	
	public int getWidth() { return width; }
	public int getHeight() { return height; }
	
	public Cell place(int row, int col, Cell o) {
		Cell ret = field[row][col];
		field[row][col] = o;
		return ret;
	}
	
	public Cell get(int row, int col) {
		return field[row][col];
	}
	
	public Cell[] getNeighbour(int row, int col) {
		ArrayList<Cell> list = new ArrayList<Cell>();
		for ( int i=-1; i<2; i++ ) {
			for ( int j=-1; j<2; j++ ) {
				int r = row+i;
				int c = col+j;
				if ( r >-1 && r<height && c>-1 && c<width && !(r== row && c == col) ) {
					list.add(field[r][c]);
				}
			}
		}
		return list.toArray(new Cell[list.size()]);
	}
	
	public void clear() {
		for ( int i=0; i<height; i++ ) {
			for ( int j=0; j<width; j++ ) {
				field[i][j] = null;
			}
		}
	}
}
package field;


import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.JPanel;

import cell.Cell;

public class View extends JPanel {
	private static final long serialVersionUID = -5258995676212660595L;
	private static final int GRID_SIZE = 16;
	private Field theField;
	
	public View(Field field) {
		theField = field;
	}

	@Override
	public void paint(Graphics g) {
		super.paint(g);
		for ( int row = 0; row<theField.getHeight(); row++ ) {
			for ( int col = 0; col<theField.getWidth(); col++ ) {
				Cell cell = theField.get(row, col);
				if ( cell != null ) {
					cell.draw(g, col*GRID_SIZE, row*GRID_SIZE, GRID_SIZE);
				}
			}
		}
	}

	@Override
	public Dimension getPreferredSize() {
		return new Dimension(theField.getWidth()*GRID_SIZE+1, theField.getHeight()*GRID_SIZE+1);
	}

	public static void main(String[] args) {
		Field field = new Field(10,10);
		for ( int row = 0; row<field.getHeight(); row++ ) {
			for ( int col = 0; col<field.getWidth(); col++ ) {
				field.place(row, col, new Cell());
			}
		}
		View view = new View(field);
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setResizable(false);
		frame.setTitle("Cells");
		frame.add(view);
		frame.pack();
		frame.setVisible(true);
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值