4

Let's say we have this interface:

public interface OutputChannel extends Closeable {
    public void writeOutput(String output);
}

Then there are several classes which implement this interface and generally it is the case that writeOutput() cannot be used after they have been closed.

However, among them there are classes like NullOutputChannel (which never does anything, so it doesn't care if it was closed), or classes that only delegate the output to other Closeables (so they don't care either - they don't do any IO themselves).

I'm not sure if every OutputChannel should always throw IllegalStateException if writeOutput() was called after close(), or if it should silently tolerate it unless it encounters an actual IO problem.

So far I have come up with some conflicting views:

THROW:

  • Calling writeOutput() after close() is an error and it should not be tolerated. Even if it doesn't matter in case of NullOutputChannel etc., it could matter if NullOutputChannel was replaced by some other OutputChannel implementation.
  • The interface will behave more consistently if the exception throwing is decided by the general logic that you can't write after closing, rather than by the implementation details of a particular class.
  • If a class doesn't throw an exception in this case, it is only hiding a bug. It would be better if the bug manifested regardless of the chosen implementation.

IGNORE:

  • If a particular class doesn't have a problem with something, it should not throw an exception.
  • Checking for closed state in a class that doesn't care anyway is an unnecessary complication.

I would be grateful for some arguments for either approach, or some recommended best practice in these situations.

4

You've already stated the pros and cons quite clearly in the question; the only real question to be answered is, which argument is more convincing?

My line of reasoning on the subject would be to keep in mind a couple of points: one of the fundamental principles of OOP is resuability. You're creating an interface and a handful of individual classes that you'll only have to write once, but then they may be used hundreds of times.

Another important principle is Liskov Substitution, which says that anything that inherits from a base has to be able to substitute for the base type in all observed behavior. (It can do other things as well, but if it doesn't do the main set of behaviors, you're violating Liskov Substitution.)

So if you're going to have a lot of disparate code that uses your Closeable-descended interface, and it expects to have something that behaves like a Closeable, then it needs to behave like a Closeable in all ways, including enforcement of expected invariants. One of the points of Interfaces and Liskov Substitution is that the same code might use different descendants of a declared base type at different times, and you want it to work the same each time.

Also, on a similar track, remember the Fail Fast principle. If you have an error in your code, you want to detect it as quickly as possible. This means that if you're using a NullOutputChannel early on, and you don't use a functional OutputChannel until much later, you don't want to wait until "much later" before discovering that your code using the OutputChannel interface is glitchy.

2

The "ignore" approach does not really have any grounds to stand on.

"If a particular class doesn't have a problem with something, it should not throw an exception": no, a class is supposed, among other things, to honor an interface, therefore it ought to have a big problem with anything that violates the rules laid out by the interface. Whether the violation of the interface has anything to do with the inner workings of the class implementing the interface, is irrelevant.

"Checking for closed state in a class that doesn't care anyway is an unnecessary complication": no, it is not an unnecessary complication, it is an absolutely indispensable part of making sure that the interface is being used correctly.

The ease of programming should not be considered as being equal to the ease of making undetected mistakes. (This is something that Edsger W. Dijkstra said, source is here.)

Generally, the habit of always making sure that everything is happening precisely as expected, and that nothing is being left to chance, leads to systems that are more stable, more predictable, less buggy, less chaotic. In contrast, writing code that allows mistakes to go undetected is a sure way to forever keep busy fixing bugs all over the place.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.