The Great Programmer calls this abstraction. This is the progress that you have made so far:
Representing values by named locations (variables)
Creating actions that work on variables (statements and blocks)
Putting
actions into lumps of code to which we can give names. We can reuse
these actions and also use them in the design process (methods)
Creating things that contain member variables as properties and member methods as actions (objects)
Making constructions that contain objects that are related in some way and want to share resources (class hierarchies)
As you think about a design
in a more abstract way you will initially describe the actions that are
needed in general terms. Rather than looking at specific behavior and
low-level detail with statements like "A sprite will draw itself using
the spriteRect rectangle and the spriteTexture
texture," you are thinking about things in more general terms and
saying things like "A sprite will have to draw itself." This is because
at the early stages of the design process, you are trying to focus on
what things need to do rather than on the specific details of how they
do them. Later on, you can come back and fill in precisely how the draw
behavior works.
It is frequently the
case that different components in your system that share a need for a
draw behavior (for example, different kinds of game object) actually
implement that draw behavior differently (some might draw a texture,
whereas others might just draw a dot or a line), but from the point of
view of the top-level design, it is best to think of them as just having
the Draw behavior.
1. Creating an Abstract Class in C#
From a C# point of view,
you can create abstract classes, which contain placeholders for methods
that need to be present when actual instances of the class need to be
created. Look at the following code:
public abstract class AbstractSprite
{
public abstract void Draw (SpriteBatch spriteBatch);
}
This is a very simple abstract class called AbstractSprite that contains a single Draw method. You would not be able to create an instance of the AbstractSprite class. If you tried as follows, the compiler would reward you with an error:
AbstractSprite s;
s = new AbstractSprite(); // would cause a compilation error
The Draw method is not actually present in the class—it is an abstract placeholder. It is saying to the compiler, "A class that extends AbstractSprite must have a Draw method if you want to make an instance of it."
You could think of an
abstract class as a really strict family business. To join the business,
you have to be related to someone already in it and be able to do all
the things that the business needs. Members of the AbstractSprite business must have a Draw method and be a child of a class in the hierarchy that has AbstractSprite at the base.
2. Extending an Abstract Class
The idea of an abstract class is that it provides a template of
behaviors that are required in all the children of the class. We can
create a child class called MySprite
that satisfies these requirements. In fact, XNA Game Studio makes it
very easy to do this. All you have to do is start typing the class
declaration, then right-click the parent class name. This action brings
up a menu from which you can select the Implement Abstract Class option,
as shown in Figure 1.
When you select the option, an empty child class that contains a placeholder Draw
method is created automatically. If the parent class contained many
abstract methods, the new class would have a placeholder for each. This
is a lovely example of just how an intelligent editor that is aware of
the design of the language you are using can make life much easier for
the programmer. Figure 2 shows how XNA Game Studio fills in the child class started in Figure 1.
The code for MySprite that XNA Game Studio has created allows a program to create an instance of the MySprite class. This is because the MySprite class contains an implementation of the abstract Draw method and therefore fulfills the entry requirements to join the AbstractSprite "club." Here’s the code for this:
public class MySprite : AbstractSprite
{
public override void Draw(SpriteBatch spriteBatch)
{
throw new NotImplementedException();
}
}
The next thing that you would do is fill in the Draw
method with the code that performs the draw behavior for this
particular type of sprite. If you forget to do this, you can still
create instances of the AbstractSprite class and call the Draw method, but the version of Draw shown here throws an exception and stops the program.
An exception is a way
that a program can signal it is unhappy and bring this to the attention
of an exception handler that might be able to sort things out . In these situations, things that you are using have thrown
exceptions when something bad happens (such as trying to load a texture
that isn’t there, or trying to read elements beyond the bounds of an
array). This time it is the other way around, in that the code you are
writing is signaling that something has gone wrong—in this case, that
the programmer has not filled in a placeholder produced by XNA Game
Studio.
3. Designing with Abstract Classes
Abstract classes let
you design a system by working out what a particular kind of object
needs to do and then setting a specification or template to ensure that
all the objects of that kind can do these things. If you were
using abstraction, you would find those behaviors that are common to all
sprites (Draw, Update, BeginGame, and EndGame)
and put them in an abstract parent class, so you could be sure that all
classes in the sprite hierarchy had those minimum behaviors. The class
could even have some data members and non-abstract methods that could be
used by all the child classes.
Of course, the fact that a class contains a Draw
method does not actually mean that it can draw itself properly. A
properly built system also has some tests that can be applied to objects
to ensure that they really can do what is needed, just like you should
have to pass some kind of interview to join the family business even if
your dad owns the company.
4. References to Abstract Parent Classes
You have seen that it is not possible to create an instance of an abstract class like AbstractSprite. This is because if the program ever needed to perform the Draw
operation on such an object, it would not know what to do. However, you
might find it surprising that you can create references of type AbstractSprite, and in fact, this is a very sensible thing to do. Here’s code that does this:
AbstractSprite anySprite;
anySprite = new MySprite();
anySprite.Draw(spriteBatch);
This code creates a reference called anySprite of type AbstractSprite. It then sets this to a new MySprite instance and calls Draw
on it. You might think that the compiler would take issue with this,
but in fact, it is completely happy. A reference to a parent class can
always refer to any of the child types. This is because a child is
always able to do everything that a parent can. (Note that this is in
direct contradiction to real life, in that none of my children seem to
have inherited my dancing ability—and they are very relieved about
this.)
The compiler knows that an instance of MySprite has a Draw method that can be called when required. This would be true for any child of the AbstractSprite class, although which actual code runs depends on precisely what class is on the end of the reference.
This turns out to be
very useful. You might change the design of your game so that all the
sprites on the display are managed using an array. You would want to
hold a large number of sprites in such an array and not have to worry
about precisely what kind of sprites they are. This can be achieved by
making the array of type AbstractSprite:
AbstractSprite[] screenSprites = new AbstractSprite[100];
This would create an
array that could hold references to 100 sprites, which could be any of
the classes that are in the hierarchy that has AbstractSprite
as its root. Actually, references like this also work with parent
classes that are not abstract, so you could manage the game objects in BreadAndCheese using an array of BaseSprite objects as well.