发布日期: 3/18/2005 | 更新日期: 3/18/2005
Pratap Lakshman
Microsoft Corporation
适用于:
Microsoft Visual J# .NET
Visual Studio .NET
摘要:本文考察了可以在 Microsoft Visual J# .NET 中使用的设计解决方案。这是讨论如何通过 J# 解决问题的文章系列(包括三个部分)的第一部分。
在“Are Toy Problems Useful”[1] 中,Knuth 在总结玩具问题的实用性时说,一个问题的教育价值主要取决于在解决该问题的过程中应用的思维过程在以后的场合中发挥作用的频率高低,而与该问题的答案的有用程度没有什么关系。
在这一包括三个部分的文章系列中,我们将介绍如何通过 J# 解决问题。尽管这些问题可能类似于玩具问题,但我们希望您能发现用于解决这些问题的思维过程对您有所帮助。这些问题足够简单,因而不会掩盖相应的解决方案。解决方案没有使用任何编程技巧,并且如果您选择用不同的语言实现所得到的设计,则可以重新使用这些设计。
该文章系列中的另外两个部分为第二部分 Delegates in Action with J# 和第三部分 Solving Constraints with J#。
如果您有特定的问题或反馈,请通过 jsfeedbk@microsoft.com 向我们发送电子邮件。我们期待着您的反馈。
简介“设计”是上下文驱动的。对于产品而言,该上下文是指它的用途、它的生存期、使用的材料、时间因素、使用模式以及规模等等。单人航空器的设计不会适合于航天飞机。汽船的设计不会适合于航空母舰。
软件系统设计没有什么不同。我们必须考虑各种问题,例如(仅举数例):
•
性能
•
可移植性
•
使用哪些算法和数据结构
•
控制各个部分之间的交互
鉴于此,良好的设计通常具有下列特征:
•
说明适当的分解过程
•
模块化
•
具有较低的偶合性
•
灵活
•
可测试
我们在进行设计时必须考虑实现。
我们将采用一个示例问题并尝试说明上述问题。
该教程在多个完整的解决方案基础之上构建了一个示例,并且在每个步骤都通过应用简单的模式 [2] 来对其进行改进;在此过程中我们将说明“将接口作为类型”、“工厂”、“策略”等模式。
问题生成玩家可能用于玩游戏的六面骰子。
简单解决方案我们的目的不是在任何编程竞赛中获胜!我们需要完成的所有工作是生成一个简单的六面骰子。
我们按如下方式描述骰子:
•
骰子是玩家可以“掷”的某种东西。骰子的值(在上面)是这一“掷”操作的结果。
也就是说,我们完全按照我们可以执行的操作(“掷”)来描述骰子。
我们在 app.jsl 中实现了该解决方案。
清单 1.
// 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());
}
}
}
将类作为类型我们的上一个解决方案没有能够解决一些问题:
•
我们没有获得使用骰子的印象。
•
如果我们需要十面的骰子,那么会怎样呢?我们需要修改哪些代码?
•
如果我们需要一个以上的骰子,那么会怎样呢?我们需要修改哪些代码?
•
代码具有“魔术”数字(1、6)。
•
Roll() 和骰子之间的关系是什么?它是在我们的程序中进行捕获吗?还是它只存在于程序员的头脑中?
•
Roll() 和骰子上的面数之间的关系是什么?
我们需要更好地表示骰子。
我们按如下方式描述骰子:
•
骰子是一个东西。
•
骰子是玩家可以“掷”的某种东西。
•
“掷”操作可以掷骰子。
•
骰子的值(在上面)是这一“掷”操作的结果。
另外,让我们消除对骰子可以具有的面数的限制。我们将把它参数化。
dice.jsl 中实现了该骰子。
应用程序 app.jsl 中使用了该骰子。
清单 2.
// 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 只根据该接口工作。
清单 3.
// 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。
我们已经将客户端与服务器隔离!
清单 4.
// 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.dll)。
dice.jsl, randomroll.jsl,
dicefactory.jsl, rollable.jsl, rollstrategy.jsl
清单 5.
// 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();
}
客户端文件(引用 dice.dll)。
app.jsl, cyclicroll.jsl
清单 6.
// 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 对于客户端而言是“可见”的。
该实现具有下列特征:
•
客户端和服务器之间明显分离
•
模块化
•
偶合性低(被限制到接口级别)
•
支持客户端和服务器的独立演变
•
我们可以在运行时改变掷骰子的策略(灵活性)
•
支持测试
完毕。
这样就完成了我们的“骰子设计”项目。
希望您能够在此过程中学习到一些知识!
设计练习•
与让单个骰子支持多个加载策略不同,我们可以具有骰子层次结构,其中每个骰子都支持不同的策略。请联系我们所选择的实现,讨论这样做的优点。
•
与让骰子实现 Rollable 接口不同,我们可以具有一个表示骰子的抽象类,并且让骰子的所有具体实现都从该抽象类继承。请联系我们所选择的实现,讨论这样做的优点。
•
请扩展该骰子以允许在各个面上具有不同类型的对象。例如,骰子的面上可以具有字母,还可以具有图片。
参考
[1] Knuth, Donald, E., Selected Papers on Computer Science, Cambridge University Press, 1996
[2] Design Patterns: Elements of Reusable Object-Oriented Software—Gamma E. et al. Addison Wesley, 1995