在总体设计中我们给出了几个类,构成了应用的整体概览。具体到每一个类,则需要我们继续去定义其内部结构。
设计一个类时,往往还要考虑它的接口和继承层次,这里我们暂时无需考虑。 简单地理解,一个类的内部无外乎两部分:
现在我们来考虑如何编写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<Node> body = new LinkedList<>();
}
Snake
应该提供什么方法来操作自己的状态呢?贪吃蛇有两种情况下会有状态的变化,一种是吃到食物的时候, 一种就是做了一次移动的时候。
此外,贪吃蛇也需要定一些查询自己状态和信息的公有方法。比如获取贪吃蛇的头部,获取贪吃蛇的body
,对应可以加入这些方法。
一开始可能定义的方法不够完整,没关系,在编码过程中你会很自然地发现需要Snake
提供更多方法来完成特定功能,这个时候你再添加即可。
把这些方法加入进去之后,Snake
的代码看起来就丰富多了:
package com.tianmaying.snake;
import java.util.LinkedList;
public class Snake {
private LinkedList<Node> body = new LinkedList<>();
public Node eat(Node food) {
// 如果food与头部相邻,则将food这个Node加入到body中,返回food
// 否则不做任何操作,返回null
}
public Node move(Direction direction) {
// 根据方向更新贪吃蛇的body
// 返回移动之前的尾部Node
}
public Node getHead() {
return body.getFirst();
}
public Node addTail(Node area) {
this.body.addLast(area);
return area;
}
public LinkedList<Node> getBody() {
return body;
}
}
eat
和move
方法都给出了详细的处理流程,来动手练习一下吧。
提高
这里简单解释一下贪吃蛇移动一格的处理。第一感觉是让body
中每个Node
的坐标都改变一次,这是一个很笨的o(n)的做法,其实只需要在头部增加一个Node
,尾部删除一个Node
即可。
一般情况下类中的每个方法不应该做太多的事情,体现在代码量上就是一个方法不要包含太多的代码。
一种最简单也是非常有用的方法就是提取出意义明确的私有方法,这样会让代码更加易懂,调试和维护都会更加方便。
大家可以对比一下下面两种写法:
public Node eat(Node food) {
if (Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY()) == 1) {
// 相邻情况下的处理
}
}
public Node eat(Node food) {
if (isNeighbor(body.getFirst(), food)) {
// 相邻情况下的处理
}
}
private boolean isNeighbor(Node a, Node b) {
return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY()) == 1;
}
我们推崇第二种写法,将节点相邻判断的逻辑提取到一个新的方法中,阅读eat()
方法的代码时,一眼就知道if
语句块要处理的问题。而第一种情况下,时间长了,你可能会一时想不起来这个长长的条件语句用来干嘛的了。
如果你说可以加注释的话,那么你想想让方法命名本身就成为有意义的“注释”是不是一种更好的方式呢?
登录发表评论 登录 注册