Introduction
In this tutorial, you will develop a Reverse Polish Notation (RPN) calculator. According to Wikipedia, PRN was developed by 1920 by Polish mathematician Jan Lukasiewicz, so the notation has some history. The purpose of this tutorial is to give you practice with BDD on an object with methods, rather than an object with one method, as in the first tutorial.
As with the first tutorial, this tutorial covers:
- The three laws of TDD
- Continuous refactoring
- Basic continuous integration
Unlike the first tutorial, this tutorial will:
- Spend more time discussing a few more of the S.O.L.I.D. principles,
- Refactoring to patterns
- Opening up classes using Ruby’s Meta-Object Protocol
By the end of this tutorial, you’ll be ready to start using story tests on a more complicated problem.
Note
This tutorial assumes you’ve either already worked through the first tutorial or are familiar with BDD and RSpec. If not, here are a few terms that come up early enough you’ll need to know them:
- Example - in RSpec parlance, this refers to a series of steps describing a use of the system. Before writing production code, you’ll write an example. That example will not run most of the time, so you’ll then create the production code necessary to get the example to pass.
- Context - A context forms a common environment under-which Examples execute. A context may just be a name or it might include common setup and initialization as well as common clean-up. Contexts can be nested, which was demonstrated in the first tutorial.
Approach
Unlike the first tutorial, this tutorial will be a bit more explicit in its steps. Where reasonable, each Example will follow the same flow:
- Title: Proceeded by a solid line, this will be at Heading Level 1
- Overview: The overview immediately follows the title
- Example: The Example be in a fixed-point font with different background.
- Check-in: As with the other tutorials, getting you to practice frequent checkings is a secondary goal so there will be not-so-subtle reminders along the way. If you find your tools don’t support such a working style, that’s valuable information as well.
- Refactor: There’s almost always room for improvement, so the tutorial will review the work it just had you do and make suggestions as applicable. These refactorings are generally of the seconds-to-minutes variety. If an example requires a more significant restructuring, then it will be an entire sub-section in the Example.
- Check-in: After refactoring, checking in again.
- Summary: A synopsis of anything new or significant
There will also be occasional side-bars on an as-needed basis.
Before you get started on any Examples, however, we’ll delve into the problem just a bit.
The Problem
An RPN calculator works by performing calculations on operands that the user has already entered. Here is an example scenario:
In this example, a user enters first 5 and then 3 and hits the + key to get the previous two operands 5 and 3, added together.
In general, using an RPN calculator obviates the need to use parenthesis. Consider the following:
That same expression could be calculated on an RPN calculator as follows:
An RPN calculator as a stack of previously entered values. The most recently entered numbers are the numbers that become available first (Last In First Out - LIFO). Consider this longer sequence:
Here’s the evaluation of the above expression:
- 3, 6, -8 get places on the operand stack
- 8 is placed in “working storage” or in HP terminology, the x register
- +, a binary operator, takes as its left hand operand the top item on the stack, -8 and as its right hand operand the x register, 89. The result, 81, is placed in the x register
- Next, sqrt, a unary operator, takes the value on the x register and calculated 9, which is then put into the x register
- The first -, another binary operator, takes as its left-hand operator the top value on the stack, which is currently 6. It takes as its right-hand operator the x register, which is 9. The result, -3, is placed in the x register
- The first -, another binary operator, takes as its left-hand operator the top value on the stack, which is currently 6. It takes as its right-hand operator the x register, which is 9. The result, -3, is placed in the x register.
- The final operator, +, another binary operator, takes as its first operand the top item on the stack, which is 3. For its right-hand operand, it takes the x register, -3. Adding, the result is 0, which becomes the value in the x register.
- The stack is empty.
The x register
The x register behaves like an accumulator. As you type numbers, those numbers get added to what is already there. When you type a non-number key, it applies itself. For example, the
- If the next key is part of a number, the x register is reset and the key pressed becomes the contents of the x register.
- If the next key is not part of a number, then it applies as is.
After any non-number key, the x register is “locked” and any attempt to change it results in it being reset.
Getting Started
There are several major parts to this problem:
- Handling input
- Handling the stack and the x register properly
- Processing the basic operators (+, -, /, *, ^)
- Handling more complex functions (e.g., prime factors, sum over the stack)
In addition, we’ll add several things to make this a bit more interesting:
- CRUD’ing (Create, Read, Update, Delete) variables
- CRUD’ing user-defined functions
Behavior Driven Development advocates starting outside in; start with the user experience and then work your way into the system. In this tutorial, you will not strictly be creating a user interface; you will, however, create the stuff necessary to build a user interface. The tutorial will start at the individual entry of keys and work up to basic math.
Your Work Area
- Create a directory to store all of your work. Unlike the first tutorial, you’ll be working in multiple files this time.
The 0th Example
As with all problems, you need to pick a starting place. As with the first tutorial, you will create a basic example describing object creation. Next, you’ll continue with a series of examples centered on basic input. Thinking further ahead at this point might constitute analysis paralysis.
Example
Here is a first Example for this problem:
- Create this Example in your directory, call the file
rpn_calculator_spec.rb
- Execute the Example, it fails:
The -c option adds color to the output. You won’t see the color in this tutorial. Also, if you are running this in a DOS terminal window, then you’ll want to leave this option off.
The message describes an unknown constant, RpnCalcualtor. You’ll need to create that class.
- Create the following Ruby class in a file called rpn_calculator.rb:
- Update rpn_calculator_spec.rb to require the file you just created:
- Run your Example again, you should be all green:
OK, you have an Example but it does not contain any verification. So one more change and one more execution.
- Update the Example to validate calculator:
- Run your Example again, you should be all green:
Check-In
You are all green, so it is time to check in your work. As with the previous tutorial, I’ll be using git:
Note that in this example I have a file you will not have, wikiwriteup.txt. That’s the raw material used for format the wikipage you are now reading.
Refactor
A quick review of the existing Example and production code does not suggest any refactoring yet.
Check-In
Since there was no need to refactor, there’s no need to check in again.
Summary
You created two files:
rpn_calculator_spec.rb
rpn_calculator
This may seem like a trivial start, and that is by design. What have you verified so far:
- You have a working environment
- The RSpec gem is installed
- Ruby is intalled
- You have the basic files in place
- You’ve checked your work into a repository
Not every Example will be so small and quick to create, but you should attempt to break your work down into bite-sized chunks like this. Why?
- If you get distracted, you don’t lose very much ground.
- If you mess up and need a do-over, you don’t lose too much work.
- If someone needs your attention, you’re not too far from having a great place to stop what you’re doing.
- You can feel a sense of accomplishment throughout the day.
- You can watch as your system organically grows from something trivial, to something complex, typically quickly.
Example: Accepting User Input
With the first Example running it is time to move on to basic user input. We have to make a decision already, where is the system boundary. Here are a few possibilities:
- The system receives a series of events for buttons pressed, e.g., pressing “9” on the UI generates a 9-button pressed event, pressing “sqrt” on the UI generates a sqrt-button pressed event.
- The system receives “digit” messages for digits or “function” messages for functions.
- The system receives messages such as a complete number, enter, + and so on.
There are other alternatives, and to some extent, the selection is arbitrary. There is no decision on the UI technology there’s no way to know the eventing mechanism, if applicable. Even so, you can make progress based on some logical representation and then adapt between the real UI technology and the system you’ve developed.
A standard model for handling user input is the Model:View:Control pattern. In this case, the View is the as-yet-to-be-determined presentation technology. The Model is the calculator itself. The control part of the equation maps between the view and the model (the UI and the calculator). So you will create the model, drive the system using RSpec as the de-facto control, and when you’ve selected the UI technology, you’ll create a new control to map from the UI to your well-tested model.
As a side benefit, most of the logic will be in easy to test code.
Since the particulars of the UI are irrelevant, this tutorial will move forward using option 2 listed above for handling numbers. When it is time to handle things like +, /, etc., it’ll be time to make another decision.
Example
Here is the beginning of an example:
There’s more to be written, but as soon as the message “digit_pressed” is sent to a calculator, the Example will fail.
-
Create this Example, adding it to rpn_calculator_spec.rb.
-
Execute the Example, you should see an error similar to the following:
Assuming you worked through the first BDD tutorial, this is familiar ground. You need to add a method to get this to compiled (you’ve just finished applying law 2 of the 3 laws of BDD).
- Update RpnCalculator:
-
Verify that your Example now executes successfully.
-
Complete the Example:
- Execute your examples, one will fail:
As with the previous failure, this failure is a result of a missing method.
- First, create the method in RpnCalculator to get your code to compiling:
- Run your Example, make sure it is failing:
- Now, do the simplest thing that could work, AKA fast-green bar, AKA, Uncle Bob’s TDD rules, #3:
- Finally, run your Examples to make sure they are passing:
Check-In
- Check in your work:
Refactor
Review of the RpnCalculator class does not suggest any refactoring.
Review of the Examples, however, does. There is some minor duplication. Both Examples create a calculator. This is a quick fix since you can create a containing Context with common setup. As with any refactoring, however, you’ll take small steps and verify as you go along.
- Create a containing context: Above the first describe
After the last end
Update the spacing - that’s one command in VI, the best-ever editor
- Verify that your Examples still execute:
- Add a common setup to your outer-context:
-
Verify your Examples still run.
-
Update the first Example to use the initialized calculator:
-
Verify your Examples still run.
-
Update the second Example as well:
- Verify your Examples still run.
You might be tempted to do more at this point. For example, you know you’ll be adding a lot of values to the x_register. However, let that generalization happen naturally. Practicing both BDD and TDD encourages bottom-up, example-based generalization.
Check-In
- Check in your work:
Summary
The first example added basic object creation. This Example defined some of the API for entering digits. The production code doesn’t actually process this yet, however, you already have executable documentation of how the API is supposed to work.
You also performed some basic refactoring on your Examples. While mentioned in the first tutorial, here are a few observations:
- You want to get to the point where your muscle memory is imbued with this kind of basic cleanup. The only way to get there is practice.
- We removed a violation of the DRY principle. I believe Ruby Dave Thomas is credited with saying all design is an exercise in removing duplication. Regardless of who said it, I think it’s a sound idea.
Example: Taking a decimal point as well
The production code is hard-coded. Somehow you want to push your solution to remove that hard-coded value. One way to do that is to and another Example (don’t do this):
On the one hand, this will force some more work in the production code. On the other hand, this Example is a proper superset of the first Example. That is, this Example completely covers the first Example, so, pragmatically, you should remove the first Example. More Examples does not necessarily mean better Examples. Each Example should push the solution a little further and avoid duplicating the work of another Example. Why?
- More examples –> longer to run –> run less often –> less effective
- When something breaks, you are likely to break more than one Example –> more maintenance –> Examples become a hassle to maintain –> Examples are commented out or left not passing –> nearly defeats the purpose of BDD.
Example
Here is another example that fits extends the functionality just a little but will also force the implementation of the production code:
-
Create this Example.
-
Run your Examples, you should have one failure:
Now it is time to do something more than hard-code the response.
- Add an initialization method to your RpnCalculator class:
- Update the digit_pressed method to record the digit:
- Update the x_register method to use the recorded input:
- Run your Examples, you should be all green:
Check-In
- It is once again time to check in your code. Everything is green:
And as a reminder, the reason there are so many insertions in this example is because my checkins include updates to the source file used to generate the page you are reading.
Refactor
Reviewing the RpnCalclator class, there’s little to do right now. What about your Examples? There appears to be duplication in that there are several lines that call the digit_pressed method. Is this duplication? It does represent the underlying objects’ API. Even so, how can you improve this?
Before making any improvements, you need a reason to make the improvement. It is to make the Example more readable, easier to write, easier to maintain, or some other characteristic?
How about allowing your Example to provide a number, which is then “typed” for you?
- Add a method to the containing context:
-
Run your Examples, they should still pass.
-
Update the
Handling User Input of Numbers
example:
-
Run your Examples, they should still pass.
-
Update the
should handle a string of digits with an embedded
. Example:
-
Run your Examples, they should still pass.
-
Finally, the second Example does not start with should, which is a pretty well-followed practice. So fix it:
- Run your Examples, they should still pass.
Check-In
- Check in your changes:
Summary
Congratulations, you have basic support for processing numeric input. You managed to use an Example that pushed the design just a bit and ended up with code in each of the methods.
There is one strange thing, your code uses “digit_pressed” for the digits 0 through 9 and ‘.’. That’s something worth considering.
Here is a snapshot of what your code should resemble at this point:
rpn_calculator_spec.rb
rpn_calculator.rb
Example: What Happens with Enter?
What happens when a user presses the
- The current value in the x_register is placed on the stack of previous operands.
- Subsequent, the next numeric digit causes the x_register to reset, but pressing
<enter>
again causes that value to placed on the stack again.
In fact, as you will see, pressing a non-numeric key generally “locks-down” the x_register and makes the next digit cause it to be reset. You can describe this we several Examples.
Example
Here is a Context with three as yet to be defined Examples:
-
Add this context as a sub-context of the RPN Calculator context.
-
Execute your Examples, you should notice that these three are listed as pending:
Before you can write any of these, you have to make a decision about how to indicate that the
- Add a method, enter_pressed.
- Add a method, execute_function(:enter)
Is there a clear winner? There are trade-offs:
- The first option suggests a “wide” API. That is, the number of methods on the PN Calculator grows as its functionality grows. This might sound “normal” but it could be a violation of the Open/Closed Principle.
- The second option requires a symbol for each function.
- The second option requires the RPN calculator to map from a symbol, :enter, to a behavior.
This tutorial will take the second approach.
- Add to the first Example (make sure to add the ‘do’ after the first line):
Why stop here? This Example is now failing because it invokes a method that does not yet exist.
- Run your Examples to verify this:
- Get the example back to Green by adding a method in RpnCalculator:
- Continue with the Example:
This both completes the Example and causes it not to work because the top method does not exist. You can verify this by running your Examples.
- Add a top method:
Did you notice I just had you “cheat”. You should have:
- Created a method that would cause the Example to fail.
- Run your Examples.
- Updated the method to return 654
- Run your Examples. Did you notice that? If not, see how easy it is to slide backwards?
Check-In
You have all passing Examples, but two are not implemented. Should you check in your work?
- Sure:
Refactor
A quick review of RpnCalculator doesn’t suggest a need for refactoring. For now, the same can be said of the Examples.
Check-In
No need, there was no need to refactor.
Summary
Do you think pressing
Have we made progress? Yes, we have extended the API of the calculator. What about checking in code with pending Examples, is that a good idea? I think it is OK, but it could cause problems. I prefer clean Example execution, so having several pending Examples is a bother. On the other hand, if I’m pairing with someone and one of us thinks of something worthy of validation, but it is not where we are working, we could write it down, email it, yell it to another team member, or we can put it in the code in an executable fashion.
Example: Checking that the x_register is reset-able
The next Example involves what happens after the
Example
Here’s an example that seems to express the idea:
- Create this Example and then run you Examples:
Notice that the 1 entered was appended to the x_register. So looks like this test pushes our current production code. You might also have observed that the support method named “enter_number” is really a bit off. Since
- Update the calculator to work with the new Example:
- Run your Examples, they should pass:
Check-In
- Now is a good time to check in, even though you have a pending Example:
Refactor
Here’s a list of things that you might consider for refactoring:
- There’s a violation of DRY between digit_pressed and initialize
- There’s some duplication between the last two Examples
- The method name enter_number either needs to be changed, or it should call the
<enter>
method.
enter_number
You can either change its name or make it do what it says. Flip a coin, you probably do not have enough information to make that decision just yet. However, in the spirit of keeping things clean, I did make a decision. I kept the method name and changed its implementation:
- Run your Examples, nothing is broken.
Violation of DRY between digit_pressed and initialize
This is one of those cases where duplication might be OK because right now initialize is simple but it is likely to get more complex later. I was originally going to change the implementation to this (don’t do this):
However, before I made that change, it occurred to me that coupling to the initialize method will probably cause problems later. Even though I’d catch that when Executing my Examples, I instead decided to introduce a simple method that documents the intent.
- Add a new method:
-
Verify that nothing broke.
-
Update initialize and digit_pressed to use this new method.
-
Verify that nothing broke.
Duplication in last two Examples
When you changed the implementation of enter_number to actually perform an
When these Examples reflect the update to the enter_number method, the duplication does not see to be bad.
Check-In
- Check in your work.
Summary
The behavior of the calculator has grown. The it also is beginning to handle the
- However, here’s a principle from Jerry Weinberg:
- Nothing + Nothing + Nothing == Something
If you are not maintaining your code and constantly cleaning it up, then it is decaying. Even if your code is not changing, it is decaying because the market is changing, the users’ understanding of your system is changing, something is changing.
Keeping your code clean is not an event, it’s not a processes, it is an attitude. You have it or you do not have it.
- Are you going to be perfect?
- Perfection is not a destination, it is a journey.
No, you are going to miss stuff. That’s why when you do find something, now is the time to handle it. If you cannot deal with it now, write it down on a piece of paper. Throw that piece of paper away in 2 days. If you haven’t found the time to fix it by then, then by keeping the paper, you’re just giving yourself undue stress.
Example: Should allow x_register to be entered again
This one has been pending. While I was just about to get to that, a few ideas went through my head:
- There’s going to be a stack of numbers somewhere.
- Once the calculator starts resetting, the code never stops, so the Examples have left work on the plate.
Those two items are officially on the punch-list. For now, let’s get back to all green. We’ve been away from all green for too long.
Example
- Create the following example:
- Run your Examples:
Well everything is all green. So this Example might not be adding value. Rather than immediately delete it, keep a note to review it after completing the first two items on the punch-list.
Check-In
Should you check in? You might not be sure about the Example, but you’ve got that to review after just a few more Examples. Even so, go ahead and commit. If you remove the Example later, your RCS tool can handle the workload.
- Commit:
Refactor
There are some duplicated validation steps in the Examples. Before refactoring, however, keep things as is until you’ve done just a little more work.
Check-In
There’s nothing to check-in right now.
Summary
You’ve added an Example. You are not sure if it is a good one or not. You’ve got two items on your punch list. Review this Example after you have addressed those other two items.
Example: Resetting after the first digit
Once an example sends the execute_function method, the RpnCalculator will continue to reset. You need to fix this.
Example
- Create the following example.
- Run your Examples, notice the failure:
The problem is, the “reset state” is set in execute_function but never reset.
There are two ways to fix this:
Update the digit_pressed method
Or Update the reset_input method
Either will work. In fact, doing both works. Question, to which method does the responsibility seem to bind? If you tought reset_input, you’re right.
-
Fix this by updating the reset_input method.
-
Make sure your Examples pass:
Check-In
- Check in your work, you’ve made some good progress:
Refactor
Reviewing the RpnCalculator there does not seem to be a need to do any refactoring. I do observe that the top method is still hard-coded, so an Example to drive that would be good.
As for the Examples, there is one thing you can do to clean up a little duplication. Every Example enters the number 654, so add a before method:
-
Add the before method and run your Examples. Make sure all Examples still pass.
-
Now, remove the first enter_number 654 from each of the subsequent Examples.
-
Verify that all of your Examples still pass.
Check-In
With the one refactoring, now is a good time to check in your changes
Summary
You’ve pushed the solution a bit further and cleaned up some duplication. Specifically, resetting the value in the x_register seems to work and you’ve put some common setup work into a before for your Context.
You still have the question of whether there should or should not be a stack. And what about that pesky Example? You’re keeping a punch-list, right?
Example: Enter actually puts values on the stack
Up to this point, your examples have addressed the effect of the
What can you say about the RpnCalculator?
- The x_register has a value of 99.
- The x_register can still receive receive digits.
- The first previous value is 14.
- The second previous value is 7.
You already have validated the first two bullets with previous Examples, so there is no value in re-checking those features. Youcould add validation to existing examples to check for the second two values. I would advise against doing so because smaller tests tend to pinpoint problems better. Also, depending on the technology you are using, once a validation fails, subsequent validations are not checked in in RSpec, nor are the in most testing tools (FitNesse being one counter example).
Why is this last part important? When you’ve broken something, how big is the break? If your Examples fail, but each has one or a very small number of validations, you’ll have a good idea of the span of the break. However, if you have Examples with dozens of validations (or many more), it is possible that an early validation will fail and you simply do not know if other validations would have failed. So while you think you only broke one thing, you broke 30 things. With many small tests, you’ll have much higher fidelity information from which to move forward.
So with that in mind, you can continue with one a few more Examples working with
Example
Here is an example that moves your production code just a little forward:
This Example suggests that after the first
-
Create this example.
-
Add the missing method with an empty implementation.
-
Get the example to pass:
- Verify that all of your Examples now pass.
Check-In
- Check in your work.
Refactor
There was little added to either of your two files, so there’s not much to refactor right now. However, did you notice that you now have two hard-coded methods? This might suggest a direction in which you want to push your Examples.
Check-In
No need, no change.
Summary
This Example really just extended the API of the RpnCalcualtor. You might be feeling like it is time to get busy because of the two pending methods.
Example: Enter should increase the number of operands
The operator_count is hard-coded, this Example will make it harder to get away with that.
Example
-
Create this Example. Notice that since you added no new API calls to the RpnCalculator’s interface, you can write the complete Example while following the second law of TDD/BDD.
-
Verify that your Example fails:
There are two ways to make this work. Here’s one way to get this to pass (don’t do this):
Update initialize
Update execute_function
Update operand_count
Alternatively, you can use an array and actually store the values entered (notice, you change the same methods, add the same amount of code):
Update initialize
Update execute_function
Update operand_count
Which of these is the least amount of code that will get the Example to pass? Both involve 3 lines of code added to 3 methods. One uses an integer, one uses an array, and an array is more complex than an integer, though in Ruby they are both objects.
Why do you try to do the least amount possible to get an Example to pass? That guideline is a “how” not a “what or a why”. That is, it is a means to an end. What end? There are at least a few:
- Writing only enough code means you are less likely to over-design or gold-plate your solution.
- It also helps keep the production code actually covered by tests.
In this case, the Example will exercise all of the added code in both approaches. What about over-design? When are you over designing? Are you over designing when you are using a design pattern where just a simple direct relationship is adequate? Yes. Are you over-designing when you a complex object structure, when a small, flat structure will suffice? Yes. Is over designing always a problem? NO. The problem is not over design. The problem is that over design leads to more rework when you’ve guess wrong. And as a developer, I can say that I guess wrong often.
Why do developers guess wrong? Because we generally see problems differently that people who provide requirements. Here’s an analogy. On the Nintendo WII, there’s a feature called “Mii’s”. A Mii is an avatar. I only created one because I had to for some games. I created one and never change it. My wife and daughter both have crated more than one, they update their looks and I’m convinced if there was a game that simply let players updated their Avatars with clothes, and makeup, etc, they’d buy it. If you asked me, it’s a waste of time. But that is one of the may appeals to the system.
Problems change in ways that we often cannot anticipate simply because they are coming from a very different place. So when you write more code than is necessary at the moment, you’re risking having to change code, or maybe make something that is designed for unnecessary flexibility, which could make the code harder to adapt to actual changes on the ground.
In this example, the domain is somewhat technical. Also, you can read the manual for RPN calculators and they will talk about a stack. So using an array as this code does is not diverging from the problem domain.
The second solution is a good one and it is not over design given the domain context.
-
Implement the second solution.
-
Verify that all of your Examples pass.
Check-In
- Check in your work.
Refactor
A quick review of the code shows a hard-coded top method. Now that you have a stack in place, can you simply return the “top” of the array?
- Experiment, change the implementation of top:
- Run your Examples, everything still passes.
This works. You have changed the production code without breaking any tests, so this is a legitimate refactoring.
Check-In
- Check in simple refactoring.
Summary
You have moved towards a more complete solution by introducing an array to hold operands. Was this over design? No. You certainly could have used the integer and then the stack, but that seems somewhat dogmatic.
You also managed to get rid of the last hard-coded method, top. This one Example managed to move the production code forward quite a bit.
Example: The right thing is on the stack
Right now, there’s no way to know if what is on the stack is the right thing on the stack. Most of the pieces are in place, in fact even though the code is fully covered by the Examples, the Examples do not reflect fully what your production code can do. It is time to exploit that.
Example
Here is an example that verifies the stack behavior of the RpnCalculator:
First observation: This code will not run because there is no pop! method on the RpnCalcualtor.
-
Create the Example, verify that the test fails as expected.
-
Add an empty pop! method to verify that the Example fails for the right reason:
-
Run your Examples.
-
Implement pop!:
- Verify that your Examples pass.
Check-In
- Check in your work.
Refactor
There has been a slowly-growing code small, Feature Envy, but hold off until just a bit longer. The “describe” part of the last Example is a huge clue.
Check-In
No need, you did not do any refactoring.
Summary
You have added an example to verify that in fact the right values are getting put onto the stack. Your RpnCalculator is growing, but at what point is the RpnCalcualtor suffering from too much design debt? It is right now, but you’ll see that after the next Example.
Example: What happens with an ‘Empty’ stack?
The stack on an RpnCalculator is conceptually filled with 0’s when it has no values. In fact, it is literally filed with 0’s. So if you check the top of your RpnCalculator, calculator, it should return 0 when it is empty. The same is true for pop!. It should return 0 if the stack is empty.
Example
These two are closely related, so here, for the first time, are two complete examples that you will fix at the same time.
- Create these Examples, they will both fail the same way:
- When code pop’s values off of an empty array, the array returns nil. The same thing happens when code calls top. This will fix both of your failing Examples:
- Make these changes and verify all of your Examples are passing.
Check-In
Check in, it is finally time to get busy removing Feature Envy and get closer to conforming to the Single Responsibility Principle.
Refactor
Review the following methods: pop!, top, available_operators. What do they all have in common? All three of these methods deal exclusively with the @operands instance variable.
Is this bad? Yes, it suggests that the RpnCalculator is doing work that should be pushed into the array. What? That’s right, the array is not pulling its weight. This is a legitimate case for wrapping a collection class and giving it domain-specific behavior (semantics).
There is a name for methods in classes working exclusively with data in other objects. It is a code smell called Feature Envy.
Normally, at this point I’d say “you cannot change a system-defined class, so you either need to subclass or wrap and adapt”. That is, create a subclass of Array, change the top method and add the pop method. Or, create a new class called something like OperandStack that holds a single instance of an array.
However, in Ruby, classes are never closed. You can add methods to a class anytime. Adding methods to Array is a bad idea since the Array class is used all over the place. However, Ruby also allows you to add methods to an instance.
This is crazy. Here is an example of how you could do that (don’t do this):
The result from running this will be “true” printed twice. But if you try to simply create an array, you will not see the same results. Neither method is defined on the Array class.
Barring extending a single instance, it is certainly reasonable to create an OperandStack class, move these methods into it and then change the implementation of the RpnCalcualtor to use the OperandStack.
Before you start, did you notice that the Context for the last three Examples was “Stack”? That’s a big clue that there’s a missing class.
-
There are 3 Examples in the “Stack” context. Move the first one out into “Handling the
<enter>
key” context. -
Verify that all of your Examples are still passing.
Next, you are going to do some “big” changes. It won’t take long, but this will be the longest amount of time the tutorial will have you work before you can see your Examples running. Note your reaction to the change.
- Change the structure of rpn_calculator_spec.rb:
If you ran your examples now, the ones in the Stack context will fail. Why? Because they no longer pick up the automatically-created calculator object form the before in the “RPN Calculator” context.
- Update the “Stack” context:
This still won’t pass. There’s no OperandStack class.
-
Move the entire OperandStack context into a new file called “operand_stack_spec.rb”.
-
Now your rpn_calculator_spec will pass:
- Next, create a new class called OperandStack in a file called opeand_stack.rb:
Note that these are mostly just** copies** of methods from RpnCalculator. There is one new method, «, which just delegates to a method of the same name.
- Verify that your operand_stack_spec.rb now passes:
Did you fully test this class? No. What you instead did was extract a class and then copy the tests that were directly testing it. You are not writing new code, you are refactoring the solution.
- Run all of your Examples, everything should be passing:
Intra-refactoring Check-In
- All of your Examples are passing, right? Now is a great time to check in before moving to the next step.
Back to refactoring
Now you should refactor the RpnCalcualtor to use your newly-created class. As with other refactorings, you’ll add, duplicate, validate and then remove code.
-
Add “require ‘operand_stack’ to the beginning of your rpn_calculator.rb file.
-
Update the initialize method:
-
Run your Examples, everything should still pass.
-
Update execute_function to duplicate the work of storing the x_register:
-
Run your Examples, everything should still pass.
-
Update top:
-
Run your Examples, everything should still pass.
-
Update pop!:
- Reviewing available_operands, you notice that there’s no length method on Operand stack. So add it:
-
Run your Examples, everything should still pass.
-
Update available_operands:
-
At this point, you can remove all code that refers to @operands (you’ll change 2 places).
-
Run your Examples, everything should still pass.
Check-In
Whew! That was a big refactoring. Check your code in!
Summary
This was a big change. You had a violation of feature envy. The RpnCalculator was doing a lot of work with information stored in an Array. So you created a class to represent that work, OperandStack, and moved Examples around to get everything situated.
If you have a decent IDE, these kinds of refactorings are quick, easy and fairly reliable. If not, then like Robert Martin tweeted:
It’s like riding a horse on the interstate.
This cleaned up your RpnCalculator class but there’s still more to be done. The implementation of execute_function is wanting to do more (or less).
Is there a problem with how you are testing OpernadStack? There are only 2 Examples but quite a few methods for which you do not have unit tests. Those methods are in fact used from the RpnCalculator so you have coverage. However, if the OperandStack is used by other classes, then the testing is not adequate. Maybe you should consider a nested class. Maybe you should write more Examples to get its “contract” fully specified.
When you are just learning TDD or BDD, this kind of stuff is going to happen. Let’s leave it alone for now and see what happens.
The Classes So Far
Here’s one example of what all of your code should look like about now.
rpn_calculator_spec.rb
rpn_calculator.rb
operand_stack_spec.rb
operand_stack.rb
Example: A Binary Operator
Some time ago you created your first Example to exercise the <enter>
key. That resulted in the following method:
Now it is time start executing more functions. The first function you’ll support is +.
Example
Here is a basic example to get started:
Why is the result 92? The support method enter_number
- Create this Example and run it. The result is not quite right:
- Getting this to pass is a “snap”:
- Get your Examples passing.
Check-In
Check in your code. You are about to refactor.
Refactor
OK, the method you just updated, execute_function, is a bit of a mess. It does two thigns:
- Determine which function to perform
- Actually perform the work.
So you’ll separate this for now.
- Extract two methods, enter, +:
-
Make sure your Examples still pass.
-
Update the execute_function to use those two methods:
- Update your RpnCalculator, verify that your Examples are all passing.
Check-In
This is all the refactoring you’ll do right now. More is on the horizaon.
Summary
You’ve improved the execute_function but you have a stubbed-out + method. If you know a bit of Ruby, you might be chomping at the bits to evaluate a symbol, or use lambda and a hash. The code will get where it will, however, based on the Examples, rather than speculation.
Example: Work on +
What should happen with + when there are no operands? Should it fail? Should it result in 0? If you’re simulating an HP calculator, the result is 0. However, what it should do will be defined by the Examples you write.
Example
-
Create this Example. Verify that it fails.
-
Update the + method to actually do some work.
- Run your Example, make sure it works.
Check-In
Check in your work.
Refactor
Something is starting to look strange. You may have noticed it the first time your code “supported” +. The following line:
I’m not sure what to do with that yet because there’s not enough code to make many decisions. It might be that the code will stay put. It just seems a bit strange.
Check-In
No refactoring done, no need to check in again.
Summary
The + method no does work. It has one strange line, and there’s still the issue of whether it modifies the stack correctly or not. Another thing occurred to me as well. After any operator, the x_register should be in “reset” mode.
Example: x_register in reset mode after +
Like
Example
- Create this Example. Run it and you’ll notice that it fails:
- Make a quick change to the + method to fix this:
- Run your Examples, they should all pass.
Check-In
Check in your code. This time you will do a little bit of refactorig.
Refactor
There is a duplicated line in the enter and + methods:
At the very least, this line of code could be put into a well-named method.
- Create a new method (if your IDE supports Extract method, use it):
-
Make sure your Examples still pass.
-
Update the enter and + methods to call this new method.
-
Make sure your Examples still pass.
Check-In
Even though small, you did make a change to your solution while keeping your Examples passing, so check in your work.
Summary
There appears to be a pattern emerging. Two functions, enter and +, both of which change the calculator into “override_mode”. Will other functions look like this? Yes. Will you be able to remove this duplication as well? Yes.
Example: What about the stack?
Some time back the tutorial asked about whether the stack was treated properly. You could have put those checks into another Example, but the rationale for not doing that is to keep your tests small and focused for better pinpointing of problems and better assessment of the size of a break.
It’s time to come back to that, if for no other reason than to expression a “contract” that the + method (and all binary operators) will need to follow.
Example
Here’s an example:
-
Create this example.
-
Run your Examples. Notice it passes.
Did we code too much or are we validating a behavior? Would it have been possible to write less code and still keep other tests passing? Maybe, the design is such that it’s getting harder to do the wrong thing. In any case, this seems like an OK Example - if maybe a bit off target.
Check-In
Check in your work.
Refactor
There’s starting to be a bit of cruft in the Examples and maybe something in the RpnCalcualtor. You might already be noticing yourself. For example, in RpnCalculator, to set the “x_register” the code does the following:
That is not self evident. In addition, getting two operands for a binary operator involves two very different expressions:
The last two Examples are OK, though do you believe that the differences between digit_pressed and enter_number is obvious?
Start by making an improvement to the bottom part of the + method:
- Add a new method that is private (since private sets subsequent methods, make this the last method in your RpnCalculatorClass):
-
Verify your Examples still pass.
-
Update +:
-
Verify your Examples still pass.
-
Check in this refactoring.
The next thing you might consider is how to better document getting parameters for +. I tried a few ways but none seemed to really make the code better. Just differently convoluted. You might try yourself (and even succeed). However, I already have an idea of where I want this code to go, so rather than any more refactoring, I want to move into other operators.
Check-In
- Check in your refactoring.
Summary
You now have the basics of the + operator worked out. You make resetting the x_register at least a little better. Now it is time to add some more operators to see how this current solution grows.
Example: Support for a new binary operator, -
Adding support for - will allow you to see how your solution grows as new math functions are supported. You have already vetted + fairly well, so adding - should be a snap.
Example
At this point, hopefully you are getting bothered by the difference between lines that start with an @ and ones that do not. Add that to your punch-list.
- Create this example. Run your examples and verify that they do not pass.
Were you surprised by the result? The results suggest that instead of -, your calculator performed +. Why is that?
A quick review of execute_function should give you a clue:
So if something is not :enter, then it is :+. Add that to your punch-list:
-
Your execute_function should fail if the operator is not found.
-
However, you are not working on that Example. So update this method to call -:
- Now your code is missing the function - (which you discovered by running your Examples, right?), so add it:
- Run your Example, now it should be failing for the right reason (value did not match).
You have two obvious choices here:
- Create a method that is hard-coded to return the correct value.
-
Create a duplicate of the + method and change it to be -
- Rather than following the third rule, you can safely duplicate the + method and update it (you’ll be refactoring this soon anyway):
- Verify that all of your Examples are passing.
Check-In
Check in, you have a lot on your punch-list.
Refactor
There are several things on your punch-list:
- Adding a new operator requires too much work: update a method, add a new method, and write that method. More importantly, it requires changing an existing method which is in direct violation of the open/close principle.
- The execute_function does not indicate if the function does not exist.
- There is duplicated code between the + and - methods. (This was on your list, wasn’t it?)
- The difference between enter_number and digit_pressed seems isn’t obvious.
- The Examples should not have such a mix of lines that use fields and support methods.
Looks like you have a lot of work.
First, if you have been chomping at the bits to fix execute_function, now is the time to do so:
- Update execute_function:
- Run your Examples to verify that they all pass.
This is a Ruby idiom. The original approach of passing in a symbol screamed for this solution. This has several effects
- Removes the need to update this method ever again
- Requires, however, that the functions of the calculator are actually methods on the class.
- Your code now will fail if you attempt to send a method that is unknown
- Allows invocation of (0 argument) private methods
Even so, this is an improvement and it follows a Ruby idiom.
- Check in your change.
Next, you’ll remove the violation of the DRY principle:
- Add a new private method to your RpnCalculator method:
-
Make sure your Examples still pass.
-
Update the + method:
-
Run your Examples, make sure they still pass.
-
Update the - method:
- Run your Examples, make sure they still pass.
Looks like you’ve managed to fix the first three things on your punch-list with a few quick changes in your code. Of course, you still do not have an Example that captures what happens when you execute a bogus method, the code should handle that now.
Before calling this refactoring session done, clean up your Examples to make this a bit more self-explanatory.
- Add some new support methods (put these at the same place as enter_number):
-
Make sure your examples still pass.
-
Update “should subtract the first …”:
- Run your Examples, they should all pass.
However, just to place a (somewhat arbitrary) stake in the ground, here’s an updated version of rpn_calculator_spec.rb:
- Get to where all of your Examples are passing.
Check-In
- Check in your work.
Summary
That was a lot of refactoring. Maybe the tutorial let you collect too much design debt. However, this will happen and it is up to you to pick up the slack.
You made several important changes:
- The execute_function method now uses the send method to execute a symbol, so you will not have to update it so long as new functions are added to the RpnCalculator clss.
- As a result of this change, your execute_function will indicate errors better than when you added an Example for - (which caused + to get called)
- You created an execute_binary_operator method that makes adding binary operators a one-line lambda expression.
- As a result, you removed some DRY violations.
- You made a significant change to how you write your examples.
What’s left? There’s still the issue of documenting what happens when you attempt to execute a missing function and the rpn_calculator_spec.rb file is getting large.
This second issue is not addressed in this tutorial.
Example: Handling missing functions
So what happens when your calculator is asked to execute an unknown function?
Example
- Create this new Context and verify that it passes.
This Example documents that you expect the behavior of throwing a NoMethodError when sending a bogus method name to your calculator.
Check-In
- Check in your code.
Refactor
You just did a bunch of refactoring. You only added an Example to capture something that was added during refactoring but not explicitly spelled out in any Example. So nothing new to refactor.
Check-In
You have no changes to check in.
Summary
This example really just captured something about your system that was not explicitly spelled out anywhere. This probably indicates too much change done during refactoring. It would have been possible for you to wait to change the implementation of execute_function and first write this Example.
You won’t always figure out to do that, so you’ve managed to recover gracefully.
Example: Add support for * and /
You have a working design for + and -, adding * and / should be a snap. First your calculator will support multiplication.
Example
-
Create this example. Run it and verify that your test fails with a missing method.
-
Create the method:
- Verify that your Examples pass.
That was so quick, before checking in add one more example:
- Add the missing method:
- Verify that your Examples pass.
Check-In
Check in your work.
Refactor
You might review your code and find some things to refactor. One issue is the size of the calculator. Right now it is not too bad, but it’s going to get bigger. Each time you add a function, it grows.
Check-In
Nothing new to check in.
Summary
You’ve just finished out the basics for your calculator.
RUNNING OUT, COPY AGAIN —- </section>
Comments