With the C# quasi-mixin pattern, one of the biggest limitations is that the mixins can't introduce state to a class. An alternative was for the mixin interface to require the composing classes to implement a property that the mixin would use as its own, as demonstrated in this example.
In the .NET Framework 4.0, the ConditionalWeakTable<TKey, TValue> class can be used to associate arbitrary state to any instance. It's thread safe and it keeps a dictionary of weak references from the target instance to its associated state. If a target instance has no references outside the conditional weak table, then it can be reclaimed by the garbage collector. When that happens, its entry in the table is removed.
This makes it possible for a mixin-like class to maintain state for the instances it extends. Let's look at a mixin for a message container:
public enum MessageType {
Error,
Warning,
Info
}
public class Message {
public readonly MessageType Type;
public readonly string Text;
public Message(MessageType type, string text) {
Text = text;
Type = type;
}
public static Message Error(string text) {
return new Message(MessageType.Error, text);
}
// other factory methods...
}
// this is the mixin
public interface MMessageContainer { } public static class MessageContainerCode {
private static readonly ConditionalWeakTable<MMessageContainer, List<Message>>
_messagesTable = new ConditionalWeakTable<MMessageContainer, List<Message>>();
public static IEnumerable<Message> RetrieveMessages(this MMessageContainer self) {
return _messagesTable.GetOrCreateValue(self);
}
public static void AddMessage(this MMessageContainer self, Message message) {
_messagesTable.GetOrCreateValue(self).Add(message);
}
public static void ClearMessages(this MMessageContainer self) {
_messagesTable.GetOrCreateValue(self).Clear();
}
}
The MMessageContainer mixin (actually, its code class) declares a ConditionalWeakTable field to hold the list of messages for each extended class. The GetOrCreateValue method takes the target instance as a key and retrieves the associated list of messages, or creates a new one by calling the default constructor for List<Message>. The mixin can be used like this:
public class Person : MarshalByRefObject, MMessageContainer {
public string Name { get; set; }
public int Age { get; set; }
public void Validate() {
// needs to use "this" to access extensions to this class
this.ClearMessages();
if (Age < 0 || Age > 120) {
this.AddMessage(Message.Error("Invalid age"));
}
if (string.IsNullOrWhiteSpace(Name)) {
this.AddMessage(Message.Error("Invalid name"));
}
}
}
The class Person extends MarshalByRefObject to support remoting, and also uses the message container mixin to maintain a list of validation messages. Let's use this class now:
class Program {
static void Main(string[] args) {
var jeanne = new Person { Name = "Jeanne Calment", Age = 122 };
jeanne.Validate();
PrintMessages(jeanne);
}
private static void PrintMessages(MMessageContainer container) {
foreach (var message in container.RetrieveMessages()) {
Console.WriteLine("{0}: {1}", message.Type, message.Text);
}
}
}
By running it we find out that Jeanne Calment actually had an invalid age when she passed away. Or is it the program that's invalid?
Error: Invalid age
To associate a number of different values for a class, the mixin can declare a nested state class and use it as a value for the ConditionalWeakTable. So, here's the pattern to use:
public interface MMixin {
// required members go here
}
public static class MixinCode {
// provided methods go here, as extension methods to MMixin
// to maintain state:
class State {
// public fields or properties for the desired state, e.g.:
public string Name;
}
private static readonly ConditionalWeakTable<MMixin, State>
_stateTable = new ConditionalWeakTable<MMixin, State>();
// to access the state:
public static string GetName(this MMixin self) {
return _stateTable.GetOrCreateValue(self).Name;
}
public static void SetName(this MMixin self, string value) {
_stateTable.GetOrCreateValue(self).Name = value;
}
}
There're still limitations to this approach, but most of them are only related to syntax. For example, the mixin still has to be declared with two constructs, an interface and a static class; and it's weird to provide explicit getter and setter methods to create "extension properties", when the language has a nice property syntax. Also, any code that uses the mixin composition classes must also import the mixin namespace in order to access its introduced extensions.
Amazing. Thanks for sharing!
ReplyDelete