This is a C# version of a post I did a while ago. Hope it's of any use.
Test Data Builders
Sometimes, when writing unit or acceptance tests, it's a good idea to use Test Data Builder. For example, let's take a network frame that has two fields - one for source, one for destination. A builder for such frame could look like this:
public class FrameBuilder
{
private string _source;
private string _destination;
public FrameBuilder Source(string newSource)
{
_source = newSource;
return this;
}
public FrameBuilder Destination(string newDestination)
{
_destination = newDestination;
return this;
}
public Frame Build()
{
var frame = new Frame()
{
Source = _source,
Destination = _destination,
};
return frame;
}
}
and it can be used like this:
var frame = new FrameBuilder().Source("A").Destination("B").Build();
The issue with Test Data Builder method reuse
The pattern is fairly easy, but things get complicated when we have a whole family of frames, each sharing the same set of fields. If we wanted to write a separate builder for each frame, we'd end up duplicating a lot of code. So another idea is inheritance. However, taking the naive approach gets us into some trouble. Let's see it in action:
public abstract class FrameBuilder
{
protected string _source;
protected string _destination;
public FrameBuilder Source(string newSource)
{
_source = newSource;
return this;
}
public FrameBuilder Destination(string newDestination)
{
_destination = newDestination;
return this;
}
public abstract Frame Build();
};
public class AuthorizationFrameBuilder : FrameBuilder
{
private string _password;
public AuthorizationFrameBuilder Password(string newPassword)
{
_password = newPassword;
return this;
}
public override Frame Build()
{
var authorizationFrame = new AuthorizationFrame()
{
Source = _source,
Destination = _destination,
Password = _password,
};
return authorizationFrame;
}
}
The difficulty with this approach is that all calls to FrameBuilder methods return a reference to FrameBuilder, not an AuthorizationFrameBuilder, so we cannot use calls from the latter after calls from the first. E.g. we cannot make a chain like this:
new AuthorizationFrameBuilder().Source("b").Password("a").Build();
This is because Source() method returns FrameBuilder, which doesn't include a method called Password() at all. Such chains cause compile errors.
Generics to the rescue!
Fortuntely, there's a solution for this. Generics! Yes, they can help us here, but in order to do this, we have to use a trick that in C++ world is called "Curiously Recurring Template Pattern" (don't know if it has any name in the C# world). By using the trick, we'll force the FrameBuilder (superclass) methods to return reference to its subclass (AuthorizationFrameBuilder) - this will allow us to mix methods from FrameBuilder and AuthorizationFrameBuilder in any order in a chain, because each method, not matter which of the classes it is defined in, returns a reference to a subclass.
Let's see how this works out in the following example:
//thankfully, the following is legal :-)
public class FrameBuilder<T> where T : FrameBuilder<T>
{
protected string _source;
protected string _destination;
public T Source(string newSource)
{
_source = newSource;
return this as T;
}
public T Destination(string newDestination)
{
_destination = newDestination;
return this as T;
}
}
public class AuthorizationFrameBuilder
: FrameBuilder<AuthorizationFrameBuilder>
{
string _password;
public AuthorizationFrameBuilder Password(string password)
{
_password = password;
return this;
}
public AuthorizationFrame Build()
{
var frame = new AuthorizationFrame()
{
Source = _source,
Destination = _destination,
Password = _password,
};
return frame;
}
}
Note that in FrameBuilder, the
T This
{
get { return this as T; }
}
public T Source(string newSource)
{
_source = newSource;
return This;
}
Summary
This solution makes it easy to reuse any number of methods in any number of different builders, so it's a real treasure when we've got many data structures that happen to share some common fields.
That's all for today - Hope you enjoy the C#-specific version of the solution. By the way, in contrast to C++, C# allows one or two solutions other than chained methods to achieve the same data-building result. Can you name them?
No comments:
Post a Comment