This week's book giveaway is in the OCPJP forum.
We're giving away four copies of OCA/OCP Java SE 7 Programmer I & II Study Guide and have Kathy Sierra & Bert Bates on-line!
See this thread for details.
The moose likes OO, Patterns, UML and Refactoring and the fly likes OO Calculator -- a Tutorial Big Moose Saloon
  Search | Java FAQ | Recent Topics | Flagged Topics | Hot Topics | Zero Replies
Register / Login


Win a copy of OCA/OCP Java SE 7 Programmer I & II Study Guide this week in the OCPJP forum!
JavaRanch » Java Forums » Engineering » OO, Patterns, UML and Refactoring
Bookmark "OO Calculator -- a Tutorial" Watch "OO Calculator -- a Tutorial" New topic
Author

OO Calculator -- a Tutorial

Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Junilu Lacar:
1. Changing the type of result was not so painful here because there was very little ripple effect. But what about in real-world applications? Changing types could be a lot more painful if there were existing clients. These clients would break and that could cause a ripple effect throughout the system. How do we avoid this kind of effect or at least minimize it?

- What had to be true about our design for the ripple effect to occur?
- How likely is such a design to emerge using test-first-development, how fast would we notice it and how hard would it be to refactor?

2. Do we have to keep old tests around, even if only as commented out code? I would think this would provide some sort of trail for maintenance programmers later on to pick up on the "theory" of the change.

- Do they need a theory of the changes, or rather a theory of the current state?
- Are there more effective ways of getting a theory of the changes, when needed? Perhaps we are already using tools that gather historical information automatically? (Well, I hope you are! )

3. Wouldn't it be also advisable to write tests for failure conditions such as border conditions, lower and upper bounds checking, and such? And what about checking for exceptions we expect to be thrown in certain cases?

- Do you need to write extra code for these conditions?
- If so, how do you motivate this code, if not by a failing test?
- If not, do you feel save without the tests? Remember, one of the main functions of the tests is to give you confidence in your code and your ability to safely refactor it.
Two simple rules:
- If writing tests gets boring, write fewer ones.
- If you find a bug not covered by your tests, reflect on what type of test would have found it and write more of this type.

4. I kind of get the concept of test-first programming but how exactly do I come up with meaningful tests? Are there any guidelines? It seems to me that thinking up what tests to write is just as hard or even harder than just going out and writing the actual code to solve the problem

Well, if you don't know which test to write, how can you be sure you know what the code should do?
But yes, granted, sometimes this is hard. Try to let your tests be driven by the production code. Take a look at it - after all we are writing whitebox tests.
- Does it look complete? If not, what test would force you to extend it?
- What are the conditions you are unsure your code handles correctly or will still handle correctly after a bunch of refactorings?
And don't forget to discuss with your Pair Programming Partner!

5. And even if we are able to think up meaningful tests, wouldn't it have been easier to think up a design first and then think of what tests we wanted? Are we really going faster by going straight to writing tests?

The tests are describing the interface and the behaviour of the code, aren't they? Isn't this design? So, isn't test first equivalent to design first?
Or are you really asking "Wouldn't more upfront design be more effective than this extremely incremental design strategy?"
Kind Regards, Ilja
[ March 16, 2002: Message edited by: Ilja Preuss ]

The soul is dyed the color of its thoughts. Think only on those things that are in line with your principles and can bear the light of day. The content of your character is your choice. Day by day, what you do is who you become. Your integrity is your destiny - it is the light that guides your way. - Heraclitus
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by jason adam:
I too share your sentiment on if this is really as productive as it sometimes is made out to be. Perhaps we're just looking at things in the short-term too much, but if we are to write tests for every single behavior that we expect our design to exhibit, that would just get insane for a really large application. I only see this approach functional if you're working on a small system, or are organized enough to break things into logical subsystems and work those out, and then bring it all together into the larger app.
Personally, I'd rather just write a simple main method with an if/else statement testing a method of a class than bother with some of this.

How well does this approach work for really large applications? How is writing a simple main method preferable to some simple JUnit test cases?
How confident are you in your ability to safely refactor your code?
How much time do you spend debugging your code? How many tests would you have been able to write in the same amount of time?
Curiously, Ilja
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Thanks Ilja
And to answer some of your questions:
What had to be true about our design for the ripple effect to occur?
The extent and magnitute of ripple effect is an indication of how brittle your code is. The pieces may be too tightly coupled. This is a candidate for refactoring anyway so in a (sad, twisted) way, getting a ripple effect may be a good thing.
On the other hand, breaking lots of code while working under tremendous stress is not good either, morale-wise or maybe even career-wise. What you may have to do is compromise. More on this later.
How likely is such a design to emerge using test-first-development, how fast would we notice it and how hard would it be to refactor?
The chances of introducing design flaws is reduced by test-first programming because you are constantly "kicking the tires" and going out for test drives so to speak. Flaws will become manifest quickly as you test every bit of code that you add. And since you are making small changes at each step, refactoring is not as difficult.
-----
Just to make it clear to everybody: what I'm trying to do at this point is to alay some common concerns about test-first programming and to warn you of some of the more common mistakes. By seeing what missteps you can make, you will at least be aware that you are making them if and when you do make them. Hopefully you can catch yourself in time before doing some serious harm to yourself or your team

Common mis-steps:
1. Writing useless tests
1a. Not writing the tests you actually need
2. Taking steps that are too big
2a. Not taking smaller steps when needed
3. Stepping out of rhythm (Green-Yellow-Red or as Kent Beck writes: Green/Red/Refactor)
We will discuss these more as we go along.
Junilu Lacar
jason adam
Chicken Farmer ()
Ranch Hand

Joined: May 08, 2001
Posts: 1932
Originally posted by Ilja Preuss:

How well does this approach work for really large applications? How is writing a simple main method preferable to some simple JUnit test cases?

Well, writing the main method is easier just because I haven't gotten used to JUnit yet. That's one of the biggest problems I'm having right now. I completely see the value of test first, it just seems a lot of upfront work. If I ever actually get used to using JUnit, it will most likely seem like second nature after awhile.

How confident are you in your ability to safely refactor your code?

I'm totally biased in how well I can refactor, because I think I've done a pretty good job in the past, but that's because I'm not in a production environment were my code impacts a lot of people besides myself Again, hoping to learn some nice refactoring techniques through this tutorial.

How much time do you spend debugging your code? How many tests would you have been able to write in the same amount of time?
Curiously, Ilja

Very good points. Get it right first so you hopefully don't have to go back and fix it.
Axel Janssen
Ranch Hand

Joined: Jan 08, 2001
Posts: 2164
I have some problems with this all or nothing approach.
I will have to write test classes for every method. This sounds like a lot of typing, doesn't it?
Have read a article about a project where they used test classes just for the "difficult" part of the system. What about such "dirty XP"?
In the article they stated, too, that junit isn't very good suited for GUI-apps. Is that true?

Axel
constant visitor of tutorial
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

I will have to write test classes for every method. This sounds like a lot of typing, doesn't it?
This will be addressed when we get around to talking about the "Writing useless tests" mis-step.
In the meantime, decide which one of these would give you and, more importantly, your client more bang for your buck:
1) more effort in writing tests or
2) more effort in debugging.
Junilu
(appreciative that some lurkers are voicing out )
[ March 17, 2002: Message edited by: Junilu Lacar ]
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Axel Janssen:
I have some problems with this all or nothing approach.
I will have to write test classes for every method.

A test class typically contains a bunch of test cases (mapping directly to methods of the test class). There is no direct mapping between the number of test cases and methods in production code, though you are right that typical test-first projects have nearly as much test code as production code.

This sounds like a lot of typing, doesn't it?

It also sounds like much less debugging. Moreover, test-first development typically leads to simpler designs, which means less typing (and easier understanding) of production code.

Have read a article about a project where they used test classes just for the "difficult" part of the system. What about such "dirty XP"?

It's better than no test classes at all...
Do you only have bugs in the "difficult" parts of your systems? Then you are better than me - I do make silly mistakes daily...
Overmore, test-*first* is not only a *test* strategy, but also a *design* strategy.
My advice would be: Try it for some time, on every part of your system. Discuss your doubts. After that, decide for yourself what to do. Your decision might surprise yourself...

In the article they stated, too, that junit isn't very good suited for GUI-apps. Is that true?

Yes and no.
Yes, testing GUI isn't easy. There is an extension called JfcUnit that helps a little bit, but not with all aspects.
No, it's very well suited for testing GUI-apps. This is because your GUI should contain as few logic as possible anyway, so that you should be able to test the biggest part of your app without testing the gui at all.
Take a look at http://www.xp123.com/xplor/xp0001/index.shtml for an example of testing a java gui.
Regards, Ilja
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by jason adam:

Well, writing the main method is easier just because I haven't gotten used to JUnit yet. That's one of the biggest problems I'm having right now. I completely see the value of test first, it just seems a lot of upfront work. If I ever actually get used to using JUnit, it will most likely seem like second nature after awhile.

Yes, I think so. And I don't think it is *that* much upfront work. Well, at least *I* got used to it very quickly...
Regards, Ilja
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Junilu Lacar:
Thanks Ilja

You are welcome!

What had to be true about our design for the ripple effect to occur?
The extent and magnitute of ripple effect is an indication of how brittle your code is. The pieces may be too tightly coupled. This is a candidate for refactoring anyway so in a (sad, twisted) way, getting a ripple effect may be a good thing.

I think getting it *as early as possible* would be a good thing. If you get it early enough, it might even not be a sad thing

On the other hand, breaking lots of code while working under tremendous stress is not good either, morale-wise or maybe even career-wise.

Well, probably working under tremendous stress is not good anyway.
I think there are (at least) two vital practices for eleminating stress: managing priorities (ala the XP Planning Game, for example) and providing safety (via automatic testing, for example).
When you eleminated the stress, you possibly have time to think about how to remove the ripple effect without breaking lot of code (that is, in tiny little baby steps).
But how at all would you build a deep reaching dependency into your design, if you'd strive to test your units *in isolation* from the beginning?
Kind Regards, Ilja
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
What should the next test look like?
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Hi all,
Sorry for the long lull there. It's taking me a bit longer than I expected to catch up on my work (you know how it piles up :roll: ) so I'll leave the next step as an exercise for all those following. Post your suggestions and I'll join the discussions over the weekend.
----
In order to organize our thoughts a little better, I'll introduce something from Kent Beck's draft book on Test-Driven Development (TDD): The To-Do List.
The list is simply some reminders of what we want to test. The list for our calculator might initially look something like this:
<pre>
/**
@todo

Init: 0

10 + 5 = 15

10 - 2 = 8

5 * 2 = 10

10 / 2 = 5
Note: don't forget to refactor!
*/
</pre>
Pick one reminder, say 10 + 5 = 15. What test would you write to show that OOCalc works with respect to addition?
Try to write a few versions of tests. With each version, ask yourself the following:
1. Is this the ideal interface to the production code (in our case, OOCalc)?
2. How easy/hard would it be to implement. Can I implement it quickly, albeit dirty, to make it work so I can go on to the refactoring step?
3. What are its implications on the current design and tests? Does it "flow" with the current design?
4. What are other alternatives?
I'll throw one out there for you to consider. Ask the questions above then consider alternative tests.
<pre>
public void testAddition() {
assertEquals(15.0, calc.calculate("10 + 5"), 0.0);
}</pre>
Junilu
[ March 21, 2002: Message edited by: Junilu Lacar ]
Terry McKee
Ranch Hand

Joined: Sep 29, 2000
Posts: 173
I am not sure how to go abou this. I created the test first:
public void testAddition()
{
assertEquals(15.0, calc.calculate("10 + 5"), 0.0);
}
It failed to compile because calculate() doesn't exist. I added the calculate method as follows:
public double calculate(String calculation)
{
return 15.0;
}
I compile both classes, test and get a green light. The problem is how to proceed from here. My thought is I need to parse out the incoming calculation to the calculate method. But I start getting bogged down with the details like:
Parenthesis: "(10 + 5)"
Multiple processes: "5 + 3 + 2"
Combinations of the above processes: "(5 + 3) + 2"
What is the best way to approach this? My thought is to create a parsing class that will separate the various elements of the calculation.
How are other people approaching this?
Terry McKee
Ranch Hand

Joined: Sep 29, 2000
Posts: 173
One other question: What is the difference between using JUnit and the new assertions capability in JDK1.4?
Michael Matola
whippersnapper
Ranch Hand

Joined: Mar 25, 2001
Posts: 1751
    
    2
Originally posted by Terry McKee:
One other question: What is the difference between using JUnit and the new assertions capability in JDK1.4?

I haven't looked at the new assertion capability in JDK1.4, but I understand that a new keyword "assert" has been introduced.
If you check out the Javadocs for recent versions of JUnit, you'll see that the method Assert.assert() has been deprecated in anticipation of 1.4, to avoid a naming conflict. It's been replaced with assertTrue(), which is what the code in this tutorial's been using.
Michael Matola
whippersnapper
Ranch Hand

Joined: Mar 25, 2001
Posts: 1751
    
    2
I'll take a shot at picking up where Terry left off and try to get this calculator to do some actual calculating. I'll start a sidebar thread with several steps from that point and post the final code at the end of that thread.
Note that there's a good bit I don't like about what I've done, but I wanted to take on Junilu's challenge and also keep the conversation going. Also, I've compressed some of the steps a bit.
Note that in the final code I'm using OOCalc2 and OOCalc2Test as the class names. The point is that my stuff is a sidebar version and not a hijacking of Junilu's tutorial. So if people are actually entering and running my code, just make copies of the original OOCalc and OOCalcTest where Junilu last stopped and use that as a starting point.
(By the way, I'll be away all day Saturday, so it'll be a day before I can fend of the oncoming attacks!)
[ March 23, 2002: Message edited by: Michael Matola ]
[ August 11, 2002: Message edited by: Marilyn de Queiroz ]
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Terry McKee:
One other question: What is the difference between using JUnit and the new assertions capability in JDK1.4?

They have completely different targets:
- assertions are embedded into your production code and used to test conditions at runtime
- unit tests are external to your production code, calling small parts of it using defined input and validating the result
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Michael Matola:
Note that there's a good bit I don't like about what I've done [...]

So, *what* don't you like about it?

I wanted to take on Junilu's challenge and also keep the conversation going.

What about the questions Junilu posted?

1. Is this the ideal interface to the production code (in our case, OOCalc)?
2. How easy/hard would it be to implement. Can I implement it quickly, albeit dirty, to make it work so I can go on to the refactoring step?
3. What are its implications on the current design and tests? Does it "flow" with the current design?
4. What are other alternatives?

I would be very interested in your thoughts.
Kind Regards, Ilja
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Terry McKee:
I am not sure how to go abou this. I created the test first:
public void testAddition()
{
assertEquals(15.0, calc.calculate("10 + 5"), 0.0);
}
It failed to compile because calculate() doesn't exist. I added the calculate method as follows:
public double calculate(String calculation)
{
return 15.0;
}
I compile both classes, test and get a green light. The problem is how to proceed from here. My thought is I need to parse out the incoming calculation to the calculate method. But I start getting bogged down with the details like:
Parenthesis: "(10 + 5)"
Multiple processes: "5 + 3 + 2"
Combinations of the above processes: "(5 + 3) + 2"
What is the best way to approach this?

First, relax
Did you ask yourself Junilus questions? Is this really the interface we need/want?
*If* it was, developing an arithmetic parser test-first really isn't that hard (I've already done it, and I was surprised on *how* well it went).
Are satisfied with your implementation for the current test case - that is, returning a hard coded value? Probably not...
So what can we do about it? Kent Beck describes two ways in his book-in-work (well, in fact three, but the third - "Obvious Implementation" - is a little bit dangerous for beginners... ):
Triangulation
We think about the easiest test that forces us to *not* return a hard coded value, for example
assertEquals(14.0, calc.calculate("10 + 4"), 0.0);
The two test-cases in combination force us to do at least *some* parsing - notice that only the "4" is different from the other testcase.
So probably we needed an additional testcase forcing us to parse the first operand. The next could force us to parse the operator. And so on...
The advantage of Triangulation is that it provides really tiny little steps, that can be taken in a nearly mechanical way (once you got used to it).
There are two disadvantages:
- In perpetuity it gets boring and
- It doesn't *really* force us to not use hardcoded values - we could use if-statements to decide which result to return, for example
Fake It Till You Make It
The second technique is to see that the hardcoded value duplicates data already contained in the parameters (for example), forcing us to factor out the duplication:
return 15.0;
we notice that somehow 15.0 is intended to be the same as "10 + 5", so it gets
return 10.0 + 5.0;
gets
return firstOperand(calculation) + secondOperand(calculation);
gets
return operator(calculation).operate(firstOperand(calculation), secondOperand(calculation));
The disadvantages of this technique:
- Sometimes it is really hard to see the duplication
- Test coverage is not as good as with Triangulation (how do you know if it is sufficient?)
Kind Regards, Ilja
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Michael and Terry get "A"s for effort
Ilja is officially promoted to TA (Tutorial Assistant)
Thanks guys! Michael, you saved me a lot of work by showing one possible design path. I have to admit that you are much braver than I am though to actually try to go down the path. I was at exactly the same point that Terry was: asking myself "Now what? ".
Personally, I just didn't feel that going down the parsing road would be something that I could easily implement. It would have been a little too much work.
Besides that, I asked myself if calculate(String) was really part of the interface I wanted and, after some reflection, I answered "No". Our first refactoring was to change a String to a double. The reason for doing that was to keep the calculator true to its nature by dealing directly with numbers instead of having to do a lot of conversions. Now I was about to introduce a method that would again require me to do conversions (parsing then convert to double). Been there, done that, don't wanna do it again.
So what did I do? I thought up another interface and changed the test altogether.
I still have a few more hours of work I have to catch up on so I'll see you all tonight and I'll post my code then (or maybe someone will have already done so and save me the work )
Keep up the good work, guys!
Junilu
Michael Matola
whippersnapper
Ranch Hand

Joined: Mar 25, 2001
Posts: 1751
    
    2
Hey, it was 2:30 am local time when I posted this. Had to get some sleep.
To respond to Ilja's first question --
What don't you like about it?
My main objection is the interface. It's decidedly uncalculatorlike. I think my first step of trying to separate out input from result getting in testAddition() was a step in the right direction:
calc.calculate( "10 + 5" ) ;
assertEquals( 15.0 , calc.getResult() , 0.0 ) ;

But it didn't go nearly far enough. How about

Maybe reset() should have its own test:

Gotta run.
[ March 23, 2002: Message edited by: Michael Matola ]
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Michael and I seem to have come up with almost the same idea except that I just don't want to deal with Strings right now because it feels like we're thinking about the interface too soon again. We're still making the circuit board here. We'll get to the faceplate later.
So now I'm forming a picture of a layering scheme in my head. Here's what it looks like, roughly:
User --> (Input translation) --> (Calculator)
Now, I know enough about how a calculator works to anticipate that at some point I'll have to implement a Finite State Machine somewhere. The FSM will have at least the following characteristics:
States:
1. Initialized
?
Input:
1. digits 0 - 9
2. decimal point '.'
3. operators: +, -, *, /
4. =
?
Transitions:
?
Output:
?
Sure, the idea is still a little cloudy but from what I can see of it, it seems to fit well with the layering theory. The FSM will probably be in the (Input translation) layer and it will hook to the calculator "circuit board" through its outputs. That is, when the FSM has an output, that output will go into the calculator as input.
With that theory in mind, here's what I came up with as the next test:
<pre>
public void testAddition() {
calc.in(10.0);
calc.in("+");
calc.in(5.0);
assertEquals(15.0, calc.getResult(), 0.0);
}
</pre>
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Before any troublemakers object to the introduction of the FSM theory (and I'm not implying anybody in particular, Ilja ) let me explain:
Some of you may have biases from past experiences. These biases may or may not affect how you approach the problem. I have a little background in Computer Science so I know a little about FSMs and Automata (note the emphasis on "little") so I'd like to see where this knowledge leads me.
I'd also like to learn something from this exercise. I would encourage everybody following this discussion to go with their own bias and see where it takes you. There are many possible solutions to this problem and it would be interesting to see the different ones that we produce and compare notes.
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Junilu Lacar:
Now, I know enough about how a calculator works to anticipate that at some point I'll have to implement a Finite State Machine somewhere. The FSM will have at least the following characteristics: [...]

We will see...

<pre>
public void testAddition() {
calc.in(10.0);
calc.in("+");
calc.in(5.0);
assertEquals(15.0, calc.getResult(), 0.0);
}
</pre>

I think Michael is right that we already can (and should) test intermediate results. We can even use this to go in smaller steps:
<pre>
public void testValueIn() {
calc.in(10.0);
assertEquals(10.0, calc.getResult(), 0.0);
}
public void testSimpleAddition() {
calc.in("+");
calc.in(5.0);
assertEquals(5.0, calc.getResult(), 0.0);
}
public void testFullAddition() {
calc.in(10.0);
calc.in("+");
calc.in(5.0);
assertEquals(15.0, calc.getResult(), 0.0);
}
</pre>
Now I have to prepare for an appointment. It would be great if somebody took on to make the tests run - one by one, of course.
So long!
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112
Originally posted by Junilu Lacar:
Before any troublemakers object to the introduction of the FSM theory (and I'm not implying anybody in particular, Ilja )

I swear I didn't see this post before I wrote the above...
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8


I swear I didn't see this post before I wrote the above...

So, you admit to being a troublemaker, eh?
Step 8
OK, the reason I'm still not bothering with interim results is that it feels to me like it's because we are still thinking about the user interface. Interim results lets us see state of the calculator as it processes each piece of input. This feels like it should be implemented in the (Input translation) layer I had in mind.
Anyway, here's what we did:
1. Added testAddition() - failed to compile (Yellow)
2. Added stubs - tests compile but fail (Red)
3. Quick & dirty implementation (Green)
You'll notice in that in step 3 above I have again introduced my bias. Whereas Michael used separate doubles to store the operands, I have taken a different (perhaps bigger) step because I remember using a Stack when I did something like this before.
Now let's try to clean up a little. Time to refactor.
[ March 23, 2002: Message edited by: Junilu Lacar ]
[ August 11, 2002: Message edited by: Marilyn de Queiroz ]
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Step 9 - Refactoring OOCalc
That "if" in OOCalc.getResult() doesn't smell good. When we start adding operations like subtraction, we'll have to modify OOCalc. This would violate an OO design principle called the Open-Closed Principle (OCP) which states that "Software entities should be open for extension, but closed for modification."
While it may seem too early in the game to even be worried about violating this principle, it's always good to keep it in mind. I am going to let my knowledge of the OCP drive my next decision to refactor.
Instead of having
if ("+".equals(operator)) { ...
}
I want getResults to launch the current operation into action, whatever that operation may be. Something like:
op.perform(...);
where "op" is the last operator received by the in() method.
op will obviously need access to the operands, which are stored in operandStack, so it seems we'll need to pass that to op.
I'm not sure if this is a documented refactoring but I'll call it "replace test with polymorphism" for now. So instead of getting:
<pre>
if ("+".equals(operator)) {
...
} else if ("-".equals(operator)) {
...
} else if ...
}
</pre>
which is what we would have had if we went on without refactoring and added quick and dirty solutions for our next increments: subtraction, etc. Instead, we'll have something like:
<pre>
if (operator != null) {
result = operator.perform(operandStack);
}
</pre>
which we will no longer have to change as we add operations.
Gotta run now. I'll step through the process of getting to this point tonight.
[ March 23, 2002: Message edited by: Junilu Lacar ]
Ilja Preuss
author
Sheriff

Joined: Jul 11, 2001
Posts: 14112

So, you admit to being a troublemaker, eh?

I am making it a virtue!


You'll notice in that in step 3 above I have again introduced my bias. Whereas Michael used separate doubles to store the operands, I have taken a different (perhaps bigger) step because I remember using a Stack when I did something like this before.

The Stack is not what bothers me. I probably had used some form of List, too.
But do we need the if-statement yet? I don't think so...
Michael Matola
whippersnapper
Ranch Hand

Joined: Mar 25, 2001
Posts: 1751
    
    2
Originally posted by Junilu Lacar:
I'm not sure if this is a documented refactoring but I'll call it "replace test with polymorphism" for now.

"Replace Conditional with Polymorphism" or "Replace Type Code with State/Strategy" seems to be where this is headed. (It's a little bit of a stretch to think of + , - , etc. as "type codes," but not too much of a stretch.)
Michael Matola
whippersnapper
Ranch Hand

Joined: Mar 25, 2001
Posts: 1751
    
    2
Also planning to use a stack for operators at some point?
Bryon Phinney
Greenhorn

Joined: Aug 17, 2001
Posts: 13
I've been following along as time permits (and have finally caught up).
Following Junilu's suggestion (my interpretation anyway) I have created an Operator interface - not sure it should be an abstract class at this point. I then created a PlusOperator class implementing the interface.

User --> (Input translation) --> (Calculator)

A question I have is will the "Input translation" know about the Operator objects. Restating - should there be a OOCalc.in(Operator operator) method?
[ March 23, 2002: Message edited by: Bryon Phinney ]
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

It's really encouraging and exciting to see that you guys are catching on to the theory I'm forming even at this early stage (I know, two pages of posts seems long but if this were being done in real time we'd be here in 10 minutes, barring digressions into the philosophy of programming).
Ilja, I think I know what you're getting at about not needing the "if" yet and that's actually what I coded first but then the testInitialResult() failed with a NullPointerException. So I needed to put some sort of condition around the addition calculation code. I guess I could have written "if (operator != null)" too but I just went with the first thing that popped into my head .
Picking up again, we'd like to apply the Replace Conditional with Polymorphism refactoring to that if statement. However, doing so seemed like too big a step at this point so I add it to the To-do list. Besides, Martin Fowler's advice in his book is to wait until the third time to refactor.
There are other things we can fix though. How about that result field? Somebody asked why we even have it. And what about the literal "+" for the operator? Does anybody else feel like it should be symbolic constant?
I'd like to delete the result field and put this code:
<pre>
if (operator == null) {
return 0.0;
}
else {
double d1 = ((Double)operandStack.pop()).doubleValue();
double d2 = ((Double)operandStack.pop()).doubleValue();
return (d1 + d2);
}
</pre>
Also, I'd like to add a symbolic constant ADD = "+" and change the test to use the constant. That will eliminate the duplication (the literal "+" in OOCalcTest is a duplication of the literal in OOCalc)
Thoughts / objections / suggestions ?
[ March 26, 2002: Message edited by: Junilu Lacar ]
David Weitzman
Ranch Hand

Joined: Jul 27, 2001
Posts: 1365
Jeez, I haven't been to JavaRanch in months and I almost miss something interesting! First of all getResult() should either have its name or innards changed (is it an accessor or a mutator?). Not the biggest of complaints, but I like to fret over details. Also, TestOOCalc should reset() the calculator's state before every test. More minor details -- my favorite. I'll stop here in accordance with the rule of baby steps.
Oh yeah, and I don't think you should drop the result field. I guess thats what my first suggestion above was about. A calculator really does have a state, and eliminating the result would eliminate the state. Maybe 'result' could be renamed to 'value' to seem more purposeful.
[ March 24, 2002: Message edited by: David Weitzman ]
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Welcome back, stranger
OOCalcTest does reset calc for every test. The JUnit framework makes sure that the setUp() method is called before each testXXX() method.
And you're right about the calculator state but there just doesn't to be enough justification to keep result at the class level since it's really only used as a temporary variable. Besides, I'm not really sure at this point that "result" correctly reflects the state of the calculator; it seems that the operandStack and operator have more to do with its current state.
I kind of agree with wanting to rename getResults() to something else though but we'll leave that for later when the code starts begging for another name.
Good to see more people joining the thread
David Weitzman
Ranch Hand

Joined: Jul 27, 2001
Posts: 1365
setUp() is called before every test. Very cool. I'd thought it just runs once. I probably should have read the manual.... That's the sort of thing you feel kind of stupid for not knowing
David Weitzman
Ranch Hand

Joined: Jul 27, 2001
Posts: 1365
So the result of some math will just be left alone on the stack? Sounds good to me. I was thinking of the stack more as a temporary place for holding calculations-in-progress, but it's probably best to hold everything there. I vote to change getResult() as you suggested, only a little different. How about this:
<pre>
if (operator == "+") {
double d1 = ((Double)operandStack.pop()).doubleValue();
double d2 = ((Double)operandStack.pop()).doubleValue();
return (d1 + d2);
}

return 0.0;
</pre>
This keeps in mind the purpose of the code.
It can be kind of frustrating not to make larger changes! I'm also purposely leaving in a error that the tests should catch later if there aren't many more good refactorings.
[ March 24, 2002: Message edited by: David Weitzman ]
Terry McKee
Ranch Hand

Joined: Sep 29, 2000
Posts: 173
David,
I am pretty new to this, but I see a couple of things wrong. First, operator == "+" will never work. Perhaps you meant operator.equals("+")? (Also, in a previous message the "+" has become a symbolic ADD variable.
Second, I am by no means an expert, but it seems to me that having two return statements is not a good idea. I really believe that OO code should build on Procedural code. That means in this method there should be one entrance (the method call) and one exit (the return statement).
Any thoughts?
David Weitzman
Ranch Hand

Joined: Jul 27, 2001
Posts: 1365
The == error was from directly copying the code Junilu posted above, and was the error I was going to let the tests catch. Ah well.
As for the symbolic ADD, it hasn't been put in the listing yet so I didn't want to use it. Is it ok to make changes that assume things about the listing?
I've never been a fan of the one entrance and one exit philosophy, but I do see where you're coming from. You can stick in an else statement at the end I suppose to please everybody. Then you're commiting yourself to a lot of chained else's, which I think is harder to read, but that's just my own opinion.
What I was trying to say with my code was that Junilu's code above didn't include any conditionals based on the '+' sign, so the code didn't say what it meant. I probably should have quoted him to make the comparison easier.
Junilu Lacar
Bartender

Joined: Feb 26, 2001
Posts: 4782
    
    8

Thanks for the suggestions! Here's what I ended up doing though: Step 9 refactoring
Thoughts?
[ August 11, 2002: Message edited by: Marilyn de Queiroz ]
David Weitzman
Ranch Hand

Joined: Jul 27, 2001
Posts: 1365
I think this would be a good time to add a few more operations. That should set up OOCalc for some bigger refactorings. Plus it would be really simple.
David Weitzman
Ranch Hand

Joined: Jul 27, 2001
Posts: 1365
Also, how about expanding the addition tests so that 5 + 5 + 5 works?
<pre>
public void testAddition() {
calc.in(10.0);
calc.in(OOCalc.ADD);
calc.in(5.0);
assertEquals(15.0, calc.getResult(), 0.0);

// add more
calc.in(OOCalc.ADD);
calc.in(5.0);
assertEquals(20.0, calc.getResult(), 0.0);
}</pre>
 
jQuery in Action, 2nd edition
 
subject: OO Calculator -- a Tutorial