NOTE: this is the second part of this article. Please read it if haven’t yet.
The interface
Let’s start from scratch. You know all the things you read in previous article, but you have no code at all.
So we said that SAM interfaces are technically equivalent to functions (to be precise: Func
or Action
depending on return type). So, let’s see where it can get us.
Knowing the interfaces were declared like this:
1 | public interface ILogger { |
we can redefine them as:
1 | // pseudo-code |
Now, we can substitute ILogger
actual type (Action<...>
) as ILoggerFactory
output type, receiving:
1 | // Pseudo-code |
So, the interface to logging framework is just Func<string, Action<Severity, Func<string>>>
, and next time someone ask you on the tube, “Hey man, what’s your Func<string, Action<Severity, Func<string>>>
?” you can just tell him: “I’m using log4net, matey”.
In my opinion, using descriptive names depends on scope. I would actually encourage you to use just Func<T>
for locally scoped or internal factory, or i
as an index in 3-line for statement but for objects which are known by entire application I would prefer longer, more distinguished names.
In case of “logging framework interface” I would suggest quite descriptive names. Yes, it is just a function, but it would be much more readable if we call it LoggerFactory
.
Let’s do it then:
1 | public enum Severity { Trace, Debug, Info, Warn, Error, Fatal } |
That’s our interface.
You can almost go home now
So, the NLog implementation we did in previous article will look now a little bit different:
1 | public static LoggerFactory NLogLoggerFactory() |
You can see that we declared a factory method which can be called LoggerFactory
constructor. A LoggerFactory
will construct a Logger
every time you call it with logger name (name => { ... }
). A Logger
will log the message when called with severity and (message) builder ((severity, builder) => { ... }
).
If you need simplest possible implementations of LoggerFactory
I would say it would NullLoggerFactory
followed by ConsoleLoggerFactory
:
1 | public static LoggerFactory NullLoggerFactory() |
Let’s look at specific usage. You have to create a logger factory first (you need one of those):
1 | var loggerFactory = NLogLoggerFactory(); // or ConsoleLoggerFactory() ? |
Then, you can create loggers when you need them:
1 | var logger = loggerFactory("default"); |
and log messages like you always did:
1 | logger(Severity.Warn, () => "Something is rotten in the state of Denmark"); |
You can also just sprint through all the layers:
1 | loggerFactory("root")(Severity.Trace, () => "Adhoc message"); |
Job’s done. That’s what we wanted.
It is not exactly the same, right?
It is not. There is only one way to use it but wasn’t it a good thing?. Joking aside, we want our extension methods back. Good news is, all you need is to ask:
1 | public static class LoggerFactoryExtensions |
So now you can use extension methods on LoggerFactory
or use LoggerFactory
as a function (yes, it may look confusing):
1 | // just call *it* |
Normalizing Logger interface
In previous article we’ve introduced message Severity
to reduce number of overloads, but when we added convenience methods there was 24 of them again (still better than 24 to be implemented on interface though). Let’s try to apply same tactics and normalize convenience layer as well. We can create new “interface” (it’s not technically an interface
but it is still an interface). Let’s call it Channel
(like Trace, Debug or Error channel).
1 | public delegate void Channel(Func<string> factory); |
As you can see, Channel
is like a Logger
, but without Severity
, as Severity
has been already applied. We can add some convenience methods as well:
1 | public static partial class LoggerExtensions |
Now you can use like this:
1 | var channel = logger.Trace(); |
There is still only one way to format a message (using Func<string>
). Although, we have a type (Channel
) we can add extensions methods to it:
1 | public static class ChannelExtensions |
So, now we have all convenience methods back:
1 | var logger = loggerFactory.Logger(); // for this class |
Syntactically, I don’t like the parenthesis after channel (Trace, Debug, etc.). Unfortunately, we don’t have extension properties yet (C# 8, maybe?). As soon as they are available, they could be used to strip parenthesis from channel selection and produce prettier code:
1 | var logger = loggerFactory.Logger(); // for this class |
Nice, isn’t it?
Full implementation
So, complete implementation of the interface with extension methods is:
While adapter for NLog could be implemented like:
What can be added?
Definitely, message expansion (builder()
) should be wrapped in try...catch
so badly formatted message cannot blow up the application. It would suggest doing it in Logger.Channel(this Logger logger, Severity severity)
as everything ultimately goes through this function (yes, it is by design - there is only one place where it needs to be done).
I would also invest in better error.ToString
to log exception. Some Func<Exception, string>
(of course) which would explain the exception, processing inner exceptions and stack traces.
Convenience methods can be still added to Logger
: like Log(Severity, ...)
, and all 24 overloads for Trace(...)
, Debug(...)
, etc. I didn’t do it here, as they are not really needed anymore, but if you really like them there is no problem to do so (see GitHub project for T4 implementation).
Sources
This project is available on GitHub. Please note, this is just a toy project to present some ideas rather than production ready solution.