notes blog about

Software design is a combination of art and science. The essence of design is describing a system in terms of magical black boxes (functions or objects) and their interfaces. As usual, the key to success is practice.

Top-Down Design

#!/usr/bin/env python3
# rball.py

from random import random

## Top-Level Design

def main():
    """ PROBLEM:
        Simulate a game of racquetball to find out whether a slightly
        better player can win significant number of games.
        SOLUTION algorithm:
        Get the inputs: probA, probB, n
        Simulate n games of racquetball using probA and probB
        Print a report on the wins for playerA and playerB
    """
    
    # Algorithm written in terms of functions that don't yet exist.
    probA, probB, n = getInputs()
    winsA, winsB = simNGames(n, probA, probB)
    printSummary(winsA, winsB)

## Second-Level Design

def getInputs():
    a = float(input("What is the prob. player A wins a serve? "))
    b = float(input("What is the prob. player B wins a serve? "))
    n = int(input("How many games to simulate? "))
    return a, b, n

def simNGames(n, probA, probB):
    """ Simulate n games and return winsA and winsB.
    """
    winsA = winsB = 0
    for i in range(n):
        scoreA, scoreB = simOneGame(probA, probB)
        if scoreA > scoreB:
            winsA = winsA + 1
        else:
            winsB = winsB + 1
    return winsA, winsB

def printSummary(winsA, winsB):
    n = winsA + winsB
    print("\nGames simulated:", n)
    print("Wins for A: {0} ({1:0.1%})".format(winsA, winsA/n))
    print("Wins for B: {0} ({1:0.1%})".format(winsB, winsB/n))

## Third-Level Design

def simOneGame(probA, probB):
    """ Simulate one game and return scoreA and scoreB.
    """
    scoreA = scoreB = 0
    serving = "A"
    while not gameOver(scoreA, scoreB):
        if serving == "A":
            if random() < probA:
                scoreA = scoreA + 1
            else:
                serving = "B"
        else:
            if random() < probB:
                scoreB = scoreB + 1
            else:
                serving = "A"
    return scoreA, scoreB

def gameOver(a, b):
    return a==15 or b==15

if __name__ == '__main__': main()

Bottom-Up Implementation

A good way to approach the implementation of a modest size program is to start at the lowest levels of the structure chart and work your way up, testing each component (unit testing) as (or before) you complete it.

Prototyping and Spiral development

#!/usr/bin/env python3
# prototype.py

from random import random

def simOneGame():
    # Play just 30 rallies. Each player has
    # a 50-50 chance of winning a given point.
    scoreA = scoreB = 0
    serving = "A"
    for i in range(30):
        if serving == "A":
            if random() < .5:
                scoreA = scoreA + 1
            else:
                serving = "B"
        else:
            if random() < .5:
                scoreB = scoreB + 1
            else:
                serving = "A"
    print(scoreA, scoreB)

if __name__ == '__main__': simOneGame()

Object-oriented design

#!/usr/bin/env python3
# oo-rball.py -- simulation of a racquet ball game.
#                Illustrates design with objects.

from random import random

class Player:
    """ Player keeps track of service probability and score.
    """
    def __init__(self, prob):
        self.prob = prob
        self.score = 0
    def winsServe(self):
        return random() <= self.prob
    def incScore(self):
        self.score = self.score + 1
    def getScore(self):
        return self.score

class RBallGame:
    """ RBallGame represents a game in progress. A game has two players and
        keeps track of which one is currently serving.
    """
    def __init__(self, probA, probB):
        # Create a new game having players with the given probs.
        self.playerA = Player(probA)
        self.playerB = Player(probB)
        self.server = self.playerA # playerA always serves first
    def play(self):
        # Play the game to completion.
        while not self.isOver():
            if self.server.winsServe():
                self.server.incScore()
            else:
                self.changeServer()
    def isOver(self):
        a, b = self.getScores()
        return a == 15 or b == 15 or \
            (a == 7 and b == 0) or (b == 7 and a == 0)
    def changeServer(self):
        if self.server == self.playerA:
            self.server = self.playerB
        else:
            self.server = self.playerA
    def getScores(self):
        return self.playerA.getScore(), self.playerB.getScore()

class SimStats:
    """ SimStats handles accumulation of statistics across multiple (completed)
        games. It tracks the wins and shutouts for each player.
    """
    def __init__(self):
        self.winsA = 0
        self.winsB = 0
        self.shutsA = 0
        self.shutsB = 0
    def update(self, aGame):
        # Determine the outcome of aGame and update statistics.
        a, b = aGame.getScores()
        if a > b:
            self.winsA = self.winsA + 1
            if b == 0:
                self.shutsA = self.shutsA + 1
        else:
            self.winsB = self.winsB + 1
            if a == 0:
                self.shutsB = self.shutsB + 1
    def printReport(self):
        # Print a nicely formatted report
        n = self.winsA + self.winsB
        print("Summary of", n , "games:\n")
        print("          wins (% total)   shutouts (% wins)  ")
        print("--------------------------------------------")
        self.printLine("A", self.winsA, self.shutsA, n)
        self.printLine("B", self.winsB, self.shutsB, n)
    def printLine(self, label, wins, shuts, n):
        template = "Player {0}:{1:5}  ({2:5.1%}) {3:11}   ({4})"
        if wins == 0:        # Avoid division by zero!
            shutStr = "-----"
        else:
            shutStr = "{0:4.1%}".format(float(shuts)/wins)
        print(template.format(label, wins, float(wins)/n, shuts, shutStr)) 

def getInputs():
    a = float(input("What is the prob. player A wins a serve? "))
    b = float(input("What is the prob. player B wins a serve? "))
    n = int(input("How many games to simulate? "))
    return a, b, n

def main():
    probA, probB, n = getInputs()
    stats = SimStats()
    for i in range(n):
        game = RBallGame(probA, probB)
        game.play()
        stats.update(game)
    stats.printReport()

main()

Design tips

Design iteratively. No one designs a program top to bottom in a linear, systematic fashion.

Try out alternatives. Good design involves a lot of trial and error. When you look at someone’s code, it’s finished work, not the process they went through to get there.

Keep it simple. Don’t design in extra complexity until it is really needed.

Resources