About Me

Hello, I'm an eclectic software professional working with enterprise software at SAP. This blog only contains my personal views, thoughts and opinions. It is not endorsed by SAP nor does it constitute any official communication of SAP.

Friday, February 12, 2010

C# Quasi-Mixins Example

As an example, I'll adapt one of the mixins found in the book Fundamentals of Object-Oriented Design in UML to use the C# quasi-mixins pattern.

Given the following (simplified) domain classes:

namespace Domain {

  public enum Medium { 
    Email,
    SnailMail, 
    IntergalacticMail
  }

  public class Customer {
    public string Name { get; set; }
    public string Address { get; set; }
    public string Email { get; set; }
    public Medium PreferredMedium { get; set; }
  }

  public class Invoice {
    public string Header { get; set; }
    public List<string> LineItems { get; set; }
    public Customer Customer { get; set; }
  }

}

We'd like to be able to send an invoice to a customer by using his preferred medium. To keep the code to send the invoice separate from the Invoice domain class, and to abstract it so that it can send any kind of document (not just invoices) that adhere to a certain format, a mixin to send documents is created:

namespace Mixins {

  public interface MSendableDocument {
    void CreateDocument();
    
    // data slot that the mixin needs
    [EditorBrowsable(EditorBrowsableState.Never)]
    object __DataSlot { get; set; } 
  }
  
  public static class SendableDocument {
    public static void ClearDocument(this MSendableDocument self) {
      self.SetDocumentBuilder(new StringBuilder());
    }
    public static void AppendTextToDocument(this MSendableDocument self, string text) {
      self.GetDocumentBuilder().AppendLine(text);
    }
    private static string GetDocument(this MSendableDocument self) {
      return self.GetDocumentBuilder().ToString();
    }
    private static StringBuilder GetDocumentBuilder(this MSendableDocument self) {
      return (StringBuilder)self.__DataSlot;
    }
    private static void SetDocumentBuilder(this MSendableDocument self, StringBuilder value) {
      self.__DataSlot = value;
    }
    public static void SnailMailDocument(this MSendableDocument self, string name, string address) {
      // example code....
      self.CreateDocument();
      string document = self.GetDocument();
      Console.WriteLine("Snail Mail\n  sent to {0}\n  at {1}", name, address);
      Console.WriteLine("Contents:");
      Console.WriteLine(document);
    }
    public static void EmailDocument(this MSendableDocument self, string name, string email) {
      // ...
    }
  }

}

Any class that represents a document that can be sent can use this mixin. The MSendableDocument mixin interface lists the required methods that it needs the target classes to implement. The extension methods to the mixin interface found in the SendableDocument static class provide the infrastructure needed for the target classes to assemble their document, realized through the methods ClearDocument and AppendTextToDocument. The target classes will provide the method CreateDocument, where a document will be assembled by calling the extension methods.

This mixin needs to keep state, it maintains the running document while the target class is building it. Unfortunately, the abstraction starts to break down, because extension methods are not suitable to maintaining per-instance state. (Update: Injecting state into a class can be done in the .NET Framework 4.0.) This issue is tackled by requiring the target classes to also implement a property, called __DataSlot, that the mixin will use to store any information it needs alongside the target instance's data. This is certainly not ideal. To accomodate future extensions of the mixin without breaking the target classes, the property is of type Object and its name is generic (data slot); to indicate that it's a kind of private property (even though it has to be public, because all the target class is doing is implementing an interface), it's preceded with double underscores and annotated with the EditorBrowsable attribute set to Never. If you ever use these kinds of mixins, it's very important that these conventions are communicated and understood by every developer in the team. This is particularly not suitable for API development, and should only be used in application development. A mixin without a data slot (i.e. it doesn't need to keep state) can be used in APIs though.

In the code, it's clear that the mixin uses the data slot to store a StringBuilder where it keeps the document that's being built. This is completely internal to the mixin implementation, and can be changed without disrupting the target classes.

All this code accomplishes something that would be much better expressed like this:

// WARNING: invalid C# ahead
namespace Mixins {

  public mixin MSendableDocument {
    public abstract void CreateDocument();
    
    public void ClearDocument() {
      DocumentBuilder = new StringBuilder();
    }
    public void AppendTextToDocument(string text) {
      DocumentBuilder.AppendLine(text);
    }
    public void SnailMailDocument(string name, string address) {
      // ...
    }
    public void EmailDocument(string name, string email) {
      // ...
    }

    private StringBuilder DocumentBuilder { get; set; } 
    private string Document {
      get { return DocumentBuilder.ToString(); }
    }
  }

}

The SendableInvoice class uses the mixin:

namespace Domain {

  using Mixins;

  public class SendableInvoice : Invoice, MSendableDocument {
    // NOTE: extension methods to "this" require the "this" variable!

    public void SendToCustomer() {
      switch (Customer.PreferredMedium) {
        case Medium.SnailMail:
        case Medium.IntergalacticMail: // not implemented, attempt Fedex!
          this.SnailMailDocument(Customer.Name, Customer.Address);
          break;
        case Medium.Email:
          this.EmailDocument(Customer.Name, Customer.Email);
          break;
      }
    }

    void MSendableDocument.CreateDocument() {
      this.ClearDocument();
      this.AppendTextToDocument(CreateHeaderText());
      this.AppendTextToDocument(CreateLineItemsText());
    }

    string CreateHeaderText() {
      return Header;
    }
    string CreateLineItemsText() {
      var sb = new StringBuilder();
      foreach (string lineItemText in LineItems) {
        sb.AppendLine(lineItemText);
      }
      return sb.ToString();
    }

    // needed by the mixin, not to be used by the class or its clients!
    object MSendableDocument.__DataSlot { get; set; }
  }
}

Invoices can now be sent like this:

var invoice = new SendableInvoice {
  Customer = new Customer {
    Name = "Roger Wilco",
    Address = "Kerona Planet, in Orat's cave",
    PreferredMedium = Medium.IntergalacticMail,
  },
  Header = "Star Generator",
  LineItems = new List<string> {
    "The Generator ---- 1M bz",
    "Gift Card --------  1 bz",
    "Shipping --------- 1K bz",
    "Taxes ------------ 1K bz"
  }
};

invoice.SendToCustomer();

The advantage of this kind of mixin is that you can create decoupled functionality that can be reused in different contexts, in a way unattained by single inheritance. The disadvantage is that C# is not completely suitable for this task, and some workarounds are necessary, raising the need for strong conventions and effective communication.

No comments:

Post a Comment