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