I know, I know. You can't override static methods. The title was just a trick to provoke your interest :-) In this post, I'll first try to explain why it is impossible to override static methods and then provide two common ways to do it.
Or rather - two ways to achieve the same effect.
So, what's the problem?
There are situations where you wish you could substitute or extend functionality of existing static members - for example, provide different implementations for it and be able to switch implementations at runtime.
For example, let's consider a static class Log with two static methods:
public static class Log { public static void Message(string message){ ... } public static void Error(Exception exception){ ... } }
Let's say your code calls Log.Message and Log.Error all over the place and you would like to have different logging behaviors - logging to console and to the Debug/Trace listeners. Moreover, you would like to switch logging at runtime based on selected options.
Why can't we override static members?
Really, why? If you think about it, this is just common sense. Overriding usual (instance) members uses the virtual dispatch mechanism to separate the contract from the implementation. The contract is known at compile time (instance member signature), but the implementation is only known at runtime (concrete type of object provides a concrete implementation). You don't know the concrete type of the implementation at compile time.
This is an important thing to understand: when types inherit from other types, they fulfil a common contract, whereas static types are not bound by any contract (from the pure OOP point of view). There's no technical way in the language to tie two static types together with an "inheritance" contract. If you would "override" the Log method in two different places, how do we know which one we are calling here: Log.Message("what is the implementation?")
With static members, you call them by explicitly specifying the type on which they are defined. Which means, you directly call the implementation, which, again, is not bound to any contract.
By the way, that's why static members can't implement interfaces. And that's why virtual dispatch is useless here - all clients directly call the implementation, without any contract.
Let's get back to our problem
I won't even mention the "solution" with if/switch:
public static void Message(string message) { if (LoggingBehavior == LoggingBehavior.Console) { Console.WriteLine(message); } else if ... }
It is so ugly that my eyes start bleeding when I stare at it long enough. Why?
- You cannot add a new type of a log at runtime
- You cannot "override" the functionality at runtime or, say, wrap it in a decorator
- You have to modify every static method to add/change/remove a logging behavior
- all other 10000 reasons why OOP is better than procedural programming
But! We are so close to the first solution - every more or less experienced developer will cry out here: use the Strategy pattern!
Solution 1: Strategy + Singleton
Yes, that's easy. Define a contract to use and it will be automatically separated from the implementation. (Unless you make it sealed. By making a type or a member sealed, you guarantee that no one else can implement this contract.)
OK, so here's our contract:
public abstract class Logger { public abstract void Message(string message); public abstract void Error(Exception exception); }
You could make it an interface as well, but with an interface you wouldn't be able to change the contract later without breaking existing clients. I already wrote about choosing abstract class vs. interface.
You'll only need one instance of a logging behavior, so let's create a singleton:
public static class Log { public static Logger Instance { get; set; }
...
Correct Singleton implementation is not part of this discussion - there is a whole science of how to correctly implement Singleton in .NET - just use your favorite search engine if you're curious.
Now we just redirect the static methods to instance methods of the Logger instance and presto:
public static void Message(string message) { Instance.Message(message); }
And that's it! All of your code continues to use Log.Message and Log.Error, and if you'd like to change the behavior, just say
Log.Instance = new DebugWriteLineLogger();
At runtime! You could even wrap the instance using Decorators, watch it using Observers, broadcast using Composite, etc. etc.
Solution 2: delegates
When I was saying that static types do not have means to adhere to a contract, I was not quite correct. Delegates are a more fine-granular type of a contract, which regulates methods as opposed to types. Let's define two contracts:
public delegate void MessageLogger(string message); public delegate void ErrorLogger(Exception exception);
Now let's specify that our Log methods define a contract, not an implementation:
public static class Log { public static MessageLogger Message { get; set; } public static ErrorLogger Error { get; set; } }
We can still call Log.Message(string), but this time we can substitute an implementation at runtime:
Log.Message = Debug.WriteLine;
Also, instead of creating explicit delegates MessageLogger and ErrorLogger, we could reuse the System ones: Action and Action, which would work just as well.
Important update: originally I wrote here that you can convert between different delegate types as long as the method signature is the same (e.g. convert MessageLogger to Action and back). THIS IS WRONG. You can assign delegates of type MessageLogger and Action to point to the same method, but you can't assign them to each other. Delegate type inferencing only works when assigning methods to delegates and not delegates to delegates. I'm sorry about the confusion and thanks to Alex for pointing this out.
Of course, as with the Singleton from Solution 1, we have to make sure we initialize the default implementation before we call it, otherwise we'll get a null reference exception. We always have to be careful about initializing contracts with the default implementation.
Now that'd be funny - what if we want to log a null reference exception that Log.Error is null - how in the world are we supposed to do that?
As you can see, delegates are just as powerful as interfaces when specifying a contract for a single method as opposed to whole type. I already wrote about it too: Delegates as an alternative to single-method interfaces
A peculiarity with delegates is that you can't switch all the delegates at once, you have to do it one by one. This can be both an advantage and a disadvantage, depending on your situation. Also, there is no clear guidance as to where to put the implementation methods for delegates - they might end up being scattered all over the code.
Conclusion
We can use any of the two solutions above to change the logging implementation at runtime, even if the Log class is defined in a different assembly. Moreover, we can switch between the first and the second solution without changing the client source code. We will have to recompile though, because it changes the static methods on Log to static properties and vice versa (breaks binary compatibility, not source level compatibility).
I'm still not sure whether I like the Singleton or Delegates better. It probably depends on the situation. What do you think?
Комментариев нет:
Отправить комментарий