In software design, a module is a self-contained unit of code. Modularizing a program allows the greatest flexibility and reusability of code. To get a sense of the importance of modularization, think about updating a stereo system. If you have separate components (tape deck, cd player, radio tuner, amplifier, speakers, etc.), then you can pull out one component and replace it, without disturbing any of the other components. On the other hand, if you own a system where all of the components are combined together in one big box, you can't make a change without replacing the entire system.
With modularized code, you can update a program easily. You pull out one implementation, insert another, and the rest of the code never knows the difference. This should be an important goal for you in all of the code you write from now on. I will definitely be looking for it when I grade your projects. Another important advantage of modularized code is that you can divide up the work among members of a team. It's the independence of the components that allows you to do this. Each team member receives an assignment like "implement the deck of cards interface" and then can work alone because the innards of the deck of cards implementation won't affect anybody else's project. Modularized code is also more likely to produce units that will be usable in some other context, again because one module is not strongly tied to the others.
One important step in modularization is to separate the user interface from the core of the program. If I write a class to play the game of War, its only function should be to go through the steps of the game itself (playing cards, comparing them, etc.). That class should have no awareness of the user interface that might allow a person to witness the game. It should be possible to switch user interfaces and still have the core program work equally well. I might want to have one version that simply displays text on a console, and a different version that displays images of cards in an applet.
I decided to write an abstract class for GameOfWar. The abstract class contains all of the routines necessary to actually play the game. No one will need to instantiate the class because it doesn't make sense to play the game without doing something with the results of the game. As you can see below, during the steps of the game, the class makes reference to certain display methods. If you look at the bottom of the class, you will see that those methods are abstract within this class. They have no method body. The intention is that client programmers will subclass GameOfWar and then they will provide a customized body for the display methods. One subclass could be for a console app, another for a graphical applet, etc.. Yet another subclass might be to run multiple simulations of the game and keep track of the number of tricks required to end each game, while ignoring any display of the data. That subclass would just write the display methods with empty bodies.
Note the code below is only a draft. It has some problems, which I am going to make you fix. Today.
import java.util.ArrayList;
public abstract class GameOfWar{
protected int cardsDownInWar;
protected int cardsPerHand;
protected HandOfCards[] pile;
protected DeckOfCards theDeck;
protected HandOfCards winnings = new ArrayList();
protected boolean gameOver = false;
protected int numTricks=0;
public GameOfWar(){
cardsDownInWar = 3;
cardsPerHand = 26;
theDeck = new StandardDeck();
}
public GameOfWar(DeckOfCards d, int perHand, int numDown){
theDeck = d;
cardsPerHand = perHand;
cardsDownInWar = numDown;
}
public void startNextRound(){
numTricks=0;
theDeck.shuffle();
//deal out cards to each player
pile = theDeck.deal(2, cardsPerHand);
}
public void playGame(){
while (!gameOver){
runTrick();
numTricks++;
}
}
void runTrick(){
if (someoneOutOfCards()){
return;
}
Card firstCard = pile[0].playCard(0);
Card secondCard = pile[1].playCard(0);
displayCards(firstCard, secondCard);
winnings.add(firstCard);
winnings.add(secondCard);
if (firstCard.equals(secondCard)){
conductWar();
}else{
//reward winner
int winner = determineWinner(firstCard, secondCard);
for (int j=0; j< winnings.size(); j++){
pile[winner].add((Card)winnings.get(j));
}
displayStandings();
winnings = new ArrayList();
}
}
void conductWar(){
displayWar();
for (int i=0; i < cardsDownInWar; i++){
if (someoneOutOfCards()){
return;
}
winnings.add(pile[0].playCard(0));
winnings.add(pile[1].playCard(0));
}
//see who won the war (or possibly lead to a nested war)
runTrick();
}
int determineWinner(Card firstCard, Card secondCard){
if (firstCard.compareTo(secondCard)> 0){
return 0;
}else{
return 1;
}
}
boolean someoneOutOfCards(){
if (pile[0].size()==0 || pile[1].size()==0){
gameOver = true;
displayEndOfGame();
return true;
}
return false;
}
abstract void displayCards(Card firstCard, Card secondCard);
abstract void displayStandings();
abstract void displayWar();
abstract void displayEndOfGame();
}
Modularization refers to splitting up code into sensible units. Some of those units are classes and some are simply methods. In the class above you see the method named "someoneOutOfCards" which returns a boolean signaling the end of the game. This method is called from two different spots: once at the beginning of the runTrick() method and also inside of the loop of conductWar(), where cards are being put down for war. Basically, any time you ask a player to produce a card, you need to make sure that the player actually has a card. We certainly could have checked for that by writing an "if" structure inside of each method, but that would be duplication. Since there are two different circumstances where we need to ask the same question, it makes sense to create a unit of code (in this case, a method) which is capable of asking the question and doing a few things in response. Then we can call that method from as many different places as we like and we never need to write the code for it again. Notice also that if we had used the "if" structure inside of runTrick(), we would have needed a comment to explain the purpose of the if. By making a method named someoneOutOfCards(), we replace the comment with a method name that explains its own purpose.
To make the GameOfWar class even more flexible, we should rewrite it to allow more than two players. The abstract class doesn't have to provide the implementation for the multiplayer game; it only needs to allow it. Lines of code like this limit flexibility:
pile = theDeck.deal(2, cardsPerHand);
I did allow for flexibility when I made the variable cardsPerHand. It allows subclassers to vary the way the game is played. I should have done the same thing with the number of players. Then I decided it would be a great exercise for you to do! You will look through the code to find other places that are dependent upon the assumption that only two people are playing. One example would be this method signature:
abstract void displayCards(Card firstCard, Card secondCard);
It is expecting to get two cards specifically. That's an easy place to introduce flexibility:
abstract void displayCards(Card[] card);
Other methods, such as determineWinner(), will also need a different signature to accomodate a flexible number of players. The body of determineWinner() in the abstract class can still proceed with the assumption of two players and return the winner on that basis. By changing the signature, though, we make it possible for subclasses to override that method in order to accomodate more players. The runTrick() method would still run in its usual fashion, but when it comes time for runTrick() to call determineWinner(), the subclass' version of that method would be called.
The determineWinners() method is another example of modularization. That method is only called once, during runTrick() and could easily have been incorporated right in to runTrick(). There's no duplication like there was with someoneOutOfCards(). Even so, it's good to pull out separate functions and give them their own method (this is sometimes referred to as "functional decomposition"). We might want to change the rules of the game and make it so that the lower card wins the trick. To do that, we just override determineWinners() to reflect our new rule. The runTrick() method still works in the same way, since it only wants to know who the winner is, not how they won. We could even have it pick the winner randomly, just to make the game a little dumber! Again, modularization heightens flexibility.
Oh dear, I now see a place where I should have used modularization: at the place in runTrick() where we figure out if there is a war. I used this if structure:
if (firstCard.equals(secondCard)){
This has two problems. First, I am assuming that there are only two players. Furthermore, I am setting the rule for what constitutes a war. I should move that function over to its own method, just like determineWinners(). You're going to take care of that, too. The runTrick() method should be completely purged of any reference to two players. Do that now, working in your groups.
Separating functions makes your code easier to read because the function-based method names are themselves like comments in the code. It also makes each individual method shorter. From now on, we have a new rule: Your methods must be short enough to fit onto the visible screen of the java editor. I'm a benevolent dictator, really.
Notice that this GameOfWar class is neither an application nor an applet. That's how it should be. Your driver programs, whether applications or applets, should actually be very short classes that don't contain the real guts of the program. For example a subclass ConsoleGameOfWar would simply override the abstract display methods, establish a class constructor, and then provide a main method to get the program going:
//class constructor
public ConsoleGameOfWar(String[] args){
super(new StandardDeck(), 10, 3);
}
public static void main(String[] args){
ConsoleGameOfWar theApp = new ConsoleGameOfWar();
theApp.startNextRound();
theApp.playGame();
}
This particular instantiation plays with only ten cards per hand (because it takes so long to win!). You might also want to play with something other than a standard deck, particularly when you are writing a test program. As you see, the class constructor for GameOfWar allows you to send in whatever deck you want to use. Work in your groups to write the entire class for ConsoleGameOfWar. Assume a two-person game. This should be a very quick thing to do. Now that the abstract class has been subclassed, we should be able to actually run the game.
After that, take your comments for the class TestGameOfWar and turn them into code. TestGameOfWar can also be a subclass of GameOfWar. This will take longer than the simple console game of war. Is the abstract class in its current form usable for testing purposes? If not, see where the functions need to be broken up more and suggest some changes to the abstract class.
Where is the example of overloading in this class? Is it a necessity or a convenience the way it was used here?
What is the difference between a method with no body and a method with an empty body?
Why are the fields protected instead of private?
Make a GUI subclass of GameOfWar, which will be run by an applet. Override the playGame method so that users can watch one trick at a time by pushing a button. Hint: the GUI subclass will be a different class than the applet itself. The applet itself will be very short; it is just a driver program. In the applet's start method, you can call the constructor for your GUI subclass and send in the applet as an argument. The GUI subclass constructor should be expecting an argument of type container. That way you could send in either an applet or a frame and it would still work. The container will call all of the usual methods like add, setBackground, etc..
| Previous Topic | Course Home Page |