第六课类与面向对象编程2-1

一、如何使对象的行为出现差异

1. 让Player具备决策能力

  前面我们在写游戏代码时,Player类里有一个方法没写,那就是决定是否要牌的方法。在这里我们用一个简单的算法来实现这个它。主要思路就是根据对方的牌与自己的牌做比较,如果自己的牌总点数小于10就要牌,或者自己的牌总数小于15且小于对方牌的总点数时就要牌,除此之外就不再要牌。如果一旦决定不要牌了按规则后续都不能再要。该方法的实现需要增加一个boolean类型的变量,以标识停止后续要牌。实现代码如下:

private boolean isClosed;   // 是否停止要牌标记
public Player(){ this.pokers = new int[12]; // 最极端情况下拿到4个A,4个2,4个3,因此长度设为12即可 this.index = 0; this.isClosed = false; // 初始化停止要牌标记}
public boolean getIsClosed(){ return this.isClosed;}
// 是否要牌决策方法public boolean wantMore(Player rival){ if (isClosed) return false;
int rivalSum = rival.getSum(); // 对家点数 int mySum = this.getSum(); // 本家点数 if ((mySum < rivalSum && mySum < 15) || mySum < 10) return true;
this.isClosed = true; // 停止要牌 return false;}

  我们在PlayerTest中增加对新方法的测试代码,因为这种决策方法不好做断言,就直接输出结果。代码如下:

public class PlayerTest {    @Test    public void wantMore() {        Pokers pokers = new Pokers();        Player p1 = new Player();        Player p2 = new Player();
for (int i=0; i<12; i++){ if (p1.getIsClosed() && p2.getIsClosed()) break;
if(p1.wantMore(p2)) p1.wantPoker(pokers.getNextPoker()); if (p2.wantMore(p1)) p2.wantPoker(pokers.getNextPoker()); }
String text = "player1:" + p1.getStatString() + "总数:" + p1.getSum() + ",player2:" + p2.getStatString() + "总数:" + p2.getSum(); System.out.println(text); }}

  在调试窗口显示的输出结果如下:

  可以看到player1与player2都按照决策算法做出了是否要牌的决策。

2. 给Player分等级

  现在我们在功能上要更进一步。大家要玩游戏的时候,电脑对手是有级别差异的,有的还分为好几级,我们也可以把Player类分一下级,分为初级与高级。在我们的程序里, 区分Player的等级的核心是决策方法的逻辑不同。那如何能使Player类的决策方法wantMore因为级别的差异而表现不同呢?一种方法是在类中增加一个等级标记,在对象被初始化时将其分别赋值为初级或高级,在wantMore内部则增加判断语句,高级时采用一种决策逻辑,初级时用另外一种。这种方法在实现功能上是没有任何问题的,问题在以后的维护上。一个标记就是一个变化点,在类中可能为了这个标记在不同的方法中会有多处if或switch语句,一旦存在问题或需要扩展的时候,这些地方都需要被逐一找到并进行修改。修改的时候原来的代码中不管有没有问题的地方都会受到影响,牵一发而动全身,可能反而引发更多的问题。因此最好避免这种随意加标记的扩展方式。我们通过上次课的学习,知道了类能够很好地封装变化,那是否可以把高级玩家与低级玩家做成两个不同的类?这样它们之间的差异就会被封装在各自类里面,后续再增加玩家等级就再加一个类,要调整各自的算法互相也不会被影响。但是这两个类明显具有很多共同的代码,只有一个方法的具体实现不同。如果做成两个完全不相关的类,把相同的代码都各抄一遍,这样显得就太低效了。

答案是还真有!面向对象有3大特性,分别是:封装、继承与多态。前面我们介绍了封装,现在这种情况呢?是要加钱的。不是,就要用到继承了!我们先敲代码:

public abstract class PlayerBase {    private int[] pokers;    private int index;    protected boolean isClosed;   // 是否停止要牌标记
public PlayerBase(){ this.pokers = new int[12]; // 最极端情况下拿到4个A,4个2,4个3,因此长度设为12即可 this.index = 0; this.isClosed = false; // 初始化停止要牌标记 }
public boolean getIsClosed(){ return this.isClosed; }
// 是否要牌决策方法 public abstract boolean wantMore(PlayerBase rival);
// 要一张牌 public boolean wantPoker(int num){ if (this.index >= 12) { // 限制性代码,避免数组序号出错 return false; } this.pokers[index] = num; index++; // 序号加1
return true; }
// 获得当前的点数之和 public int getSum(){ int sum = 0; for (int i = 0; i < this.pokers.length; i++){ if (pokers[i] == 0){ // 为0时表示当前已经没牌了 break; }
sum += Pokers.getCount(pokers[i]); // 获得真正点数 }
return sum; }
// 获得牌局结算信息 public String getStatString(){ String txt = ""; for (int i = 0; i < this.pokers.length; i++){ if (pokers[i] == 0){ break; }
txt += Pokers.getColorString(pokers[i]) + Pokers.getCount(pokers[i]) + ","; }
return txt; }}

  我们新建了一个PlayerBase类,它与原来的Player类差不多,差别只在三处:一是在类名前加上了abstract修饰符,表明这是一个抽象类。二是它的wantMore方法没有具体实现,只有一个方法声明,而且也加了abstract修饰符,表明这是一个抽象方法。并且的参数的类型也对应地变成PlayerBase类。三是成员变量isClosed的访问修饰符从private改为了protected,以便继承类能够访问它。我们接着创建两个类来继承这个基类,分别是JuniorPlayer与SeniorPlayer,即初级玩家与高级玩家类。初级玩家类JuniorPlayer代码如下:

public class JuniorPlayer extends PlayerBase {    @Override    public boolean wantMore(PlayerBase rival) {        if (this.isClosed)            return false;
int rivalSum = rival.getSum(); // 对家点数 int mySum = this.getSum(); // 本家点数 if ((mySum < rivalSum && mySum < 15) || mySum < 10) return true;
this.isClosed = true; // 停止要牌 return false; }}

  JuniorPlayer类用extends关键词标明继承了PlayerBase,在该类中重写了PlayerBase类的wantMore方法(前面加上了@Override注解),方法的实现与原来的Player类中是一致的。

  我们再接着写高级玩家类SeniorPlayer。在这个类中的wantMore方法在决策时为了提高胜率,我们采用“作弊”的方式:在要牌之前提前看一眼下一张牌。这样我们先给Pokers类增加一个偷看的方法。代码如下:

// 偷看下一张牌public int peepNextPoker(){    return pokers[index];   // 返回下一张牌,但是不移动指针}

再创建SeniorPlayer类,代码如下:

public class SeniorPlayer extends PlayerBase {    private Pokers pokers;
public SeniorPlayer(Pokers pokers){ super(); this.pokers = pokers; }
@Override public boolean wantMore(PlayerBase rival) { if (this.isClosed) return false;
int mySum = this.getSum(); // 本家点数 int nextCount = Pokers.getCount(this.pokers.peepNextPoker()); // 偷看下一张牌 if(mySum + nextCount <= 21) // 如果要下一张牌不会爆就一直要牌 return true;
this.isClosed = true; // 停止要牌 return false; }}

  同样继承了PlayerBase类,但SeniorPlayer却拥有一个新的成员变量Pokers类的对象pokers,因为后面要用它调用偷看方法。为了对这个对象进行初始化,SeniorPlayer有自己的构造方法,并在内部用super()来调用基类PlayerBase的构造方法,并通过构造方法传入的参数完成对成员变量的初始化。SeniorPlayer也重写了wantMore方法,并通过偷看牌来提升胜率。

  完成了两个继承类之后,我们再修改测试类中的调用代码,看看实际运行结果。我们只需要将

Player p1 = new Player();Player p2 = new Player();

替换成:

PlayerBase p1 = new JuniorPlayer();PlayerBase p2 = new SeniorPlayer(pokers);

就完成了修改。我们看看运行的结果。

  可以看到player1因为决策方式低级所以存在爆点的可能,而player2则胜率大大提升。我们就用继承实现了对象的行为差异,相关基础知识我们一个个来说。


第一季 零基础学习Android开发

第一课 第一个Android程序(1)

第一课 第一个Android程序(2)

第二课 Java语言基础1(1)

第二课 Java语言基础1(2)

第三课 Java语言基础2-1

第三课 Java语言基础2-2

第四课 Java语言基础3-1

第四课 Java语言基础3-2

第五课 类与面向对象编程1


文章转载自微信公众号:跟陶叔学编程

类似文章