When it comes to dealing with third party dependencies in my code I've used the same trick for a long time. But I was recently introduced to a new trick that I have mixed feelings about.
So my old trick is simple. I do two things. In order to understand the dependency and how it works I write a bunch of tests to see how the dependency works. Depending on what type of dependency it is I can run these tests all the time or I just keep them around for the day I want to use a newer version of the dependency. That way I'll know if some assumption I made on how the dependency works changes.
The other thing I typically do is that I assume the dependency was poorly written when it comes to testability. I create my own interface that works the way I want and then use an adaptor implementation to wrap the dependency in my own abstraction.
All this is pretty straight forward and fairly easy to test for all kinds of error cases since the exploratory testing I first mentioned typically surface the common error cases from the dependency. The down side is that there is always something, an edge case that I miss and then unit tests are fine but code relying on the dependency behave in an unsuitable way for these edge cases.
In order to solve this a colleague suggested that we use reflection on the dependency to figure out what exceptions it will throw. I got excited for about three seconds since it is an excellent idea until you scratch the surface...
Because in order to figure out what exceptions the dependency will throw I essentially need to evaluate all possible code paths at all levels in the code. And I still only get all the exceptions and not all other ways error could surface...
I hated the throws keyword you had to put on methods in Java in order to declare all possible exceptions a method would throw, including things thrown my methods you called in the method unless you handled those exceptions. But I must say it is a nifty feature when it comes to making sure your code handles all possible exceptions of a dependency. Assuming all errors are exceptions...
I think I'll continue to take my chances on my usual way to deal with these things. Since it has been good enough for a long time.
Checked Exceptions are intended to be a sort of modification of your return type: You either return the method's standard return type, or you 'return' one of the declared exceptions. It's far more robust than people credit it for, but Java's infamous implementation leaves you with highly verbose handling and rigid subtypes. However, I think that - when used correctly - it can have the benefit you stated of providing well-defined behaviour even in exceptional circumstances.
ReplyDeleteWhat does "used correctly" mean to you?
DeleteNot sure yet, to be honest. Still exploring my mental model, but I'd expect that it could be a trade-off of sorts: exceptions crossing the boundary of a component would be carefully selected and enumerated to provide sufficient information to clients. At the same time, these outer exceptions give future subtypes within the module the flexibility to leverage whichever exceptions they want, as long as it can be wrapped by these outer exception types.
DeleteThat being said, this is all conjecture on my part. My current theory is that checked exceptions could be treated as a variant of the 'Either' functional programming structure, which would then lead you to use standard programming methodology to handle exceptions instead of a specialized try-catch construct. This would also allow for variant exception types (being able to define and return your own subtype of the declared exception). And all this is feasibly possible in OOP languages, too, just without the convenience of pattern matching.
I agree that the mental model of considering declared exceptions as part of the "return type" is correct. And I agree that crossing component boundaries I want a well defined set of possible exceptions. I love all that. The problem is how can I know that without the "throws nightmare". The use of throws in Java kind of forces me into considering every single method I write to be a component boundary (which is not necessarily bad). But it also means a lot of catching and rethrowing of new exception types in order to keep it clean.
DeleteBut then it was a long time since I used Java so I'm very curious to what a good usage would be because in theory I like it. Just haven't experienced it in a good way yet...
No question there, it makes code incredibly verbose. Perhaps using a single specialized wrapper Exception per module would allow further detail to be extracted only when desired, and would keep the explicit exception types minimal. I think that's the kind of strategy I've seen with AWS' APIs, for example.
DeleteAnyway, if nothing else, this could be a neat area for either language design or pattern development. Use cases this common shouldn't have the kind of friction that checked exceptions tend to.
What if your dependency throws a NullReferenceException or a DivideByZeroException dependign on your input? How do you define a code path? If you have an expression in your dependency producing a DivideByZero only on certain values of one input Int32 parameter, then do you have 2^32 code paths, or you have one branch succeeding and one failing? In the latter case how do you find the branch? How do you reflect on that? Sorry for all the questions, I don't mean to sound offensive, I am genuinely curious :)
ReplyDeleteI think you are just pointing out how ridiculous it would be to try and use reflection to find all possible exception... But technically I wouldn't care what input caused the exception. I see it this way; reflection is really replacement for having all the source of all possible execution paths and then I search for "throw new" in that source code. And yes the source would need to include *everything* including the runtime generating the NullReference and DeviceByZero exceptions...
DeleteRidiculous...