“Any sufficiently advanced technology is indistinguishable from magic”
Athur C. Clarke
While reviewing some code, I stumbled upon a class that receives an ObservableCollection<ILogFormatter>
in the constructor. I discovered that this collection is never explicitly initialized. Instead, it is magically constructed by the Unity container and injected into the class.
According to the documentation, Unity container resolves IEnumerable<T>
to a sequence that contains one object for each registration of type T. ObservableCollection<T>
is not IEnumerable<T>
, but it has a constructor that takes an IEnumerable<T>
, so Unity resolves it recursively.
Our code used this feature to inject a list of globally-defined formatters into the logger.
class Logger { public Logger(ObservableCollection<ILogFormatter> formatters) {...} } ... var container = new UnityContainer() .RegisterType<ILogFormatter, ParenthesisFormatter>("Parenthesis") .RegisterType<ILogFormatter, CurrentTimeFormatter>(); var logger = container.Resolve<Logger>(); logger.Log("First message"); logger.Log("Second message");
This prints each message adding parenthesis, courtesy of the first formatter, and current time, thanks to the second formatter:
03:48:43.928 (First message)
03:48:43.930 (Second message)
Of course, DI containers are designed to “magically” construct our classes like Santa elves, but to my taste, this is too much magic. Not only the reader must be aware of this obscure feature of Unity to understand the code, he also must remember what objects can be constructed, directly or indirectly from IEnumerable<T>
. To top it up, Unity treats IEnumerable<T>
and T[]
slightly differently.
I created a sample that mimics this code in branch “magic”, and a refactored version where formatter registration is explicit in branch “master”. The refactored code is larger, but is much easier to understand and contains less magic. In refactored populating the list of formatters is explicit:
class DefaultFormatterList { public DefaultFormattersList Add<T>() where T : ILogFormatter; public ReadOnlyCollection<ILogFormatter> FormattersList { get; } } container.Resolve<DefaultFormattersList>() .Add<ParenthesisFormatter>() .Add<CurrentTimeFormatter>();
Also, in the spirit of the single responsibility principle LoggerWithFormatters
is a decorator: it takes a plain logger in the constructor and adds the formatters to it:
class LoggerWithFormatters : ILogger { public LoggerWithFormatters( ILogger upstream, IReadOnlyCollection<ILogFormatter> formatters); }
Container registration then looks as follows:
container
.RegisterType<DefaultFormattersList>(new ContainerControlledLifetimeManager())
.RegisterFactory<ILogger>(c =>
new LoggerWithFormatters(
c.Resolve<ConsoleLogger>(),
c.Resolve<DefaultFormattersList>().FormattersList));
This may look more complicated then the original, but it tells us exactly what is going on: we resolve ILogger interface by creating an instance of LoggerWithFormatters
and passing some parameters to it.
By replacing “magic” container feature with explicit formatter list, we achieved a number of important advantages:
- No more astonishment, “how this can possibly work?”. This is somewhat subjective, but the feature of the container to supply a collection is not very well known.
- The collection of formatters can be easily examined and debugged. If we use container regisrations, the collection of formatters is buried somewhere inside the container’s private fields.
- We can add locking, sorting, logging (“added formater XYZ”), etc. to the formatters collection
- No more redundant registration names
The moral of the story is that at times relying on the obscure features of the library makes the code difficult to maintain and reason about. Even if a feature is there, it does not mean it necessarily must be used.
PS. ObservableCollection<T>
actually has two 1-parameter constructors: one accepting IEnumerable<T>
and one taking List<T>
. Older versions of Unity would refuse to construct such as class due to ambiguity, but the modern version just takes the first suitable constructor in the order of declaration, which happens to be IEnumerable<T>
. However, even if it were the the List<T>
constructor, the result would have been the same, since List<T>
has a constructor that takes IEnumerable<T>
. On the other hand, if the other constructor accepted T[]
, the result would have been different, and hard to debug, as unlike the enumerable, the array does not include the implicit (unnamed) type registration.