|
Many times a method needs to perform a series of steps. The problem is that one of these steps can fail. Usually, this results in an exception being thrown from the source of the failure. When this happens, it's important that the object whose method is being invoked is left in a valid state.
Here is an example of the problem:
public class MyClass
{
private SomeObject obj;
private int a;
private int b;
public MyClass(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.obj = obj;
this.a = obj.CalculateSomething();
this.b = obj.CalculateSomethingElse();
}
public void DoSomething()
{
a = obj.CalculateSomething();
b = obj.CalculateSomethingElse();
}
}
Now in the above example, suppose that the call in DoSomething to the CalculateSomething method succeeds but that the call following it to CalculateSomethingElse fails and an exception is thrown. Also suppose that a and b should always be updated together. If one is updated successfully and the other is not, the object is no longer in a valid state. It's not hard to think of "real" examples of this type of situation, e.g. updating values in an object by loading data from a file (suppose a read operation fails?), but here we'll just have to assume this is the case.
A way to circumvent this is to cache the results from each step temporarily and update the object at the end of the method. At this point, all of the steps have been completed successfully and we can update our object while ensuring that its invariants are maintained.
public class MyClass
{
private SomeObject obj;
private int a;
private int b;
public MyClass(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.obj = obj;
this.a = obj.CalculateSomething();
this.b = obj.CalculateSomethingElse();
}
public void DoSomething()
{
int tempA = obj.CalculateSomething();
int tempB = obj.CalculateSomethingElse();
a = tempA;
b = tempB;
}
}
In the second example, the calls to CalculateSomething or CalcualteSomethingElse can fail without our object being left in an invalid state.
When it's necessary to perform many steps in a method, caching the results can get complicated. My current approach is view this as an opportunity to refactor. What I do is take the values that need updating and put them into a seperate class. The previous example then looks like this:
public class RefactoredValues
{
private int a;
private int b;
public RefactoredValues(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.a = obj.CalculateSomething();
this.b = obj.CalculateSomethingElse();
}
public int A
{
get
{
return a;
}
}
public int B
{
get
{
return b;
}
}
}
public class MyClass
{
SomeObject obj;
RefactoredValues v;
public MyClass(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.obj = obj;
v = new RefactoredValues(obj);
}
public void DoSomething()
{
v = new RefactoredValues(obj);
}
}
Any time DoSomething is called, it creates a new object representing the updated values. If an exception is thrown, the MyClass 's values are left intact.
-- modified at 14:09 Wednesday 27th June, 2007
|
|
|
|