Design: Custom Exception Usage

T

Terrence Jones

I have a design question that I would like to get some opinions on.

I have a class (ClassA) that uses a custom exception (ExceptionA). The
class and exception are defined together in one assembly.

I have ClassB that uses ClassA. If an ExceptionA is thrown from ClassA, the
users of ClassB may need to catch it.

I would prefer the users of ClassB to not need to reference ClassA for the
sole purpose of catching its exceptions. I was thinking of making a subclass
ExceptionB : ExceptionA (direct encapsulation) and catching and rethrowing.

I think this is too much effort. What are your thoughts on situations like
this?


Thanks
 
A

Anthony Jones

Terrence Jones said:
I have a design question that I would like to get some opinions on.

I have a class (ClassA) that uses a custom exception (ExceptionA). The
class and exception are defined together in one assembly.

I have ClassB that uses ClassA. If an ExceptionA is thrown from ClassA, the
users of ClassB may need to catch it.

I would prefer the users of ClassB to not need to reference ClassA for the
sole purpose of catching its exceptions. I was thinking of making a subclass
ExceptionB : ExceptionA (direct encapsulation) and catching and rethrowing.

I think this is too much effort. What are your thoughts on situations like
this?

Define ExceptionA outside of ClassA.
 
D

DSK Chakravarthy

First thing first, Try to avoid the Exceptions to the extent that you can.
Use is only in case where you can't handle.

Secondly, when you want to propogate any messages back to the calling
method, use OUT parameter from C# or use ByRef at VB.NET

HTH
 
A

Anthony Jones

DSK Chakravarthy said:
First thing first, Try to avoid the Exceptions to the extent that you can.
Use is only in case where you can't handle.

That's reasonable.
Secondly, when you want to propogate any messages back to the calling
method, use OUT parameter from C# or use ByRef at VB.NET

That IMO isn't

My advice would be avoid OUT or ByRef parameter like the plague.

The TryParse pattern has made this sort of thing legitimate but its a poor
pattern.
 
R

Registered User

That's reasonable.


That IMO isn't

My advice would be avoid OUT or ByRef parameter like the plague.
The whole problem concerns methods which can have multiple return
values. If getFoo is supposed to return a Foo and takes status
code/message arguments by reference the calling code can get pretty
ugly. It is much simpler encapsulate a Foo object and any ancillary
crap as members of an object returned by getFoo().

FooWrapper fw = getFoo();
if (fw.StatusCode == 0)
doSomething(fw.Foo);

regards
A.G.
 
A

Anthony Jones

Peter Duniho said:
[...]
Secondly, when you want to propogate any messages back to the calling
method, use OUT parameter from C# or use ByRef at VB.NET

That IMO isn't

My advice would be avoid OUT or ByRef parameter like the plague.

Hmmm...

While I'd agree that in this thread, the suggestion to use a by-reference
parameter doesn't make much sense (exceptions are meant to be thrown :) ),
I wouldn't say that the general technique should be avoided "like the
plague". There are too many examples of situations in which a
by-reference parameter is useful and not at all harmful for a broad
generalization like that to be valid.


Hmm... yes perhaps 'plague' is too strong. It is however a bad smell and
one would really have to question a choice to use it.

Taking TryParse as an example I would have prefered to have:-

bool CanParse(string)

but TryParse would clearly be faster. Not having CanParse I'm forced to use
TryParse even in code where performance is not an issue (which frankly is
true of most code).

int x;
string y = "Brass Monkeys";

if (Int32.TryParse(y, out x))
{
// do stuff with x but how did x get assigned a value its not that
obvious
}

Where I would have prefered:-


if (Int32.CanParse(y))
{
x = Int32.Parse(y);
// do stuff with x and its clear where x got its value.
}

OR alternatively

int? x;
string y = "Jon 'Google' Skeets idea'"

x = Int32.ParseOrNull(y)
if(x != null)
{
//do stuff with x
}

Of course its easy enough to build round but it means doing so out of
principle rather than actual need.
 
T

Terrence Jones

My rule of thumb is to use exceptions for those errors that will likely not
be immediately handled and re-tried. ArgumentNullException is a good example
of an exception that truely indicates an error.

Using return status, ByVal status, HRESULTS, etc on every method is
nightmare to use. Exceptions have their place and return codes have their
place.

ArgumentOutOfRangeException is necessary exception but indicates that
perhaps you should provide a "Test" method (TryParse etc) as well.
 
T

Terrence Jones

Actually, ExceptionA is defined outside of ClassA but within the same
assembly and namespace. My concern is that the user of ClassB must reference
the assembly and namespace. Of course any ExceptionA is relevant only with
knowledge of ClassA so in that sense it only makes sense for the consumer of
the exception to reference the assembly.

So let me change the question slightly. What do you all think of catching
all ExceptionA exceptions in ClassB and rethrowing them as ExceptionB
exceptions (preserving inner exception)? This "hides" the usage ClassA from
the user.

This idea is covered in Richter's "CLR via C#" 2nd Ed, Chpt 19.
 
A

Anthony Jones

I wouldn't want that at all. If I'm calling CanParse(), 99 times out of a
100 it's because I want to parse the string. It'd be silly to call a
method that basically has to do the work of parsing the string, but which
only returns a flag telling me if actually parsing the string will work.

Well thats the crux of the matter isn't it? Why is it "silly" ?

Sometimes as experienced developers we can start thinking like a computer
and write code that is constantly performance 'aware' even when its not
necessary which is _most_ of the time.
If anything the "Try..." pattern is a great example of the usefulness of
by-reference parameters.

Its also an example of obscurity of function. The vast majority of
functions we use will take input only parameters, the paremeters are never
changed by the function and the result of the function is its return value.
Anything which varies from that makes code harder to read. True TryParse
has its uses where performance is needed but the CanParse pattern I
suggested is more _human_. Can I parse, ok so I will. Simple clear
obvious.
You write "forced" as though there's something about calling TryParse()
that is harmful.

No I wrote "Not having CanParse I'm forced ...". Its not the presence of
TryParse as an option which bothers me its not having a CanParse that I have
issue with.
Beyond that, however, since sometimes performance _is_ an issue, it's not
like TryParse() could have been left out of the API.

Agreed and I didn't suggest that should've.
Why is it the case that "how did x get assigned a value" is "not that
obvious"? The "out" in the parameter list for the call to TryParse()
makes it very clear where x is getting assigned.

Its a relative thing. True it isn't actually hard to see that the out
keyword on the parameter indicates where x gets its value. However it is
harder to see than x = because that's what our minds will be condition to
expect when looking for an assignment. This may seem to be a microscopic
and insigniifcant difference but to think so is a mistake.

Think of it this way. The difference in real time between CanParse/Parse
and TryParse will be measured in nano-seconds. However its a difference
which you seem to think is worth gaining. In a few case (and let me
re-iterate it, it _will_ be a few) this may actually be true.

The same principle can be applied to readability of code. Small differences
such as CanParse/Parse vs TryParse only have a small effect individually on
readability however the cumalitive effect of computer oriented over human
oriented coding is significant.

The big difference between the two is that only a small amount of the
overall number of lines actually impact in any signficant way the perfomance
of most apps whereas all lines of code affect readability.

Especially in the context of C# where the compiler is very strict about
dealing with uninitialized variables, I never find myself wondering how
something got assigned. Because an "out" parameter is required by the
compiler to be assigned in the method being called, a call like this is no
exception.

Thats great if you happen to think like the C# compilier, perhaps you do and
you see out parameters as easily as assignments. However most humans would
not. So as long as no other less experienced developers have to come along
and read your code that'll be fine.

If performance is not a concern (and you already wrote that it almost
never is), you can always just wrap a call to Parse() with a try/catch
block. That provides the exact assignment pattern you're asking for.

That simply replaces one unusual pattern of code with another. Not a
solution to the human problem I'm looking for.
I would not be against the inclusion of a method like that as an
_optional_ pattern. However, I certainly would resent being forced to use
it in situations where I don't already have a nullable type.

Agreed. Changing the type of a variable to accommodate this would be a bad
thing. However there are some cases where in fact the nullable type is the
appropriate type. It would be nice to have the choice of all three but if I
could only have two I would take CanParse and TryParse. Sadly I only have
the one TryParse.
Indeed. And IMHO it's the principle that's flawed. There's nothing
inherently wrong with passing by reference. Why go out of your way to
avoid it?

My comments above outline why you should. Apart from being correct the most
important thing for code to be is readable not just to yourself but any who
may need to. Code that is readable to a human is not always code that uses
the full cleverness of computer science but takes into account the
psychology of the human reading it.

ByRef parameters are unusual and can catch the less wary out. They can be
gotchas even for the experienced. Whilst they do have their place they
should be used sparingly and be treated with suspicion. In short, they
smell bad.
 
M

Marc Gravell

ByRef parameters are unusual and can catch the less wary out. They can be
gotchas even for the experienced. Whilst they do have their place they
should be used sparingly and be treated with suspicion. In short, they
smell bad.

In general, I agree; but the "bool Try{Something}({args}, out result)"
is now quite common-place and /reasonably/ understood. For example,
int.TryParse, Dictionary<TKey, TValue>.TryGetValue, etc.

Maybe it sounds like two sets of rules, but I'm pretty comfortable
with using "out" in this scenario, and it avoids the 2 calls for
CanParse, and avoids the additional repeated .Value calls for
Nullable<T> (which can only really for for struct anyway); in fact,
I'm almost wondering whether TryMoveNext would have made a better
pattern for IEnumerator[<T>] (following on from a conversation about
the meaning of .Current when the enumerator is in the before/after
state) - especially since the consuming code is usually owned by the
compiler (foreach).

But yes; in everyday use on typical methods, "out" and "ref" should be
treated with caution. Not "do not use", but just a double-check that
this is the most reasonable design.

Marc
 
W

Weeble

Anthony Jones said:
Well thats the crux of the matter isn't it? Why is it "silly" ?

Apart from anything else, I would point out that the "try it and see
if it worked" strategy is more general than the "look before you leap"
strategy. For example, when opening a file, no amount of checking
whether the file is there and whether you have appropriate permissions
will be sufficient to ensure that opening the file succeeds. So even
if you encourage "look before you leap" where possible, people will
still need to be familiar with "try it and see if it worked" for
reasons that have nothing to do with performance and everything to do
with correctness.

Regards,
Weeble.
 
A

Anthony Jones

Marc Gravell said:
In general, I agree; but the "bool Try{Something}({args}, out result)"
is now quite common-place and /reasonably/ understood. For example,
int.TryParse, Dictionary<TKey, TValue>.TryGetValue, etc.


That is true, its the sort of thing that will get enough use that it may
become part of the expected structure of code. My concern is that
developers will take the view 'If its good enough for MS, it must be ok'
approach. I've seen that happening already and developers taking it as
carte blanche licence to use out parameters left, right and centre citing
TryParse as a similar pattern to what they are doing.
Maybe it sounds like two sets of rules, but I'm pretty comfortable
with using "out" in this scenario, and it avoids the 2 calls for
CanParse,

Why are you 'comfortable'? Could it be _because_ it avoids 2 calls like
CanParse would need?
 
M

Marc Gravell

Why are you 'comfortable'?  Could it be _because_ it avoids 2 calls like
CanParse would need?

Being *forced* to make 2 calls would make me actively uncomfortable,
but that isn't what I meant; it was the "out" usage that I am
comfortable with here (where-as in most circumstances I would question
the wisdom of out/ref re the ongoing maintenance of the system). The
other options single-call, non-throwing options (Nullable<T>, or some
bool/T pair being returned) seem just a little awkward. But in raw
performance terms, it would seem that an immutable struct (like
Nullable<T>, but where T can be anything, not just struct) should be
about the same in terms of call-stack etc.

But the Try{...} pattern; I can't explain it - but it "works" for me;
one thing I like is that I can test the result all in one line; i.e.

if(blah.Try{..., out value}) {
// use value
}

rather than having to:

resultPair = blah.Try{...};
if(resultPair .HasValue) {
// use result.Value
}

And yes; I know I need to declare "value" in the first example, but
often I am doing multiple Try calls with the same out value - meaning
I only declare it once - where-as the resultPair approach has to be
used on every call... trivial, and not a good reason to like/dislike
it - but there it is. (I did say I couldn't explain it!)

Marc
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top