When we write methods, we need to define, for ourself and others, the environment before and after the method executes. These expectations should be explicitly documented in the contract for the method.
Often, a method is written under the assumption that certain things are true about the environment: these are called pre-conditions. For instance, consider the following buggle method:
public void square1 (int n) { this.forward(n); this.left() this.forward(n); this.left() this.forward(n); this.left() this.forward(n); }
A first cut at the contract of square1()
might be:
Causes this buggle to draw a square whose sides consist of
n + 1
cells. Uses this buggle's current color as the
color of the square.
But the buggle will only draw the square if its brush is down when
the method is invoked. The brush being down is a pre-condition for
the method to work as expected. In a contract for a method,
pre-conditions can be stated as assumptions. For example, here is a
second cut at the contract for square1()
:
Assume that this buggle's brush is down. Causes this
buggle to draw a square whose sides consist of n + 1
cells. Uses this buggle's current color as the color of the
square.
There are other assumptions necessary in order for square1()
to work as expected. For instance there can't be any walls in the
path of the square, or else the buggle will get stuck. This could
also be stated as an explicit pre-condition, but for simplicity we
will assume that it is understood that buggles cannot draw through
walls.
A post-condition indicates the changes made to the state of
the world by invoking a method. In the case of buggles and turtles,
it is important to document any changes to their state as a result of
executing a method. For instance, because the square1() method brings
the buggle back to its initial position, but because it only turns
the buggle left three times, the buggle ends up in a direction to the
right of its initial direction. It is important to document such
post-conditions in the contract. Here is a final cut at the
square1()
contract:
Assume that this buggle's brush is down. Causes this
buggle to draw a square whose sides consist of n + 1
cells. Uses this buggle's current color as the color of the
square. The buggle's final position, color, and brush state are
the same as they were when the method was called, but its final
heading is 90 degrees to the right of its initial heading.
Aspects of the state that remain unchanged by the invocation of a
method are called invariants. Thus, the buggle's position,
color, and brush state are invariants of square1()
,
but its heading is not.
When writing methods that change the state of objects like buggles
or turtles, it is often a good idea to have as many invariants as
possible. This simplifies reasoning about sequences of method
invocations (and increases modularity). For example, suppose we want
to draw three nested squares that share a corner. Using
square1()
, we can do this as follows:
public void nestedSquares (int n) { this.square1(n); this.left(); this.square1(n + 2); this.left(); this.square1(n + 3); }
The fact that square1()
effectively
turns the buggle right means that it is necessary to call
left()
to set the buggle's heading
properly for the next square. Note that the buggle's heading is not
an invariant of nestedSquare()
.
Writing methods like nestedSquares()
would be simpler if the square-drawing method did not change the
heading of the buggle. Below is a square()
method that leaves the
position invariant, and the corresponding version of nestedSquares()
:
// Assume that this buggle's brush is down. // Causes this buggle to draw a square whose sides consist // of n + 1 cells. // Uses this buggle's current color as the color of the square. // The buggle's position, heading, color, and brush state are invariant. public void square (int n) { this.forward(n); this.left(); this.forward(n); this.left(); this.forward(n); this.left(); this.forward(n); this.left(); } // Assume that this buggle's brush is down. // Causes this buggle to draw a three squares whose sides consist // of n + 1, n + 3, and n + 4 cells, respectively. // The squares share the corner at the buggles initial position. // Uses this buggle's current color as the color of the square. // The buggle's position, heading, color, and brush state are invariant. public void nestedSquares (int n) { this.square(n); this.square(n + 2); this.square(n + 3); }
Invariants are especially important in cases where an object asks questions about the environment. In these cases, we don't usually expect that asking a question will change the object or its environment. As an example, let's write a method that will determine if there is a wall to our buggle's right. Well, we could do the following:
public boolean isWallToRight () { this.right(); return isFacingWall(); }
So, what's wrong? Well, nothing initially, the method does give us the answer we want. However, things get confusing if we try to use it in a program. After we've asked the question, our buggle is no longer facing its original direction. Instead, it's facing the direction that's to the right of its original direction. Ok, no problem. Let's do the same thing for figuring out if there is a wall to our buggle's left:
public boolean isWallToLeft () { this.left(); return isFacingWall(); }
Again, this works. However, if you ask the buggle if there is a
wall to its left, it will end up facing the direction left of its
original direction. Where is the problem? Let's say we want to figure
out whether or not our buggle is in a hallway. Our buggle is in a
hallway if there is a wall to the left and to the right of the
buggle. So, let's define a new method called isInHallway()
like
so:
// buggle is in hallway if there is a wall to the left and to the right public boolean isInHallway () { return (isWallToLeft() && isWallToRight()); }
Does this work? No. Why not?
Well, let's assume that the buggle is in the middle of a hall (so
there is no wall in front or behind). When we ask
isWallToLeft()
, the buggle turns to the left and answers
true
. Now when we ask it isWallToRight
, the buggle
turns to the right and gives us what answer? Well, the buggle is now
facing it's original direction with no wall in front, so it tells us
false
. Therefore, our method isInHallway()
also returns false
(since true &&
false
is false
). In order
for our isInHallway()
method to work, we need to have the buggle
figure out whether there are walls to its left and right without
changing its position. It's not really possible to do so without
changing the buggle's position, but we can put the buggle back into
its original position before answering the question. So, the better
way to write isWallToLeft()
and isWallToRight()
follows:
public boolean isWallToLeft () { this.left(); boolean result = isFacingWall(); this.right(); // reset direction return result; } public boolean isWallToRight () { this.right(); boolean result = isFacingWall(); this.left(); // reset direction return result; }
Notice that we had to save the result away in a variable rather than return the result of the test directly.
With the redefined methods
above, our isInHallway()
method will now work
correctly.
Moral of the story: Methods that answer questions about the state of the environment should not change the state of the environment!