In these C# examples, a class has three properties. One .Text is required. Of the other two, one of them must be set. The IInterface declares these properties.
interface IInterface
{
string Text { get; } // required
// either AltRequiredNumber or AltRequiredString must be set
int AltRequiredNumber { get; }
string AltRequiredString { get; }
}
The Text property is defined in a base class.
abstract class BaseClass
{
public virtual string Text { get; set; }
}
Default Constructor
A class with a default constructor (to illustrate the problem) is derived from BaseClass and implements IInterface.class DefaultConstructor : BaseClass, IInterface
{
public DefaultConstructor() { }
public int AltRequiredNumber { get; set; }
public string AltRequiredString { get; set; }
}
When instantiated, the potential problems become clear. There is no help to remember to set the required properties or even to know which are required (except for the unacceptably intrusive "Required" in the property name). Additionally, because the interface only defines getters for the properties, the concrete class must be used to assign the property values. This also means their values cannot be immutable.
DefaultConstructor obj = new DefaultConstructor();
obj.Text = "The answer is {0}";
obj.AltRequiredString = "forty-two";
A C# object initializer helps a little, but there is still no validation that required fields have been assigned.
IInterface obj = new DefaultConstructor
{
Text = "The answer is {0}",
AltRequiredString = "forty-two"
};
Required Arguments Constructor
Following is a look at several alternatives to the default constructor. Declaring constructors with required parameters is a huge step in the right direction. Constructors with different signatures define which properties are required. However, accessing virtual members in the constructor may result in methods running on partially initialized objects.class RequiredArgumentsConstructor : BaseClass, IInterface
{
private RequiredArgumentsConstructor(string text)
{
Text = text; // WARNING: Virtual member call in constructor
}
public RequiredArgumentsConstructor(string text, int number) : this(text)
{
AltRequiredNumber = number;
}
public RequiredArgumentsConstructor(string text, string value) : this(text)
{
AltRequiredString = value;
}
public int AltRequiredNumber { get; private set; }
public string AltRequiredString { get; private set; }
}
Another risk when calling the constructor is that the arguments will be passed in an incorrect order, especially if the order changes in the future.
IInterface obj = new RequiredArgumentsConstructor("The answer is {0}", "forty-two");
Immutable Constructor
In RequiredArgumentsConstructor, the properties are private, but not immutable (i.e., readonly). The following class has readonly fields. As of C# 6, the backing field is no longer needed.class ImmutableConstructor : BaseClass, IInterface
{
readonly int _requiredNumber;
readonly string _requiredString;
public ImmutableConstructor(string text, int number = 0, string value = null)
{
Text = text; // WARNING: Virtual member call in constructor
_requiredNumber = number;
_requiredString = value;
}
public int AltRequiredNumber { get { return _requiredNumber; } }
public string AltRequiredString { get { return _requiredString; } }
}
With optional parameters on the constructor, using named arguments mitigates the order problem when passing arguments.
IInterface obj = new ImmutableConstructor("The answer is {0}", value: "forty-two");
Factory Constructor
The Factory pattern offers the benefit of separating object creation from object initialization.class FactoryConstructor : BaseClass, IInterface
{
public static FactoryConstructor Create(string text, int number)
{
return new FactoryConstructor(number: number) { Text = text };
}
public static FactoryConstructor Create(string text, string value)
{
return new FactoryConstructor(value: value) { Text = text };
}
readonly int _requiredNumber;
readonly string _requiredString;
private FactoryConstructor(int number = 0, string value = null)
{
_requiredNumber = number;
_requiredString = value;
}
public int AltRequiredNumber { get { return _requiredNumber; } }
public string AltRequiredString { get { return _requiredString; } }
}
The base class and any derived classes are instantiated prior to setting the Text property. The new keyword is avoided. Control of the constructor is in the hands of the class itself.
IInterface obj = FactoryConstructor.Create("The answer is {0}", "forty-two");
Fluent Constructor
Lastly, the Fluent Constructor pattern provides even tighter control over the instantiation of the object. In short, the fluent interface uses method chaining popular in jQuery (JavaScript) and LINQ (C#). This is how the constructor would be called.
IInterface fluentConstructor = FluentConstructor.Text("The answer is {0}").AltRequiredString("forty-two");
The beauty of this pattern is seen in the Intellisense. After typing FluentConstructor., Text is the only method because it is required. Once Text() is called, both AltRequiredNumber and AltRequiredString are available because one or the other is required. The order is enforced and the arguments are named leaving very safe and readable code. Additionally, no virtual members are called in the constructor. The downside is that only the first set of arguments can be readonly (in this example, the second argument should be readonly, not the first). Additionally, whether good or bad, the resulting object is defined solely by the interface, not the base class because the Context class is private.
static class FluentConstructor
{
public static IRequiredValue Text(string text)
{
var context = new Context();
return ((IRequiredText)context).Text(text);
}
public interface IRequiredText
{
IRequiredValue Text(string text);
}
public interface IRequiredValue
{
IInterface AltRequiredNumber(int number);
IInterface AltRequiredString(string value);
}
private class Context : BaseClass, IRequiredText, IRequiredValue, IInterface
{
public Context() { }
public int AltRequiredNumber { get; private set; }
public string AltRequiredString { get; private set; }
IRequiredValue IRequiredText.Text(string text)
{
Text = text;
return this;
}
IInterface IRequiredValue.AltRequiredNumber(int number)
{
AltRequiredNumber = number;
return this;
}
IInterface IRequiredValue.AltRequiredString(string value)
{
AltRequiredString = value;
return this;
}
}
}
No comments:
Post a Comment