You are the software developer at the local video store. You have a program that tracks movie rentals and prints a customer statement. The owner comes to you and says:
I'd like to build a web presence. Add the ability to print an HTML version of the customer statement. Pretty soon, we will add a special pricing option for web rentals.
What do you do?
What can we do? The existing statement() method does multiple things: produce a statement as a string, compute the prices for all the rentals, and update the customer's frequent renter points. About all we can do is copy-and-paste. That will work, but it creates a new problem.
The client is already thinking about adding another category of movie and perhaps changing an existing category. Copy-and-paste duplicates a bunch of logic that depends on these types. When the set of categories changes, we will have (to remember) to change all those locations.
This exposes a violation of the Principle of Continuity in our code. A small change in the business model requires a big change in the program.
This a Java program. It works; it passes all of our tests. (Run the tests now.) But... It's not a very good program. It's hard to understand. It will be hard to change.
Your reading assignment for today said, Bring a list of suggested improvements to the code.... What say you?
Based on what we have learned thus far in the course, we can argue that this is not an object-oriented program. Yes, it has three classes. But there are no objects as we have used that term. The program does not consist of independent agents that collaborate to solve a problem. Two of the classes are wrappers for data, what we call records or structures in other languages. The other class consists primarily of a "god" method, which embodies most of the program's knowledge.
After six weeks in CS 2530, we can do better. Let's.
The statement() method produces a statement, as its name states, but it also computes the price of rentals and computes the frequent renter points for the customer. These are different responsibilities. Even before arriving at this course, you learned that different responsibilities should be in separate methods.
Extract the price computation into its own method.
Run the tests.
We notice that we can now initialize statement()'s variable to be the result of the call, rather than 0. We do so, and clean up. Run the tests.
Extract the points computation into its own method.
Run the tests.
Always run the tests. Take short steps; this minimizes the number of errors we can make and the locations in which they can occur. Then run the tests; this will find any errors as soon as possible. No fear!
the resulting code
Our code is already better. statement() is easier to read. It will be easier to change. We can do better yet, by changing some of the variable names in the new methods and eliminating unnecessary variables in the older method. But we have a bigger problem.
The new methods don't belong in the Customer class. They work entirely on data in the Rental and Movie classes. They don't use any data in the Customer class! They belong in one of the other classes.
These methods use data from both other classes. I'm not sure which is the best home for them yet, so let's make the smaller move. A Customer talks to its Rentals, so let's move them there.
Move the price computation to Rental.
Run the tests.
Move the points computation to Rental.
Run the tests.
Notice that this results in less code: no argument passed, and no repeated delegation. Each bit of code occurs once.
We also now have a better balance between the Customer and Rental classes. That is good! These objects are now more equal contributors to the solution. They share responsibility.
the resulting code
We are now implement the owner's request.
(Show the resulting code. No time to code live.)
I use copy-and-paste as my first step, then add HTML tags as desired in the appropriate places. It is simple HTML for now. We can do more later.
Run the tests.
But wait... Those tests can't fail. Noone calls the new method yet. Why run them? It's a habit, and a good one. That way, I never forget top run them when I do need them.
Write new tests.
Run the tests.
As was true when we finished Steps 1-2, the program could be better. Lots better. The two statement-generating methods are nearly identical. We could extract methods for creating the headers, the detail lines, and the footers. That would help.
But this would still leave duplication in the two methods that should trouble us. The control structuee is the same; the code to compute the prices and frequent renter points are the same. How can we get rid of this kind of duplication? (We'll learn a cool pattern for eliminating such duplication later in the course.)
For now, though, life is good. The program could be a lot better, but it is already a whole lot better than it was just a short time ago.
The owner loves the new HTML statement. He feels so... 1998. Now he says:
Don't forget about that special pricing option for web rentals that I want to add. I'm also gonna change the price we charge for new releases, and bump up the frequent renter points for them. I'll have the details tomorrow.
What do you do today?
We need to create a new movie classification, presumably a new named constant. We will need to change computations for an existing classification.
The methods that do these computations reside in a single place now, rather than duplicated in two (the statement-generating methods. But the switch statement in price() will become more complex. We will have to create a new switch, too, in frequentRenterPoints().
This exposes another weakness in our program. Rental is making decisions based on another object's data. That's a bad idea. What happens if we need to change this implementation detail? Remember the Taking Care of Business principle: Every class should be responsible for its own stuff.
There's another elephant in the room. Just like when we designed Arnold's coffee machine.
The video rental store has three different kinds of movies, but only one movie class!
It's not quite true that we have three different kinds of movies. We just have movies. But each movie changes from one price classification to another over its shelf life. It starts as a new release, and later is a regular title. In some video stores, a movie may go from a new release, to "almost new", to "favorite", to regular. Some movies skip a step or two along the way.
A movie may change from one price classification to another, but it is still a movie. In an object-oriented program, an object never changes type. It is what it is.
What we really have are different kinds of categories of movie. Each category has a pricing scheme and a policy for frequent renter points. The next step is to make our categories into objects.
What we want to do is replace the code that uses conditional logic with messages to objects that behave in the way desired. We want "smarter Movies"!
Implementing this step takes a bit more time than we have left in this session, and requires a little Java that we haven't learned yet. Besides, your brain may be swimming from all the changes we have already made today.
So let me just describe at the top level what we need to do:
After each step, run the tests.
We can't make the switch statement go away, but we can be sure that we need only one, no matter where else we decide to do computations. It will be in the method that sets the Movie's price code! We make the decision once, and thereafter the Category objects can just do their thing.
Take a look at our program after Step 3 (or, if you are adventurous, after Step 4). We have done a lot of work, but the program is so much better.
We got all of this by simply applying the principles we have learned thus far in the course.
We spent almost all of this session making the video store software better. Only once did we add new code, the method to generate HTML statements.
This is common idea in industry: improving code without changing its behavior. We call this process refactoring.
Martin Fowler's book Refactoring: Improving the Design of Existing Code teaches how to do this in a wide variety of circumstances. He shows you how to take small, safe steps. With care, we can refactor a program without ever failing a test!
Even without quite so much tedium, we can make changes to our code confidently, taking small steps and running our tests repeatedly. This process makes us feel more comfortable using copy-and-paste. It is often the quickest way to a working solution, and then we can refactor to eliminate duplication.
This is an idea you can use to improve your code throughout the course. But you gotta have good tests.
(I returned Homework 4. Grading was generous, but you had to make an effort... We need more than one test for each method. And if your only test for a method was the free one I gave you, well, that's just not right!)