简单解决方案
我们的目的不是在任何编程竞赛中获胜!我们需要完成的所有工作是生成一个简单的六面骰子。
| • | 骰子是玩家可以“掷”的某种东西。骰子的值(在上面)是这一“掷”操作的结果。 |
也就是说,我们完全按照我们可以执行的操作(“掷”)来描述骰子。
// File: app.jsl
public class App
{
public static int roll()
{
java.util.Random r = new java.util.Random();
return (1 + r.nextInt(6));
}
public static void main(String [] args)
{
// roll multiple times
for (int i = 0; i < 10; i++)
{
System.out.println(roll());
}
}
}
将类作为类型
| • | |
| • | 如果我们需要十面的骰子,那么会怎样呢?我们需要修改哪些代码? |
| • | 如果我们需要一个以上的骰子,那么会怎样呢?我们需要修改哪些代码? |
| • | |
| • | Roll() 和骰子之间的关系是什么?它是在我们的程序中进行捕获吗?还是它只存在于程序员的头脑中? |
| • | |
另外,让我们消除对骰子可以具有的面数的限制。我们将把它参数化。
// File: dice.jsl
// this represents a die
public class Dice
{
public Dice(int i)
{
numSides = i;
}
public int sides()
{
return numSides;
}
// number of sides on the die
private int numSides;
}
// File: app.jsl
public class App
{
public static int roll(Dice d)
{
java.util.Random r = new java.util.Random();
return (1 + r.nextInt(d.sides()));
}
public static void main(String [] args)
{
// create a 6 sided die
Dice d1 = new Dice(6);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d1));
}
// create an 8 sided die
Dice d2 = new Dice(8);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d2));
}
}
}
将接口作为类型
在继续讨论之前,让我们引入客户端和服务器的概念。“服务器”提供某种服务。在这种情况下,dice.jsl 提供骰子服务。“客户端”为 app.jsl,它利用该服务。
客户端和服务器之间的“耦合”是怎样的?客户端与它使用的骰子之间的联系有多紧密?如果我们要对骰子进行一些更改(例如,引入一些新的数据/方法),则会发生什么情况?客户端需要重新编译吗?
那样合理吗?如果在多个文件中使用该骰子,那么会怎样呢?如果它由多个客户端使用,那么会怎样呢?
我们在 rollable.jsl 中引入了骰子接口。dice.jsl 中的骰子类实现了该接口。客户端 app.jsl 只根据该接口工作。
// File: rollable.jsl
// an interface representing a die
interface Rollable
{
public int sides();
}
// File: dice.jsl
// this represents the die; it now implements
// the Rollable interface
public class Dice implements Rollable
{
public Dice(int i)
{
numSides = i;
}
public int sides()
{
return numSides;
}
// number of sides on the die
private int numSides;
}
// File: app.jsl
public class App
{
public static int roll(Rollable d)
{
java.util.Random r = new java.util.Random();
return (1 + r.nextInt(d.sides()));
}
public static void main(String [] args)
{
// create a 6 sided die
Rollable d1 = new Dice(6);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d1));
}
// create an 8 sided die
Rollable d2 = new Dice(8);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d2));
}
}
}
工厂
我们的上一个解决方案引起了与客户端和服务器之间的耦合有关的问题。
像当前所实现的那样,骰子的实现对于客户端而言是“可见”的。这意味着当我们下一次对骰子类进行更改时,客户端需要重新编译。在客户端和服务器之间没有明显的分离。
我们创建了一个新的类 — dicefactory.jsl — 来管理骰子对象的创建。客户端现在只根据该工厂进行处理。这还为服务器提供了管理骰子的存储的灵活性。客户端不再需要知道以下信息:骰子位于何处?骰子是否是在堆上分配的?骰子是否是从预先分配的“骰子”集合中分配的?等等。
现在,下列文件提供了骰子服务(假设生成为 dice.dll)。
dice.jsl
rollable.jsl
dicefactory.jsl
app.jsl 是客户端(引用 dice.dll)。
骰子的“实际”表示对于客户端而言不再是(而且也不需要是)“可见”的。客户端和服务器之间的耦合被限制为 dicefactory.jsl 和接口 rollable.jsl。
// File: dice.jsl
// this represents a die
public class Dice implements Rollable
{
public Dice(int i)
{
numSides = i;
}
public int sides()
{
return numSides;
}
private int numSides;
}
// File: rollable.jsl
// the interface representing a die
interface Rollable
{
public int sides();
}
// File: dicefactory.jsl
// this class handles the creation of dice
public class DiceFactory
{
public static Rollable create(int i)
{
Rollable d = new Dice(i);
return d;
}
}
// File: app.jsl
public class App
{
public static int roll(Rollable d)
{
java.util.Random r = new java.util.Random();
return (1 + r.nextInt(d.sides()));
}
public static void main(String [] args)
{
// creation of dice is done through the factory
// create a 6 sided die
Rollable d1 = DiceFactory.create(6);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d1));
}
// create an 8 sided die
Rollable d2 = DiceFactory.create(8);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d2));
}
}
}
策略
请记住,我们是在游戏方案中使用骰子 — 由人与计算机进行赌博,或者像在赌场中一样。
没有任何赌场喜欢输。因此,要求之一是使掷骰子具有可预测的结果。这一般称为“加载”骰子。如果赌场开始输了,则它们会切换到“已加载”的骰子。然后,它们就可以打败您,并开始赚钱。
正常情况下,骰子具有“随机性”的加载;也就是说,结果将是骰子各个面上的数字范围中的随机数。
游戏开发人员如何测试该游戏?如果掷骰子的结果是随机的,则很难编写测试用例(尽管在该示例中,结果将位于固定范围中)。我们必须能够确保得到可预测的结果,以便有效地测试我们的软件。
我们必须能够改变“加载”策略,而无须重新生成我们的骰子组件。(我们甚至可能不具有相应的源代码!)无论是从灵活性还是测试角度而言,能够在运行时改变掷(“加载”)骰子的策略都是值得的。
我们在 rollstrategy.jsl 中将掷骰子抽象为一个接口。每个骰子都通过该接口引用 RollStrategy 对象的实例。我们将“加载”操作抽象为该骰子接口上的一个方法,该方法使我们可以设置要由骰子使用的 RollStrategy。该对象表示要在掷骰子时使用的策略。
我们在 dice.jsl 中对骰子表示进行增强。我们将 Roll 方法移到 app.jsl 外面,并且创建了 RandomRoll 类 (randomroll.jsl),以表示随机分布策略。这是骰子使用的默认策略。
我们在 rollable.jsl 中引入了用于加载骰子的方法,并且在骰子类 dice.jsl 中实现了它。Roll 函数现在使用该“加载”执行掷骰子操作。我们更新了 dicefactory.jsl 以便在创建时设置骰子的默认加载。
我们在 cyclicroll.jsl 中以类似的方式创建了 CyclicRoll 策略。
现在,客户端将初始化所需的掷骰子策略,在骰子上设置该策略,然后掷骰子。请注意我们如何在运行时改变它。
dice.jsl, randomroll.jsl,
dicefactory.jsl, rollable.jsl, rollstrategy.jsl
// File: dice.jsl
// this represents a die.
// Note that it can be 'loaded' with a rolling strategy
public class Dice implements Rollable
{
public int sides()
{
return numSides;
}
public Dice(int i)
{
numSides = i;
load = null;
}
public void load(RollStrategy r)
{
load = r;
}
public int roll()
{
int i = -1;
if (load != null)
{
i = load.roll();
}
return i;
}
private int numSides;
private RollStrategy load;
}
// File: randomroll.jsl
// this represents on strategy of loading a die
public class RandomRoll implements RollStrategy
{
public RandomRoll(int i, int j)
{
from = i;
through = j;
}
public int roll()
{
java.util.Random r = new java.util.Random();
int ceiling = through + 1;
int i = from + r.nextInt(ceiling);
return i;
}
private int from;
private int through;
}
// File: dicefactory.jsl
// this class handles the creation of dice
// it loads the die with a rolling strategy as
// part of the initialization of a die
public class DiceFactory
{
public static Rollable create(int i)
{
Rollable d = new Dice(i);
RollStrategy r = new RandomRoll(1, i);
d.load(r);
return d;
}
}
// File: rollable.jsl
// the interface representing a die
interface Rollable
{
public int sides();
public void load(RollStrategy r);
public int roll();
}
// File: rollstrategy.jsl
// the interface that represents a rolling strategy
interface RollStrategy
{
public int roll();
}
app.jsl, cyclicroll.jsl
// File: app.jsl
public class App
{
public static int roll(Rollable d)
{
int i = d.roll();
return i;
}
public static void main(String [] args)
{
// create a die; by deafult it is loaded with
// the random rolling strategy
Rollable d = DiceFactory.create(6);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d));
}
// explicitly load it with a different
// rolling strategy at 'run time'
RollStrategy r = new CyclicRoll(1, d.sides());
d.load(r);
for (int i = 0; i < 10; i++)
{
System.out.println(roll(d));
}
}
}
// File: cyclicroll.jsl
// this represents one strategy of loading a die
public class CyclicRoll implements RollStrategy
{
public CyclicRoll(int i, int j)
{
from = i;
through = j;
curVal = from;
}
public int roll()
{
if (curVal > through)
{
curVal = from;
}
return curVal++;
}
private int curVal;
private int from;
private int through;
}
只有 dicefactory.jsl、rollable.jsl、rollstrategy.jsl 对于客户端而言是“可见”的。
[1] Knuth, Donald, E., Selected Papers on Computer Science, Cambridge University Press, 1996