Recall that we have been designing coffee machine software for Arnold. Our final design looked like this:
All is well. In a few weeks, Arnold returns with a brilliant idea. He has heard that some companies use their employee badges to directly debit the cost of coffee purchases from their employees' paychecks. Arnold's employees already have badges, so he thinks this should be a simple change to the system. And he hates to be behind the curve.
He modifies the hardware to include a badge reader and network connection to the payroll system.
How do you change your design?
It doesn't look like such a good idea now for the front panel to ask the cash box how much money was put in. The coffee machine software ends up knowing the salary of every employee who swipes a card!
But it was never a good idea. The front panel has become an object that knows everything in the system: prices, recipes, and which object to talk to, whatever the user of the machine does. Some people call this kind of object a "God class", because it is all-knowing and all-powerful. The front panel is no longer a collaborator in the problem-solving process; it is a component we have to change no matter what else we have to change.
The front panel does not need to know how much money has been put in, only whether enough money has been put in. Let's move the responsibility for that decision to the cash box, which seems a more natural place for it.
This localizes knowledge of the card swipe to the cash box. We can further guard our users' privacy by having the cash box query the employee database only for the requested amount, not the employee's full available credit.
Our design now looks like this:
We have now implemented three use cases, based on Arnold's growing requests. The result is a better design, as we have used the new requests to make our objects more cohesive.
This can actually be an effective way to design software. We collect a set of use cases and iterate over a design that grows to provide more and more functionality. By considering different scenarios in order, we are able to guide the evolution of the design. Our goal is to make it more flexible and more evenly balanced among the collaborating objects. Many designers find that it is easier to grow a design in this way than to create a finished design from scratch.
All is well again in Arnold Land. But will the peace last?
An ancient Chinese parable goes like this: [...]
Object-oriented programming tries to "vault the wall", to solve the challenges that face software developers by redefining the problem. Instead of asking:
How can I write a program that is structured like the machine on which it runs?
How can I write a program that is structured like the part of the world it describes?
This is a different way of thinking about programs. It encourages us to think about the computer in a very different way: as a simulator of the world. On this view, a program is a blueprint for a simulation, a map of some part of the world that we would like to imitate.
Our program no longer consists of actions on data. Instead, it consists of actors that interact to solve a problem.
One of the benefits of this approach is that the language we use to understand our client's problem can be a useful starting point for the language we use to describe and implement our solution. This was the goal behind the design of the first OO language, Simula, by Kristen Nygaard and his colleagues in the 1960s -- a perspective that unifies all phases of software development: analysis, design, and programming.
Already this semester I have asked you many times, How do we know that our code works?
We have progressed through several stages in our approach to verifying that a program works correctly:
Our first "batch test" example was MemoPad's ArrayDatabaseTest class...
Actually, our Interactions pane approach and batch test approach should also say inspect the results visually, too. Even our small program has enough different requirements that our test has a large number of individual outputs. The test code itself is long enough that it becomes a chore to read and understand. It's also hard to connect to specific parts of the test program to specific parts of the tested code.
Each time we run the test, we must inspect the outputs to be sure that they are correct. This process becomes tedious quickly, and on even the first pass it is error prone. Humans aren't very good at repeatedly doing such comparisons. It quickly becomes tempting to skip checking the output, or even running the tests in the first place. Then we are back to not knowing if our code works at all.
A natural next step in the progression of our testing is to decompose our batch test program into individual cases. This is the sort of simple procedural decomposition we learn back in CS 1. Each helper method is smaller and thus easier to understand. Even better, the methods have names, which can convey information about the test being done.
(In Java, decomposing main() directly requires that we make the helper methods static. What does that mean? why must we do it?)
But this leaves us with a tedious and error-prone visual inspection process, one that we are likely to skip. This is the sort of task that humans are bad at but which programs are good at. The computer scientist's mindset is: How can I eliminate tedious, repetitive, and error-prone tasks by having the computer do them for me?
We can. Take a look at this code I used to test your solutions to Homework 2.
This is JUnit, an OO framework that provides a lot of support for creating and running tests.
This class is different from our others...
... run the tests. Green bar. The result is obvious visually.
What if a test fails. ... change one test and re-run. Red bar! The result is obvious visually.
JUnit provides other kinds of assertion, to allow us to test conveniently specific conditions in our program:
Dr. Java comes with JUnit built-in and provides a lot of support for running tests (Test button, command-T) and writing them (template, with name support).
... create a new test case: BidUnitTests.java.
(There is more... JUnit at the command line. Text and graphical UIs. You can download and install junit as a jar file. Homework 4 will have links and some help resources.)
Automated testing is one of the great advances in the history of programming. It started in the world of dynamic languages, where it was easier to take this next obvious step. First Smalltalk, then Java. It is now available in many, many languages, even not-so-dynamic languages, such as C and Ada.
The idea is simple: Let's make testing as straightforward and as helpful as possible...
We do not have class on Tuesday, September 25.