Lecture Notes
Introduction to Programming and Algorithm Design (COP-1000)

Case study: interface design

This chapter discusses one important aspect of software engineering: how to design functions that are useful, easy-to-use, reusable, and extensible. The design feature that makes this possible is the function's interface what arguments it accepts, and what value it returns. This chapter also introduces TurtleWorld() a graphical module written by the author of our text to help illustrate the concepts of encapsulation and generalization that are fundamental to interface design.


TurtleWorld

  • The TurtleWorld module is available for download as part of a suite of modules called Swampy, written by Allen Downey
  • TurtleWorld is similar to Logo, whose "... best-known feature is the turtle, which is an on-screen cursor, which can be given movement and drawing instructions, and is used to programmatically produce line graphics." (from it's Wikipedia entry)
  • Here is how to download and install it:
     
    1. Download Swampy by right-clicking here:
       
           http://allendowney.com/swampy.1.1.zip
       
    2. Right-click on the downloaded file and chose "Extract all ..." (if you have Windows XP or above; if not, you will need an unzip program to extract the compressed files)
    3. This process should produce a folder named swampy.1.1, in which the various modules and sample programs should have been extracted move this folder to somewhere convenient; you can change the name to something simpler if you want, perhaps ending up with the swampy modules in c:\cop1000\swampy, for example
       
  • To test your installation, start the Python IDLE and type the following program in the edit window (or download the program here), and save it to the same folder as you saved the Swampy files:
# polygon.py -- 090115 (awd p29)
#    Draw polygon with Swampy

from TurtleWorld import *

def main():
   world = TurtleWorld()
   bob = Turtle()
   fd(bob, 100)
   rt(bob)
   fd(bob, 100)
   wait_for_user()

main()
  • Run the program, and you should see a TurtleWorld window open, and the Turtle (bob) trace some lines, using a few of the basic functions a Turtle can perform:
Command Description
bk(t, dist=1) Move Turtle t back specified distance (default distance is 1 pixel)
fd(t, dist=1) Move Turtle t forward specified distance (default distance is 1 pixel)
lt(t, angle=90) Turn Turtle t left by given angle (default angle is 90 degrees)
pd(t) Put Turtle t's pen down
pu(t) Put Turtle t's pen up
rt(t, angle=90) Turn Turtle t right by given angle (default angle is 90 degrees)
  • You can see all the commands available by importing TurtleWorld in the shell, and typing help(Turtle)
  • Now modify the polygon.py program by adding the code necessary to draw a complete square, and save it as polygon2.py

Simple repetition

  • Your code for polygon2.py probably included four repetitions of these statements:

       fd(bob, 100)
       rt(bob)

    (Note that for some reason the text decided to draw a square with lt insead of rt commands?)
  • All programming languages have a way to specify that a statement (or group of statements) is to be repeated a specified number of times; this is called definite repetition
  • In Python, one way to implement definite repetition is to use the for statement and the built-in range function, like this:
     
       for v in range(n):
          statement
          ...

    In the above, v is any variable name, and n is the number of times you want the statement(s) repeated
  • Rewrite polygon2.py using definite repetition, and save it as polygon3.py; it should look like:
# polygon3.py -- 090115 (awd p31)
#    Draw polygon with Swampy

from TurtleWorld import *

def main():
   world = TurtleWorld()
   bob = Turtle()
   for i in range(4):
      fd(bob, 100)
      rt(bob)
   wait_for_user()

main()

Encapsulation

  • We have condensed our code by using definite repetition, but have not made it very easily reusable
  • If you look at the part of our code that actually draws the square, it will work only if the Turtle's name is bob; we would have to change the name in the code to make any other Turtle draw a square, like sally in the sample program polygon4.py
  • A good solution here is to encapsulate the process of drawing a square by putting the logic in a function; we can tell the function which Turtle to use to draw the square by passing it's name as an argument that will be received into a parameter
  • Here is the definition of one such function we could write:
def square(t):
   for i in range(4):
      fd(t, 100)
      rt(t)
  • When this function is called, it draws a square using Turtle t, which is whatever Turtle you pass it as an argument; see sample program polygon5.py
  • When we have encapsulated a function like this, it is easily reusable because we don't have to change anything in it to make use of it again

Generalization

  • Although we have designed a reusable function, it is still not as useful as it could be, because it only draws squares with 100 pixel sides
  • One characteristic of good software engineering is functions that are not only reusable, but that have been made flexible by generalizing them to work in a variety of ways
  • We can improve on our square function by parameterizing the length of the side in the interface to the function:
def square(t, length):
   for i in range(4):
      fd(t, length)
      rt(t)
  • Now a client of the function can specify by arguments both which Turtle should draw a square, and how big the square should be; see polygon6.py
  • There is one value in the function that is still a constant the number of sides drawn (4, since we are drawing a square); what happens if we parameterize the number of sides?
def polygon(t, n, length):
   angle = 360.0 / n          # turn angle
   for i in range(n):
      fd(t, length)
      rt(t, angle)
  • Now we have a reusable, useful function that can draw any polygon, not just a square; see polygon7.py

Interface design

  • We have a function that can draw any size polygon of any number of sides, how about a circle? (A circle is a polygon with an infinite number of sides)
  • We could require the client to specify the number of sides with which to approximate the circle with a polygon, and approximate how long each side must be, but this is seldom how a client would want to invoke a function to draw a circle; the natural way would be to specify the radius of a circle
  • We can design a function circle that requires just two arguments, then calculates the ones needed to approximate a circle with the polygon function and then call it:
def circle(t, r):
   circumference = 2 * math.pi * r
   n = int(circumference / 3) + 1
   length = circumference / n
   polygon(t, n, length)
  • The number of sides needed (n) is proportional to the circumference the larger the circumference, the more sides we will use to approximate it: for a 100 pixel circumference, construct a 34-sided polygon, for a 200 pixel circumference, a 67-sided polygon; see circle.py
  • We have designed a function with an interface that is naturally usable by a client, rather than forcing the client to provide (unnatural) arguments required by a less usable function

Refactoring

  • Now suppose we want to write a function arc that draws just a portion of the circumference of a circle, rather than a complete circle
  • The circle function won't do it will only draw a whole circle, and there is no clear way to modify circle to draw an arc
  • But it is clear that if we had an arc function we could use it to draw a circle; how?
  • Try a new function polyline that parameterizes the number of line segments to draw, their length, and the angle at each turn:
def polyline(t, n, length, angle):
   for i in range(n):
      fd(t, length)
      rt(t, angle)
  • Now we can rewrite polygon to call polyline:
def polygon(t, n, length):
   angle = 360.0 / n
   polyline(t, n, length, angle)
  • And an arc is just part of a circle (polygon) of a specified arc-angle
def arc(t, r, angle):
   arc_length = 2 * math.pi* r * angle / 360
   n = int(arc_length / 3) + 1
   step_length = arc_length / n
   step_angle = float(angle) / n
   polyline(t, n, step_length, step_angle)
  • And, of course, a circle is just an arc with a given radius and an arc_angle of 360 degrees; see circle2.py
  • We finally have a set of useful, reusable functions, which a client can use to construct arcs, circles, and polygons with a natural interface
  • We achieved this by rearranging the program to design more elementary functions; this process is called refactoring
  • It was not clear at the beginning what the best design of our functions would be, but we learned as we went along

A development plan

  • A development plan is a process for writing a program; this case study looked at the "encapsulation and generalization" method, that includes these steps:
     
    1. Start by writing a small program with no function definitions
    2. Once you have the program working, encapsulate the program in a function
    3. Generalize the function by adding appropriate parameters
    4. Repeat steps 1-3 until you have a working set of functions
    5. Look for opportunities to improve the program by refactoring, which will usually mean writing a function that does less than the ones you have
       
  • My maxim: By coding a problem you learn more about it. Your second attempt is always better than you first.

docstring

  • Even if you have spent some time making functions reusable and useful, how does a client know how to use your functions?
  • One way is to write a separate API (Application Programmers Interface) that documents each function and its interface
  • Another way, made simple in Python, is to include the interface documentation in the code itself, by including a docstring (documentation string) at the beginning of each function; see circle3.py
  • The docstring explains the interface interface
 Updated: 12.13.2010