This documentation is archived and is not being maintained.

An Introduction to Design Concepts Using J#

Visual Studio .NET 2003
 

Pratap Lakshman
Microsoft Corporation

January 2005

Applies to:
   Microsoft Visual J# .NET
   Visual Studio .NET

Summary: Review design solutions available in Microsoft Visual J# .NET with this article. This is the first part of a three-part series discussing problem solving with J#. (16 printed pages)

In "Are Toy Problems Useful" [1] Knuth summarizes the utility of toy problems by saying that the educational value of a problem depends mostly on how often the thought processes that are invoked to solve it will be helpful in later situations—it has little to do with how useful the answer to the problem may be.

In this three-part series, we introduce problem solving with J#. Although the problems may seem like toy problems, we hope you find the thought process invoked to solve it helpful. The problems are simple enough that they do not overwhelm the solutions, the solutions do not use any programming tricks, and the resulting designs lend themselves to reuse should you choose to implement in a different language.

The other two articles in this series are part two, Delegates in Action with J#, and part three, Solving Constraints with J#.

If you have specific questions or feedback, send an e-mail message to us at jsfeedbk@microsoft.com. We look forward to your feedback.

Contents

Introduction
The Problem
A Simple Solution
Classes as Types
Interfaces as Types
The Factory
Strategy
Design Exercises

Introduction

"Design" is context-driven. For a product, this context would be its purpose, its lifetime, the materials used, the time factor, the usage pattern, and the scale, to name but a few. The design of a single seater aircraft would be inappropriate for the Space Shuttle. The design of a steamer would be inappropriate for an aircraft carrier.

Software system design is no different. We must consider various issues like (to name a few):

  • performance
  • portability
  • which algorithms and data structures to use
  • controlling interaction between the various pieces

Having said that, good design typically has the following characteristics:

  • shows appropriate decomposition
  • is modular
  • has low coupling
  • is flexible
  • can be tested

We must design with an eye on implementation.

We'll take a sample problem and try to illustrate these issues.

This tutorial builds up a sample over several complete solutions, refining it at each step by applying a simple pattern [2]; the patterns 'interface as type,' factory,' and 'strategy' are illustrated as we go along.

The Problem

Build a six-sided die that a player might use to play a game.

A Simple Solution

The intention is not to win any programming contest! All we need to do is build a simple six-sided die.

We characterize a die as follows:

  • A die is something that a player can "roll." The value (on the top face) of the die is a result of this "roll" operation.

That is, we characterize a die purely in terms of the operation that we can perform ("roll").

We implement the solution as in app.jsl.

Listing 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());
    }
  }
}

Classes as Types

Our previous solution left some issues open:

  • We don't get the impression of using a die.
  • What if we need a ten-sided die? What do we need to modify?
  • What if we need more than one die? What do we need to modify?
  • The code has "magic" numbers (1, 6).
  • What is the relation between Roll() and the die? Is it captured in our program? Or is it only in the mind of the programmer?
  • What is the relation between Roll() and the number of sides on the die?

We need a better representation of a die.

We characterize a die as follows:

  • A die is a thing.
  • A die is something that a player can "roll."
  • The "roll" operation will roll a die.
  • The value (on the top face) of the die is a result of this "roll" operation.

Let's also remove the restriction on the number of sides that the die can have. We'll parameterize it.

The die is implemented as in dice.jsl.

The die is used from the application app.jsl.

Listing 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));
    }
  }
}

Interfaces as Types

Reorganizing the Modules

Our previous solution allowed us to have more than one die.

Before proceeding, let's introduce the notion of a client and a server. A "server" provides some service. In this case dice.jsl provides the die service. The "client" is app.jsl, which makes use of this service.

What about "coupling" between the client and server? How tightly is the client bound to the die that it is using? What happens if we were to make changes to the die—like introducing some new data/methods? Does the client need to be recompiled?

Is that reasonable? What if the die is used in multiple files? What if it is used by multiple clients?

Interfaces as Types

We introduce an interface to the die in rollable.jsl. The die class in dice.jsl implements this interface. The client, app.jsl, works only in terms of this interface.

Listing 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));
    }
  }
}

Do you see any more areas where we can improve?

The Factory

Coupling

Our previous solution raised questions regarding the coupling between the client and server.

Our design must support independent evolution of the client and server.

For that, we need to minimize this coupling.

As implemented currently, the implementation of die is "visible" to the client. That means that next time we make a change to the die class, the client needs to recompile. There is no clear separation between client and server.

Let's clearly separate the client and server.

We create a new class dicefactory.jsl to manage the creation of die objects. The client now deals only in terms of this factory. This also gives the server the flexibility to manage the die's storage. The client no longer needs to know where the die lives; is it allocated on the heap, is it allocated from a pre-allocated collection of "dice," and so on.

Now, the following files provide the die service (built as say, dice.dll).

dice.jsl
rollable.jsl
dicefactory.jsl

app.jsl is the client (referencing dice.dll).

The "real" representation of the die is no longer "visible" to the client, and it doesn't need to be. The coupling between client and server is restricted to dicefactory.jsl and the interface rollable.jsl.

We have insulated the client from the server!

Listing 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));
    }
  }
}

Strategy

Flexibility

Remember that we are using the die in a game scenario—man versus computer, or like in a casino.

No casino likes to lose. So, one requirement would be to have a predictable outcome to rolling a die. This is popularly called "loading" the die. If the casino starts losing, they will switch to a "loaded" die. Then they can beat you. And make money.

Normally a die will have a "random" loading; that is, the outcome will be a random number in the range of numbers on the faces of the die.

Testing

How would the game developer test the game? It's difficult to write test cases if the outcome of the roll is going to be random (although in this case it is going to be in a fixed range). We must be able to ensure a predictable outcome to effectively test our software.

Strategy

We must be able to vary the "loading" strategy without having to rebuild our die component. (for which we may not even have the source code!) From both flexibility and testing perspectives, it is desirable to be able to vary the rolling strategy ("loading") of our die at run time.

Solution

We abstract the rolling as an interface in rollstrategy.jsl. Every die refers to an instance of a RollStrategy object through this interface. We abstract the "loading" as a method on the die interface that allows us to set the RollStrategy to be used by the die. This object represents the strategy to be used when the die is rolled.

We make the addition to the die representaion in dice.jsl. We move the Roll method out of app.jsl and create a RandomRoll class, randomroll.jsl, representing the random distribution strategy. This is the default strategy used by the die.

We introduce a method to load the die in rollable.jsl and implement it on our die class, dice.jsl. The roll function now uses this "load" to perform the rolling operation. We update dicefactory.jsl to set the default load on the die on creation.

We create a CyclicRoll strategy in a similar fashion in cyclicroll.jsl.

Now the client would initialize the desired rolling strategy, set it on the die, and then roll it. Notice how we can vary it at run time.

Server files (built as say, dice.dll).

dice.jsl, randomroll.jsl,
dicefactory.jsl, rollable.jsl, rollstrategy.jsl

Listing 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();
}

Client files (referencing dice.dll).

app.jsl, cyclicroll.jsl

Listing 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;
}

Only dicefactory.jsl, rollable.jsl, rollstrategy.jsl are "visible" to the client.

This implementation has the following characteristics:

  • clear separation of client and server
  • modular
  • low coupling (restricted to the interface level)
  • supports independent evolution of the client and server
  • we can vary the rolling strategy of the die (flexibility) at run time
  • supports testing

QED.

This completes our "die design" project.

Hope you learned something along the way!

Design Exercises

  • Instead of having a single die support multiple loading strategies, we could have a hierarchy of dice, each supporting a different strategy. Discuss the merits of this with respect to our choice of implementation.
  • Instead of having the die implement the Rollable interface, we could have an abstract class representing a die, and all concrete implementations of a die would inherit from this abstract class. Discuss the merits of this with respect to our choice of implementation.
  • Extend the die to allow for different types of objects on the faces. For example, a die might have letters on its faces, or it might have pictures on its faces.

References

[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

Show: