patterncsharpMinor
Using TryXXX pattern to avoid exceptions
Viewed 0 times
exceptionstryxxxavoidusingpattern
Problem
I have been using the TryXXX and GetXXX pattern lately to give clients a choice whether to trap an exception or to read a boolean. An example of this concept would be
It is my understanding that you should get two benefits from this:
In this class, I am attempting to do this, but I think I am getting only benefit 1, not benefit 2. I am drawing a blank as how to not throw any exception without code duplication.
```
Public Function ConvertFractionToDecimal(ByVal fraction As String) As Decimal
If String.IsNullOrWhiteSpace(fraction) Then Throw BuildBadFractionException(fraction)
'will accept fractions of the form:
'X-Y/Z
'X Y/Z
'Y/Z
'will not accept negative signs in numerator, denominator, or whole number
Dim whole As String
Dim numen As String
Dim denom As String
Dim loc1 As Integer
Dim loc2 As Integer
fraction = fraction.Trim
If fraction Like "?-?/?*" Then
loc1 = InStr(fraction, "-")
loc2 = InStr(fraction, "/")
ElseIf fraction Like "? ?/?*" Then
loc1 = InStr(fraction, " ")
loc2 = InStr(fraction, "/")
ElseIf fraction Like "?/?" Then
loc2 = InStr(fraction, "/")
Else
Throw BuildBadFractionException(fraction)
End If
If loc1 > 0 Then
whole = Mid(fraction, 1, loc1 - 1).Trim
Else
whole = "0"
End If
numen = Mid(fraction, loc1 + 1, loc2 - loc1 - 1).Trim
denom = Mid(fraction, loc2 + 1).Trim
Dim Uwhole As UInt32
Dim Unumen As UInt32
Dim Udenom As UInt32
If Not UInt32.TryParse(whole, Uwhole) Then Throw BuildBadFractionException(fraction)
If Not UInt32.TryParse(numen, Unumen) Then Thr
System.Integer.TryParse and System.Integer.Parse.It is my understanding that you should get two benefits from this:
- using an if statement in calling code instead of try-catch
- a performance benefit from not causing an exception.
In this class, I am attempting to do this, but I think I am getting only benefit 1, not benefit 2. I am drawing a blank as how to not throw any exception without code duplication.
```
Public Function ConvertFractionToDecimal(ByVal fraction As String) As Decimal
If String.IsNullOrWhiteSpace(fraction) Then Throw BuildBadFractionException(fraction)
'will accept fractions of the form:
'X-Y/Z
'X Y/Z
'Y/Z
'will not accept negative signs in numerator, denominator, or whole number
Dim whole As String
Dim numen As String
Dim denom As String
Dim loc1 As Integer
Dim loc2 As Integer
fraction = fraction.Trim
If fraction Like "?-?/?*" Then
loc1 = InStr(fraction, "-")
loc2 = InStr(fraction, "/")
ElseIf fraction Like "? ?/?*" Then
loc1 = InStr(fraction, " ")
loc2 = InStr(fraction, "/")
ElseIf fraction Like "?/?" Then
loc2 = InStr(fraction, "/")
Else
Throw BuildBadFractionException(fraction)
End If
If loc1 > 0 Then
whole = Mid(fraction, 1, loc1 - 1).Trim
Else
whole = "0"
End If
numen = Mid(fraction, loc1 + 1, loc2 - loc1 - 1).Trim
denom = Mid(fraction, loc2 + 1).Trim
Dim Uwhole As UInt32
Dim Unumen As UInt32
Dim Udenom As UInt32
If Not UInt32.TryParse(whole, Uwhole) Then Throw BuildBadFractionException(fraction)
If Not UInt32.TryParse(numen, Unumen) Then Thr
Solution
I was going to edit my first answer, but as I wrote the example code I wanted to show, and refactored, and refactored again, ...the result went so far away from the original code that I thought it warranted a separate answer.
So I copied your code into Visual Studio, and started by implementing the changes I suggested in my other answer. Quickly though, I felt the need to extract a
Being a
So the
And then I thought "great, now I can get rid of pretty much the whole rest of the code!" ...and then it struck me: you're not showing where your
And to follow the single responsibility principle, the only logical place to put such conversion methods would be in a value type called
I suggest you take a look at this question and answers (disclaimer: it's one of my questions on this site) - it's C#, but the basic idea is essentially the same.
After much refactoring, this is what I ended up with - I'm not fully satisfied with it because it will throw a
These functions are
```
Public Structure Fraction
Private Const pattern As String = "^((?[0-9]+)?\s+?)?(?[0-9]+)\s?/\s?(?[0-9]+)\s*?$"
Private Shared ReadOnly regexp As Regex = New Regex(pattern)
Private Shared ReadOnly emptyValue As Fraction = New Fraction()
Private ReadOnly WholePart As Integer
Private ReadOnly NumeratorPart As Integer
Private ReadOnly DenominatorPart As Integer
Public Sub New(ByVal whole As Integer, ByVal numerator As Integer, ByVal denominator As Integer)
WholePart = whole
NumeratorPart = numerator
DenominatorPart = denominator
End Sub
Public ReadOnly Property Whole() As Integer
Get
Return WholePart
End Get
End Property
Public ReadOnly Property Numerator As Integer
Get
Return NumeratorPart
End Get
End Property
Public ReadOnly Property Denominator As Integer
Get
Return DenominatorPart
End Get
End Property
Public Shared ReadOnly Property Empty As Fraction
Get
Return emptyValue
End Get
End Property
Public Function ToDecimal() As Decimal
Return Whole + CDec(Numerator) / CDec(Denominator)
End Function
Public Overrides Function Eq
So I copied your code into Visual Studio, and started by implementing the changes I suggested in my other answer. Quickly though, I felt the need to extract a
FractionInfo type, a Structure that would hold the Whole, Numerator and Denominator parts of a fraction, so that I could write the regex part into a separate function that would return an instance of this FractionInfo type.Being a
Structure, I wanted to make that type immutable, so I implemented an Empty property that returned a Shared default instance... and then I tried doing If result = FractionInfo.Empty and noticed the = operator needed to be implemented - I'm lazy, so instead I just implemented Equals, and since when you override Equals you also need to override GetHashCode, I implemented GetHashCode as well.So the
ConvertFractionToDecimal function started like this:Dim info As FractionInfo = GetFractionInfo(value)
If info.Equals(FractionInfo.Empty) Then Throw BuildBadFractionException(value)And then I thought "great, now I can get rid of pretty much the whole rest of the code!" ...and then it struck me: you're not showing where your
Convert.../TryConvert... methods are written, but I'm assuming they're not written in a Structure called Fraction.And to follow the single responsibility principle, the only logical place to put such conversion methods would be in a value type called
Fraction.I suggest you take a look at this question and answers (disclaimer: it's one of my questions on this site) - it's C#, but the basic idea is essentially the same.
After much refactoring, this is what I ended up with - I'm not fully satisfied with it because it will throw a
FormatException whenever parsing fails, even when one would expect an OverflowException. It does throw an ArgumentNullException when you give it Nothing, but the produced stack trace might be a little surprising.Private Shared Function TryParseMatchGroups(ByVal groups As Match, ByRef result As Fraction) As Boolean
Dim success As Boolean
Dim wholePart As String = groups.Groups("whole").Value
Dim numeratorPart As String = groups.Groups("numerator").Value
Dim denominatorPart As String = groups.Groups("denominator").Value
Dim whole As Integer
Dim numerator As Integer
Dim denominator As Integer
success = Integer.TryParse(IIf(String.IsNullOrEmpty(wholePart), "0", wholePart), whole) _
And Integer.TryParse(IIf(String.IsNullOrEmpty(numeratorPart), "0", numeratorPart), numerator) _
And Integer.TryParse(IIf(String.IsNullOrEmpty(denominatorPart), "0", denominatorPart), denominator)
If success Then
result = New Fraction(whole, numerator, denominator)
Else
result = Fraction.Empty
End If
Return success
End Function
Public Shared Function Parse(ByVal value As String) As Fraction
Dim result As Fraction
Dim match As Match = regexp.Match(value)
If match.Success And TryParseMatchGroups(match, result) Then
Return result
Else
Throw New FormatException()
End If
End Function
Public Shared Function TryParse(ByVal value As String, ByRef result As Fraction) As Boolean
If value Is Nothing Then Return False
Dim match As Match = regexp.Match(value)
Return match.Success And TryParseMatchGroups(match, result)
End FunctionThese functions are
Shared, exactly like Decimal.Parse and Decimal.TryParse are. Here's the rest of the type:```
Public Structure Fraction
Private Const pattern As String = "^((?[0-9]+)?\s+?)?(?[0-9]+)\s?/\s?(?[0-9]+)\s*?$"
Private Shared ReadOnly regexp As Regex = New Regex(pattern)
Private Shared ReadOnly emptyValue As Fraction = New Fraction()
Private ReadOnly WholePart As Integer
Private ReadOnly NumeratorPart As Integer
Private ReadOnly DenominatorPart As Integer
Public Sub New(ByVal whole As Integer, ByVal numerator As Integer, ByVal denominator As Integer)
WholePart = whole
NumeratorPart = numerator
DenominatorPart = denominator
End Sub
Public ReadOnly Property Whole() As Integer
Get
Return WholePart
End Get
End Property
Public ReadOnly Property Numerator As Integer
Get
Return NumeratorPart
End Get
End Property
Public ReadOnly Property Denominator As Integer
Get
Return DenominatorPart
End Get
End Property
Public Shared ReadOnly Property Empty As Fraction
Get
Return emptyValue
End Get
End Property
Public Function ToDecimal() As Decimal
Return Whole + CDec(Numerator) / CDec(Denominator)
End Function
Public Overrides Function Eq
Code Snippets
Dim info As FractionInfo = GetFractionInfo(value)
If info.Equals(FractionInfo.Empty) Then Throw BuildBadFractionException(value)Private Shared Function TryParseMatchGroups(ByVal groups As Match, ByRef result As Fraction) As Boolean
Dim success As Boolean
Dim wholePart As String = groups.Groups("whole").Value
Dim numeratorPart As String = groups.Groups("numerator").Value
Dim denominatorPart As String = groups.Groups("denominator").Value
Dim whole As Integer
Dim numerator As Integer
Dim denominator As Integer
success = Integer.TryParse(IIf(String.IsNullOrEmpty(wholePart), "0", wholePart), whole) _
And Integer.TryParse(IIf(String.IsNullOrEmpty(numeratorPart), "0", numeratorPart), numerator) _
And Integer.TryParse(IIf(String.IsNullOrEmpty(denominatorPart), "0", denominatorPart), denominator)
If success Then
result = New Fraction(whole, numerator, denominator)
Else
result = Fraction.Empty
End If
Return success
End Function
Public Shared Function Parse(ByVal value As String) As Fraction
Dim result As Fraction
Dim match As Match = regexp.Match(value)
If match.Success And TryParseMatchGroups(match, result) Then
Return result
Else
Throw New FormatException()
End If
End Function
Public Shared Function TryParse(ByVal value As String, ByRef result As Fraction) As Boolean
If value Is Nothing Then Return False
Dim match As Match = regexp.Match(value)
Return match.Success And TryParseMatchGroups(match, result)
End FunctionPublic Structure Fraction
Private Const pattern As String = "^((?<whole>[0-9]+)?\s+?)?(?<numerator>[0-9]+)\s?/\s?(?<denominator>[0-9]+)\s*?$"
Private Shared ReadOnly regexp As Regex = New Regex(pattern)
Private Shared ReadOnly emptyValue As Fraction = New Fraction()
Private ReadOnly WholePart As Integer
Private ReadOnly NumeratorPart As Integer
Private ReadOnly DenominatorPart As Integer
Public Sub New(ByVal whole As Integer, ByVal numerator As Integer, ByVal denominator As Integer)
WholePart = whole
NumeratorPart = numerator
DenominatorPart = denominator
End Sub
Public ReadOnly Property Whole() As Integer
Get
Return WholePart
End Get
End Property
Public ReadOnly Property Numerator As Integer
Get
Return NumeratorPart
End Get
End Property
Public ReadOnly Property Denominator As Integer
Get
Return DenominatorPart
End Get
End Property
Public Shared ReadOnly Property Empty As Fraction
Get
Return emptyValue
End Get
End Property
Public Function ToDecimal() As Decimal
Return Whole + CDec(Numerator) / CDec(Denominator)
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Dim other As Fraction = DirectCast(obj, Fraction) 'let it blow up on InvalidCastException
Return other.Whole = Me.Whole _
And other.Denominator = Me.Denominator _
And other.Numerator = Me.Numerator
End Function
Public Overrides Function GetHashCode() As Integer
'ok this is a bit like cheating.. but so much easier than implementing the real thing
Return Tuple.Create(Me.Whole, Me.Numerator, Me.Denominator).GetHashCode()
End Function
Public Overrides Function ToString() As String
If Me.Whole <> 0 Then
Return String.Format("{0} {1}/{2}", Me.Whole, Me.Numerator, Me.Denominator)
Else
Return String.Format("{0}/{1}", Me.Numerator, Me.Denominator)
End If
End FunctionContext
StackExchange Code Review Q#62552, answer score: 8
Revisions (0)
No revisions yet.