Teaching Design Patterns By Stealth
Stephen Weiss
Department of Computer Science
University of North Carolina at Chapel Hill
Chapel Hill, NC27599-3175
919-962-1888
Abstract
Learning design patterns is tough, even for seasoned programmers who have seen lots of programs and hence have a sense for constructs that tend to recur. Teaching design patterns to new programmers is even tougher. As Asher Sterkin states, “Teaching design patterns in isolation is similar to studying a foreign language with only a dictionary.” [4]. It is far better to try to teach design patterns using killer examples to help motivate and illustrate each pattern. I propose here something a little more radical: to teach by stealth. With a small number of principles of good program design, and using a running case study that grows in complexity through the semester, we can, through class discussions and exercises, “invent” programming solutions that turn out to be some of the important design patterns. The official names and definitions of the pattern [2,3] are revealed only after the fact, if at all.
Categories and Subject Descriptors
K.3.2 [Computers and education]: Computer and information science education – Computer science education, Curriculum. D.1.5 [Programming techniques]: Object oriented programming. D.3.3 [Programming languages]: Language constructs and features – Patterns.
General Terms
Languages, Design
Keywords
Programming pedagogy, design patterns
1. Introduction
Learning design patterns is hard, even for seasoned programmers. But we’ve seen enough programs to understand their value. It is a far more difficult task to try to motivate and teach design patterns in early programming courses where students have not experienced enough programs to have seen anything repeated significantly. The difficulty is compounded at this university where our CS-1 course is strictly a service course
Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee.
SIGCSE’05, February 23–27, 2005, St. Louis, Missouri, USA.
© 2005 ACM 1-58113-997-7/05/0002...$5.00.
for arts and science students, most of whom are taking their one and only computer science course. And our CS-2 course, while required for our undergrad CS majors, is taken by many students in other sciences, business, or the humanities, who are not going further in computer science.
So I have decided not to cover design patterns per se, but rather to try to motivate the underlying principles using running case studies and to reveal, only after the fact, the identity of the design pattern. I also do not make a big deal of the pattern names so students need not puzzle over, for example, whether what they did was a façade or an adapter, an enumerator or an iterator, or whether the factory object was abstract or not.
In this paper, I outline a series of in-class discussions with companion exercises that introduce students to some of the important design patterns.
The underlying design principles I stress, often with examples from outside programming, are:
- Change happens; expect it; design for it.
- It should be easy to locate the place where a change is needed.
- A change to a single artifact in a program should be able to be made in just one place.
- A change to an artifact should require a minimum of change (or better yet, no change) to the rest of the program.
- If an error is introduced in the change process, it should affect only the thing being changed and not jeopardize other parts of the program.
2. An Analogy
A typical car battery has an expected life far less than the expected life of the car. So (unlike some small cordless electronic devices), a car battery is designed to be replaced. It is easy to unbolt the old battery, install a new one, connect it up, and nothing else needs to be replaced. The only additional changes required are possibly resetting the clock, the radio presets, and the remote entry system. The change is localized and does not require a change to the rest of the system. All but the last of the design principles are illustrated by this analogy. It makes an interesting class discussion to consider how you might protect a car’s various electronic components from an error (connecting the battery cables to 110 volts AC, for example), and the horrendous changes that would be required to replace a car’s 12 volt battery with a battery of a different voltage.
3. Case study
A good case study to use is a simple payroll program.[1] It can start out very simple and then get as complex as you want. I begin with an array of hourly employees, each represented as object of an HourlyEmployee class. Each employee object in the array has a name, number of hours worked this week, and an hourly wage. An employee’s weekly salary is hourly wage times hours for the first forty hours with time and a half after forty hours. The students’ task is to go through the array and generate “paychecks”: a list of each employee, the raw data, and their weekly salary. This is a good first assignment in CS-2 containing everything that they should have learned in CS-1: straight line code, selection, iteration, arrays, classes with multiple objects, and methods. It is also a good refresher exercise for students not coming directly from CS-1. In addition, many of our CS-2 students have waived our CS-1 course based on a high school programming course, making our CS-2 course their first college level computer science course. This exercise is both a refresher for some of these students, and for others who struggle, is a gentle nudge back to CS-1.
Most student programs will have very a simple employee class with only reader and writer methods, with the raw data display and salary calculation done in the main method. This is a trap I allow (even encourage) students to fall into in order to better motivate what’s to come.
Once they have the basic payroll program written, I start making changes and additions. First, I change the overtime threshold to 35 hours. I’ve been temped to make this change an hour before the assignment is due, but I have been afraid that students would view this as cruel and unusual. A properly written program can be changed and retested in less than a minute. But if the program has “magic numbers” embedded in the code, the process could take much longer and be less likely to be correct. The lesson here is to use constants and if the same constant value is to be used in more than one class, then define it once only, and don’t repeat its definition.
4. Expanding the case study
4.1 More Employees
Next, I expand the variety of employees by adding a salaried employee (whose weekly pay is their annual salary divided by 52), and a commission employee (whose weekly salary is gross sales that week times their commission rate). I also add a couple of new properties (employee’s state of residence and number of dependents) anticipating tax calculations later on. We begin in class discussing how to accommodate multiple employee types. The first problem is how to get different object types into a single array. We could use an array of Object and figure out (using the instanceofoperator) the class of each object and retrieve the appropriate raw data and calculate the salary accordingly. Bad idea! Instead, we could create an Employee class with subclasses HourEmp, SalEmp and CommEmp. Then the array would be of Employee descendant objects. But we would still have to figure out the specific type so that salary could be computed correctly. Better, but still messy. A better idea, and an important lesson, is to move the data retrieval and salary computation to the specific employee classes. Then the main method could call common methods, toString() and getSalary(), to retrieve the needed information. Now the responsibility for calculating salary resides where it belongs: with the specific employee. The main method can now go through the Employee array getting the raw data and salary without having to know the type of employees, exactly what data to retrieve, and how to calculate salary. Lessons here are the virtue of polymorphism, and of putting responsibility in its proper place. Also properties and methods common to the subclasses (e.g. name, state, dependents and their associated readers and writers) can be brought into the parent class so that they appear only once. Lesson: avoid repeating things in multiple places; we’ve discovered a bit of refactoring.
4.2 Federal Tax
Next I add Federal tax to the paycheck beginning with a simple 20% of gross salary. Where should the tax be calculated? We already know that it should not be in the individual employee classes. That would create multiple instances of the same code. It shouldn’t even be in the Employee parent class; tax is not a property of employees. So we can create a separate FederalTaxCalc class with a static method to compute tax given the relevant parameters. But why should the employee have to know which of its properties are relevant to the Federal calculation? A better approach would be for the employee to call the tax calculator sending it a reference to itself (this) giving the tax calculator full access to whatever properties it needs. Again, the lesson here is to give full responsibility to the method calculating the tax rather than splitting the responsibility between the employee (knowing what information to send) and the tax calculator (actually computing the tax). This lesson is driven home by changing the tax calculation to include a deduction for each dependent. Students are amazed by how easy this change is added to the tax calculator without any change in the employee methods. In particular, the call to the tax calculator method is unchanged even though additional information is now being used in the tax calculation.
4.3 State Tax
Next I add state tax. For this exercise Istart with only three states, each with its own formula for computing state tax (percent of gross and dependent deduction). One of the states (e.g. Nevada) has no state tax. Students are told to imagine what the program would be like with fifty states, each with its own tax formula. The new tax motivates a discussion of where the state tax calculator should reside and how we should determine which formula to use. We propose (and later reject) the possibility of having a state tax calculator class and a single state tax calculator method (just as was done with the Federal calculator). And just like with the Federal tax, the Employee object would call the state tax calculator sending it a reference to itself. The state calculator would, with a huge case statement, figure out which formula to use and perform the calculation. The problem with this is that to change a single state’s tax formula, we must find the relevant code within a large method, and a careless change could jeopardize all state tax calculations, not just the one being changed. We’re better off having fifty individual state tax calculator classes, all of which implement a common tax calculator interface. This results in a much larger number of classes than before, but each one is very small. If a state tax formula changes, it’s easy to locate which class to change, and errors introduced affect only that state. But now, how to determine which class/method to call? Again, we don’t want a huge case statement cluttering up the Employee class. Instead, we propose a special class with a method that examines the employee object, creates a reference to a new state tax calculator object of the appropriate type, and returns a reference to that object. The employee can then compute state tax with only two lines of code: one line in the employee constructor that gets a reference to an appropriate state tax calculator and one line to call the method to actually compute the tax. And this code is exactly the same, regardless of the state. We have created a factory! And we quickly discover that since the state tax calculator doesn’t store any information specific to the employee, we really need at most one copy of each state calculator: voila, the singleton.
What happens if an employee moves to another state? Easy! Just add a second line to the state writer method to invoke the state tax factory to create (if necessary) and return a reference to the new state tax calculator. This might also be a motivation for an observer that is notified every time the employee object is changed. But this is not a very compelling motivation for the observable/observer pattern.
4.4 Processing the Array
If all the employees got paid on the same schedule, then we could go through the array with a simple for loop.
for (int i=0;i<empList.length;i++)
// Process employee i.
But what if we wanted some times to process only a subset of employees? For example we might want to pay hourly employees every week, and the salaried and commission employees biweekly. We don’t want to change the code every time we run the program in order to choose the desired subset. How can we modify the program so that a program parameter (or value read in from an environment file) specifies the desired subset? We could, for example, specify the desired selection criteria as a set of Boolean flags, one for each employee type. In class discussions we develop the necessary code using either a case statement or a series of if statements. For example,
for (int i=0;i<empList.length;i++)
{ if (employee[i] instanceof SalEmp &
salFlag
|| employee[i] instanceof CommEmp &
commFlag
|| employee[i] instanceof HourEmp & hourFlag)
// Process employee[i]
}
It’s messy, but it gets the job done. But if we have to add another employee type, say, one who earns a base salary plus a commission, we not only have to create a new Employee subclass, but also must add a new flag and a new test to the selection. How can we do better?
As an analogy, think about a busy doctor’s office. The doctor is occupied healing the sick and doesn’t want to be bothered about maintaining the waiting line. An assistant tells the doctor “who’s next” and, at the end of the day, “no more.” We can do the same in our payroll program.
We delegate the list scanning to a new class that provides the main method with only three operations: reset (start the list anew), next (get the next element that meets the selection criteria), and hasMore (returns a boolean that tells if the list contains any more elements matching the selection criteria.) A true value of hasMore is a precondition of next. Now processing the employee list is simple.
while(enum.hasMore())
// Process enum.next() employee
All that needs to be done is to instantiate the desired enumerator object (enum). Again, we have created a bunch of small classes, each dedicated to only a single task. If changes are needed, the place to change is easy to locate and errors introduced are restricted to just that one class. The errors are easy to locate and do not jeopardize other parts of the program.
5. Additional Complexity
I can now go back and add even more complexity. For example, an additional employee type, say, an employee who receives both a base salary and a commission on sales. And I can provide the students with an appropriate class, but one that does not match exactly the employee interface. Students quickly see the value of creating a simple adapter class rather than try to write the new class from scratch. They also see how easy it is to incorporate the new employee type into the existing program.
6. Summary
By the end of this exercise, the students have written about five different versions of the payroll program, each more complex than the previous. More importantly, they have learned a bunch of design patterns. They have learned these patterns not by reading their descriptions, but by developing them as solutions to problems. They may not know the names of the patterns they know, but now when they read the Gang of Four [2], they can recognize some old friends. More important still, they come away with some important programming principles and some experience putting those principles to work.
7. References
[1]O. Astrachan, G. Berry, L. Cox, G. Mitchener. Design
Patterns: An Essential Component of CS Curricula. SIGCSE Bulletin, 30,1 (March 1998), 153-160.
[2]E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design
Patterns. Addison Wesley, 1995.
[3]A. Shalloway, and J. Trott. Design Patterns Explained.
Addison Wesley, 2002.
[4]A. Sterkin. Teaching Design Patterns.
advCPlusProg/Teaching%20Design%20Patterns.pdf
[5]S. Stuurman, and G. Florijn. Experiences with Teaching
Design Patterns. Proceedings of the 9th Annual Conference on Innovation and Technology in Computer Science Education (ITICSE), 2004.
[1] Because of the vastly different backgrounds of the students in our course, we must choose examples very carefully. This is one that everyone should understand.