如何设计一个类
在总体设计中我们给出了几个类,构成了应用的整体概览。具体到每一个类,则需要我们继续去定义其内部结构。
设计一个类时,往往还要考虑它的接口和继承层次,这里我们暂时无需考虑。
简单地理解,一个类的内部无外乎两部分:
成员变量:一个类操作的数据和内容应该被定义为成员变量,这些成员变量共同构成了一个对象的状态。
成员方法:公有方法就是这个类提供给外部世界的接口,系统中的其他类可以通过公有方法来操作这个类的数据,因此需要考虑这个类的职责和功能,从而确定公有方法。私有方法则一般为公有方法的辅助方法,供内部调用。
现在我们来考虑如何编写Snake类。
设计成员变量
一条贪吃蛇是由一个一个的节点组成的,在传统的贪吃蛇应用中这个节点通常展示为一个黑色的小方块。所以我们需要选择一种数据结构来表示这些相互连接的节点。不过在这之前,需要先定义出节点这个东西。
显然,表示节点状态的就是它的X坐标和Y坐标,那么我们通过一个类来定义节点:
package com.tianmaying.snake;
public class Node{
private final int x;
private final int y;
public Node(int x, int y){
this.x = x;
this.y = y;
}
public int getX(){
return x;
}
public int getY(){
return y;
}
}
提示
成员变量x和y构成了一个Node的状态。注意这两个成员变量使用final修饰了,表示进行初始赋值之后就不能改变。
选择数据结构
为了表示相互连接在一起的节点,我们可以为Snake定义一个集合类型的成员变量,让集合来保存所有节点。
你可能会说也可以使用数组来存储一组节点,但是数组的尺寸是固定的,通常情况下程序总是在运行时根据条件来创建对象,我们可能无法预知将要创建对象的个数(贪吃蛇的身体会不断变长),这时Java的集合(Collection)类了(通常也称集合为容器)就是一个很好的选择,因为它们可以帮我们方便地组织和管理一组对象。
提示
关于集合请参考Java集合。
常用的集合类包括Map、 List和Set,这里显然List是比较适合的,它提供了一系列操作一个元素序列的方法。
接下来要考虑的问题是选择哪一种List,因为List也有许多种,常见的有ArrayList和LinkedList。这两者的主要不同在于:
ArrayList:通过下标随机访问元素快,但是插入、删除元素较慢
LinkedList:插入、删除和移动元素快,但是通过下标随机访问元素性能较低
其实ArrayList是基于数组实现的,而LinkedList是基于链表实现的。这两种数据结构的特点决定了这两个容器的不同之处。
结合我们自己的应用场景可以发现,贪吃蛇不断变长小经常做插入操作,而且我们不需要随机去访问贪吃蛇中的某一个节点。因此,果断选择LinkedList。
有了这个思考过程,接下来Snake的成员变量就很清晰了:
package com.tianmaying.snake;
import java.util.LinkedList;
public class Snake{
private LinkedList body = new LinkedList<>();
}
设计方法
Snake应该提供什么方法来操作自己的状态呢?贪吃蛇有两种情况下会有状态的变化,一种是吃到食物的时候, 一种就是做了一次移动的时候。
此外,贪吃蛇也需要定一些查询自己状态和信息的公有方法。比如获取贪吃蛇的头部,获取贪吃蛇的body,对应可以加入这些方法。
一开始可能定义的方法不够完整,没关系,在编码过程中你会很自然地发现需要Snake提供更多方法来完成特定功能,这个时候你再添加即可。
把这些方法加入进去之后,Snake的代码看起来就丰富多了:
package com.tianmaying.snake;
import java.util.LinkedList;
public class Snake{
private LinkedList body = new LinkedList<>();
public Nodeeat(Nodefood) {
// 如果food与头部相邻,则将food这个Node加入到body中,返回food
// 否则不做任何操作,返回null
}
public Nodemove(Direction direction) {
// 根据方向更新贪吃蛇的body
// 返回移动之前的尾部Node
}
public NodegetHead() {
return body.getFirst();
}
public NodeaddTail(Nodearea) {
this.body.addLast(area);
return area;
}
public LinkedList getBody(){
return body;
}
}
eat和move方法都给出了详细的处理流程,来动手练习一下吧。
提高
这里简单解释一下贪吃蛇移动一格的处理。第一感觉是让body中每个Node的坐标都改变一次,这是一个很笨的o(n)的做法,其实只需要在头部增加一个Node,尾部删除一个Node即可。
定义意义明确的私有方法
一般情况下类中的每个方法不应该做太多的事情,体现在代码量上就是一个方法不要包含太多的代码。
一种最简单也是非常有用的方法就是提取出意义明确的私有方法,这样会让代码更加易懂,调试和维护都会更加方便。
大家可以对比一下下面两种写法:
public Nodeeat(Nodefood) {
if (Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY()) == 1) {
// 相邻情况下的处理
}
}
public Nodeeat(Nodefood) {
if (isNeighbor(body.getFirst(),food)) {
// 相邻情况下的处理
}
}
private boolean isNeighbor(Nodea, Nodeb) {
return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY()) == 1;
}
我们推崇第二种写法,将节点相邻判断的逻辑提取到一个新的方法中,阅读eat()方法的代码时,一眼就知道if语句块要处理的问题。而第一种情况下,时间长了,你可能会一时想不起来这个长长的条件语句用来干嘛的了。
如果你说可以加注释的话,那么你想想让方法命名本身就成为有意义的“注释”是不是一种更好的方式呢?