Cover page images (keyboard)

Object-Oriented Design

David Riley & Sue Evans

Press spacebar for next slide

Object-oriented design

Wasn't the last lecture about Object-oriented design ?

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.

General principles

There are several steps that a typical OOD process goes through. Some of these will look familiar from top-down.

More general principles

Case study

Racquetball simulation

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.

Candidate objects and methods

Let's start by identifying the major objects to be used.

Preliminary Interface Design

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() 

Designing the Stats class

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

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.

The Stats.printReport method

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

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)

Designing the Game class

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 Game.play() method

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

The Game.gameOver() method

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

Let's play racquetball!

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]

Cards

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 ?

The new cards code

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()

Let's play cards!

Wow! Look at that. I finally got a pair of aces!

Cards, again.

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.