The advantage of assertions (or contracts, which are basically a different take on assertions) comes when your codebase grows large enough that you can no longer hold the entire thing in your short-term memory. Especially when you're still developing it and changing things every day.
When you first wrote MapEntity
, you most likely wrote it because some other routine somewhere required its functionality. You were working on the other routine at the time, and you know it's not going to pass any nulls. But a few months from now, you might be writing something else that needs the same functionality, so you have it call MapEntity
too. But maybe this code can have a null somehow and you didn't think to check for that.
With no assertions, you get a null reference exception at this point. With assertions in place, you get an assertion failure (or whatever violated contracts throw, in this case.) Either way, you have an error here and you have to dig into the code to figure out what's going on. You look at the code, and you see that the problem is in MapEntity
because it's choking on one of the parameters. So is there a bug in MapEntity
, or is routine that called it feeding it an invalid parameter?
One of the advantages of an assertion/contract is that it clearly documents the fact that you anticipated this problem and explicitly did not want it to happen. This makes figuring out how to fix it that much easier because it makes it easier to determine the source of the problem. In this case, it shows clearly that the problem is in the caller, and not in MapEntity
.
On the other hand, sometimes I'll get an assertion failure in code that's receiving perfectly cromulent parameters, because the requirements have changed. This helps me see what has to be changed, and sometimes it's not as simple as "argument must not be null". Sometimes the argument has to have certain properties set certain ways or data would have been silently corrupted. Having an assertion in place makes it easier to remember what I was thinking when I wrote it, so I can fix the code more easily.
Remember inheritance defines a "is a" kind of relationship. The code
class Position2D extends Vector2D
tells the compiler that every Position2D is a Vector2D, but not vice versa.
Check out the following examples assuming that you already made the above declaration:
// obviously okay, type of instance matches reference type
Vector2D vecVecRef = new Vector2D();
Position2D posPosRef = new Position2D();
// this is okay because a Position2D is a Vector2D
Vector2D posVecRef = new Position2D();
// The compiler will not automatically convert a Vector2D to a Position2D
Position2D vecPosRef = new Vector2D(); //Compiler error!
A Position2D variable can never refer to a Vector2D object. If you try typecasting it, the compiler will say "fine...I trust you" but then the JVM will get angry when it actually finds out that you are trying to assign a Vector2D to a Position2D. It will raise a ClassCastException
at runtime.
// compiles fine but raises ClassCastException in runtime
Position2D vecPosRef = (Position2D) new Vector2D();
This is because you can only Class cast a subclass into a superclass and not vice versa. So basically, you cannot cast Vector2D to Position2D and you cannot assign it without casting either.
The simplest solution to this problem is to have a constructor defined in your subclass that makes a Position2D object out of a given Vector2D object.
class Position2D extends Vector2D {
Position2D() {
// default stuff
}
Position2D(Vector2D v) {
// you currently don't have the getX and getY methods
// so define them in your superclass
setX(v.getX());
setY(v.getY());
}
}
With that one simple and convenient constructor, you can use code like this:
public class Inheritance {
public static void main(String[] args) {
Position2D pos1 = new Position2D();
Position2D pos2 = new Position2D();
pos1.setX(3);
pos1.setY(4);
pos2.setX(5);
pos2.setY(6);
Position2D pos3 = new Position2D(pos1.add(pos2)); // using constructor
System.out.println(pos3.getX()); // this prints 8.0
}
}
As you can see, this way is much more extensible than rewriting all of the subclass methods.
Best Answer
I think you are on the right track. Neither throwing, catching, nor documenting all potentially throwable exceptions makes much sense. There are times where the product stringency requires a higher degree of exception employment and documentation (e.g. certain safety critical aspects of a systems).
The strategy of being more defensive, using contract concepts to identify preconditions (and postconditions) on especially downstream callers (e.g. anything that resembles a public or protected member) will often be more effective and more flexible. This applies not only to the implementation, but to the documentation. If developers know what is expected, they are more likely to follow the rules and less likely to be confused or misuse the code you have written.
Some of the common things that should be documented include the case of null parameters. Often there is a consequence for their use that drives the result to something that wouldn't normally be expected, but are allowed and used for a variety of reasons, sometimes for flexibility. As a consumer of a member that has parameters that allow null, or other special, non rational values (like negative time, or negative quantities), I expect to see them identified and explained.
For non null parameters, as a consumer of a public or protected member, I want to know that null is not allowed. I want to know what the valid range of values in the given context is. I want to know the consequences of using values that are outside the normal range, but are otherwise valid in a different calling context (eg the value of the type is generally valid for any operation, but not here -- like a boolean parameter that doesn't expect false as a valid value.
As far as platform, or otherwise well known interfaces, I don't think you have to go to extremes in documenting it. However, because you have an opportunity as a developer to vary the implementation from whatever platform guidance, making note of how it follows that guidance may be of value.
Specific to IDisposable, often implementations of this interface offers an alternative method that is preferred over the explicit disposal process. In these cases, highlight the preferred method, and note that the explicit disposal is not preferred.