Original post at: https://siderite.dev/blog/better-options-than-object-and-collection-initiali
C# 3.0 introduced Object Initializer Syntax which was a game changer for code that created new objects or new collections. Here is a contrived example:
var obj = new ComplexObject
{
// object initializer
AnItem = new Item("text1"),
AnotherItem = new Item("text2"),
// collection initializer
RestOfItems = new List<Item>
{
new Item("text3"),
new Item("text4"),
new Item("text5")
},
// indexer initializer
[0]=new Item("text6"),
[1]=new Item("text7")
};
Before this syntax was available, the same code would have looked like this:
var obj = new ComplexObject();
obj.AnItem = new Item("text1");
obj.AnotherItem = new Item("text2");
obj.RestOfItems = new List<Item>();
obj.RestOfItems.Add(new Item("text3"));
obj.RestOfItems.Add(new Item("text4"));
obj.RestOfItems.Add(new Item("text5"));
obj[0] = new Item("text6");
obj[2] = new Item("text7");
It's not like the number of lines has changed, but both the writability and readability of the code increase with the new syntax. At least that's why I think. However, outside these very simple scenarios, the feature feels like it's encumbering us or that it is missing something. Imagine you want to only add items to a list based on some condition. You might get a code like this:
var list = new List<Item>
{
new Item("text1")
};
if (condition) list.Add(new Item("text2"));
We use the initializer for one item, but not for the other. We might as well use Add for both items, then, or use some cumbersome syntax that hurts more than it helps:
var list = new[]
{
new Item("text1"),
condition?new Item("text2"):null
}
.Where(i => i != null)
.ToList();
It's such an ugly syntax that Visual Studio doesn't know how to indent it properly. What to do? Software patterns to the rescue!
Seriously now, people who know me know that I scoff at the general concept of software patterns, but the patterns themselves are useful and in this case, even the conceptual framework that I often deride is useful here. Because we are trying to initialize an object or a collection, which means we are attempting to build it. So why not use a Builder pattern? Here are two versions of the same code, one with extension methods (which can be used everywhere, but might pollute the member list for common objects) and another with an actual builder object created specifically for our purposes (which may simplify usage):
// extension methods
var list = new List<Item>()
.Adding(new Item("text1"))
.ConditionalAdding(condition, new Item("text2"));
...
public static class ItemListExtensions
{
public static List<T> Adding<T>(this List<T> list, T item)
{
list.Add(item);
return list;
}
public static List<T> ConditionalAdding<T>(this List<T> list, bool condition, T item)
{
if (condition)
{
list.Add(item);
}
return list;
}
}
// builder object
var list = new ItemListBuilder()
.Adding("text1")
.ConditionalAdding(condition, "text2")
.Build();
...
public class ItemListBuilder
{
private readonly List<Item> list;
public ItemListBuilder()
{
list = new List<Item>();
}
public ItemListBuilder Adding(string text)
{
list.Add(new Item(text));
return this;
}
public ItemListBuilder ConditionalAdding(bool condition, string text)
{
if (condition)
{
list.Add(new Item(text));
}
return this;
}
public List<Item> Build()
{
return list.ToList();
}
}
Of course, for just a simple collection with some conditions this might feel like overkill, but try to compare the two versions of the code: the one that uses initializer syntax and then the Add method and the one that declares what it wants to do, step by step. Also note that in the case of the builder object I've taken the liberty of creating methods that only take string parameters then build the list of Item, thus simplifying the syntax and clarifying intent.
I had this situation where I had to map an object to another object by copying some properties into collections and values of some type to other types and so on. The original code was building the output using a top-down approach:
public Output BuildOutput(Input input) {
var output=new Output();
BuildFirstPart(output, input);
BuildSecondPart(output, input);
...
return output;
}
public BuildFirstPart(Output output, Input input) {
var firstSection = BuildFirstSection(input);
output.FirstPart=new List<Part> {
new Part(firstSection)
};
if (condition) {
var secondSection=BuildSeconfSection(input);
output.FirstPart.Add(new Part(secondSection));
}
}
And so on and so on. I believe that in this case a fluent design makes the code a lot more readable:
var output = new Output {
FirstPart = new List<Part>()
.Adding(BuildFirstSection(input))
.ConditionalAdding(condition, BuildSecondSection(input),
SecondPart = ...
};
The "build section" methods would also be inlined and replaced with fluent design methods. In this way the structure of "the output" is clearly shown in a method that declares what it will build and populates the various members of the Output class with simple calculations, the only other methods that the builder needs. A human will understand at a glance what thing it will build, see its structure as a tree of code and be able to go to individual methods to see or change the specific calculation that provides a value.
The point of my post is that sometimes the very out-of-the-box features that help us a lot most of the time end up complicating and obfuscating our code in specific situations. If the code starts to smell, to become unreadable, to make you feel bad for writing it, then stop, think of a better solution, then implement it so that it is the best version for your particular case. Use tools when they are useful and discard them when other solutions might prove more effective.
Top comments (1)
The tag
#c
is for C, not C Sharp, which is#csharp