David Riley & Sue Evans
Press spacebar for next slide
The last lecture was about the mechanics of creating objects.
This one is about object-oriented design process and how it differs from the more traditional mode of top-down design.
The object-oriented design approach derives a lot from regular top-down design; one could say it's an "enhanced" version of it.
We still favor breaking down a problem into lots of smaller sub-problems; that much is carried over directly from procedural top-down design.
We add several more design principles to our process that objects enable. In particular, the object-oriented approach favors a more data-centered approach.
There are several steps that a typical OOD process goes through. Some of these will look familiar from top-down.
We took the racquetball example from the top-down design chapter of the textbook (Chap 9) and redesigned it to be object-oriented.
Consider a program to simulate multiple racquetball games.
Some key design points:
With these inputs in mind, let's step through the design process.
Let's start by identifying the major objects to be used.
Now that we've defined sketches of our objects, we can come up with a quick outline of how the top level of our program should run.
Let's assume some things already exist, like printGreeting() and getInputs(), and that we've named our game class Game and the stats class Stats.
Let's write main()
def main(): printGreeting() # Get the skills (probabilities of scoring) and the number of games. skillA, skillB, numGames = getInputs() # Initialize our stats. stats = Stats() # Play the games. for i in range(numGames): # Create the game and then play it. game = Game(skillA, skillB) game.play() # Update our stats with the results. stats.update(game) # Once we've played all the games, print the results out. stats.printReport() main()
The Stats class should be the easier one to implement, so let's start on that. Here's a sketch of the class (note that we use "pass" as the contents of the method so that the method still exists, but doesn't do anything because we haven't filled it out yet):
class Stats: def __init__(self): self.winsA = 0 self.winsB = 0 self.shutA = 0 self.shutB = 0 def update(self, game): pass def printReport(self): pass
First, let's develop the update method.
The Stats.update() method takes a Game object as its parameter and updates the internal stats based on the scores supplied. Here's what the code might look:
def update(self, game): # Get the scores from the game. scoreA, scoreB = game.getScores() # Did player A win? if scoreA > scoreB: # If so, update our score to reflect it. self.winsA += 1 # Was it a shutout? if scoreB == 0: self.shutA += 1 # If A didn't win, B did (there are no ties). else: # Update B's score and do the shutout check again. self.winsB += 1 if scoreA == 0: self.shutB += 1
If you've been watching carefully, you'll notice that we hadn't defined a Game.getScores() method in our initial interface (we didn't necessarily know we'd need it unless we were thinking ahead). This is OK; it's part of the iterative development process. Just add that to the list of methods to be developed for the Game class.
Our printReport() method should be a little easier, since it only uses internal data.
We'll probably want the report to look something like this:
Summary of 500 games: wins (% total) shutouts (% of wins) ----------------------------------------------------- Player A: 411 82.2% 60 14.6% Player B: 89 17.8% 7 7.9%
Here's a sketch of that method.
def printReport(self): # Print the header, mostly preformatted. numGames = self.winsA + self.winsB print print "Summary of", numGames, "games:" print print " wins (% total) shutouts (% of wins)" print "-----------------------------------------------------" # Use the printLine() method to print the formatted stats line printLine("A", self.winsA, self.shutA, numGames) printLine("B", self.winsB, self.shutB, numGames) print
This method would be overly-complicated if we tackled all of the print formatting and error avoidance within it. We decided to make a helper function called printLine(), as in top-down design.
This could be either a method or a function, because it doesn't modify any of the object's variables. We decided to make this a module-level function, just passing in the player, that player's number of wins and number of shutouts, and the number of games played.
The printLine() function mostly involves a lot of hairy text formatting.
def printLine(player, wins, shutouts, games): # Make a template format line. template = "Player %s: %7d %9.1f%% %11d %11.1f%%" # Avoid a divide by zero error. if(wins == 0): shutout_percent = 0.0 else: shutout_percent = ((float(shutouts) / wins) * 100) # Print it all out. print template % (player, wins, (float(wins) / games) * 100, shutouts, shutout_percent)
The Game class is next.
As it turns out, it's somewhat simpler than the Stats class;
There's no pesky text formatting, for one thing.
Recall, though, that we've added a method onto the class since our original specification, since we realized we needed a Game.getScores() method when we were implementing the Stats class. It's a simple accessor, so we can write it quickly here. Here is a sketch of what we think we'll need:
class Game: def __init__(self, skillA, skillB): # percentages are passed in, so change to probabilities # and force self.skillA and self.skillB to be floats self.skillA = skillA / 100.0 self.skillB = skillB / 100.0 self.scoreA = 0 self.scoreB = 0 def play(self): pass def getScores(self): return (self.scoreA, self.scoreB)
The principle behind Game.play() is fairly simple; play the game until someone wins. Whenever someone does not win a point, the serve transitions to the other player, who then has a chance to win a point. This continues on until either someone wins on points (someone gets to 15 points first), or by shutout (someone gets to 7 points with their opponent at 0).
For clarity and simplicity, we'll break the game-over determination out into a separate method, as in top-down design.
Let's define the body of the Game.play() method now.
def play(self): # Start with player A. turnA = True; # Go until we have determined that the game has finished. while not self.gameOver(): # Determine which skill we want to use. if turnA: skill = self.skillA else: skill = self.skillB # Did we win the point? if random() <= skill: # Determine to whom we should award the point. if turnA: self.scoreA += 1 else: self.scoreB += 1 # If not, we lose our turn. else: turnA = not turnA
We said previously that we wanted a Game.gameOver() method to
determine when the game had been won. This lets us keep the Game.play()
code fairly clear, and it also lets us change the scoring rules of
the game if we want to.
Here's what the Game.gameOver() method might look like:
def gameOver(self): # Determine if either player has gotten a shutout. shutoutA = self.scoreA >= 7 and self.scoreB == 0 shutoutB = self.scoreB >= 7 and self.scoreA == 0 # Determine if either player has won on points. points = (self.scoreA >= 15) or (self.scoreB >= 15) # If any of the above are true, the game is over. return shutoutA or shutoutB or points
linuxserver1.cs.umbc.edu[143] python racquetball.py Let's play racquetball! Player A will win the serve what percent of the time ? 52 Player B will win the serve what percent of the time ? 49 Simulate how many games ? 500 Summary of 500 games: wins (% total) shutouts (% of wins) ----------------------------------------------------- Player A: 317 63.4% 34 10.7% Player B: 183 36.6% 10 5.5% linuxserver1.cs.umbc.edu[144]
In the Classes lecture, we wrote the Card class.
Now that we have a better idea about Object-oriented design, I believe we can all agree that we could improve our code if we also had a Deck class.
So if we write a Deck class, what internal variables (data members) should it have ?
What methods should it have ?
from graphics import * import string import sys import random import time RANK_NAMES = {1:'Ace', 2:'Two', 3:'Three', 4:'Four', 5:'Five', 6:'Six', 7:'Seven', 8:'Eight', 9:'Nine', 10:'Ten', 11:'Jack', 12:'Queen', 13:'King'} SUIT_NAMES = {'s':'spades', 'c':'clubs', 'h':'hearts', 'd':'diamonds'} class Card: # Constructor def __init__(self, rank, suit): self.rank = int(rank) self.suit = str(suit) if self.rank == 1: self.bJPts = 1 self.rummyPts = 15 elif self.rank > 1 and self.rank < 10: self.bJPts = self.rank self.rummyPts = 5 else: self.bJPts = self.rummyPts = 10 self.name = RANK_NAMES.get(self.rank) + ' of ' self.name = self.name + self.suit self.filename = str(self.rank) + self.suit + ".GIF" # Accessors def getRank(self): return self.rank def getSuit(self): return self.suit def getBJPts(self): return self.bJPts def getRummyPts(self): return self.rummyPts def getName(self): return self.name def getFilename(self): return self.filename class Deck: # Constructor def __init__(self): self.deckList = [] numRanks = len(RANK_NAMES) for suit in SUIT_NAMES: for rank in range(1, numRanks + 1): self.deckList.append(Card(rank, SUIT_NAMES.get(suit))) self.deckSize = len(self.deckList) def shuffle(self): random.shuffle(self.deckList) def getDeckSize(self): return self.deckSize def dealHand(self, hand, numCards): if self.deckSize < numCards: print "There are only", self.deckSize, "cards left in the deck" print "So I can't deal a hand of", numCards, "cards" sys.exit() for i in range(numCards): hand.append(self.deckList[0]) del(self.deckList[0]) self.deckSize -= 1 def main(): print "\nLet's play cards!\n" # Make a deck of cards deck = Deck() # Shuffle it print "I'm shuffling the deck.\n" deck.shuffle() # Get number of cards for this hand numCardsPerHand = int(input("How many cards would you like ? ")) hand = [] deck.dealHand(hand, numCardsPerHand) # Show them to the player numCards = len(hand) win = GraphWin("Let's Play Cards!", numCards * 100, 300) win.setBackground("green3") x = 50 y = 100 for i in range(numCards): pic = Image(Point(x, y), Card.getFilename(hand[i])) x += 100 pic.draw(win) time.sleep(10) win.close() main()
Wow! Look at that. I finally got a pair of aces!
So, now we have classes for Card and Deck. Shouldn't we also have a class Hand ?
Sure.
The nouns of this problem are:
and they should be classes.
The verbs of this problem are:
and they should be methods.
As we expand this problem, so that it really does play some games of cards, there will obviously be more classes, like the pot in poker or the discard pile in rummy.