Extension Object


Still another way to add functionality to a hierarchy without changing it is to use the EXTENSION OBJECT pattern. This pattern is more complex than the others but is also much more powerful and flexible. Each object in the hierarchy maintains a list of special extension objects. Each object also provides a method that allows the extension object to be looked up by name. The extension object provides methods that manipulate the original hierarchy object.

For example, let's assume that we have a BOM system again. We need to develop the ability for each object in this hierarchy to create an XML representation of itself. We could put toXML methods in the hierarchy, but this would violate CCP. It may be that we don't want BOM stuff and XML stuff in the same class. We could create XML by using a VISITOR, but that doesn't allow us to separate the XML-generating code for each type of BOM object. In a VISITOR, all the XML-generating code for each BOM class would be in the same VISITOR object. What if we want to separate the XML generation for each different BOM object into its own class?

EXTENSION OBJECT provides a nice way to accomplish this goal. The code that follows shows the BOM hierarchy with two kinds of extension object: one kind converts BOM objects into XML; the other kind converts BOM objects into CSV (commaseparated-value) strings. The first kind is accessed by GetExtension("XML"); the second, by GetExtension("CSV"). The structure is shown in Figure 35-7 and was taken from the completed code. The «marker» stereotype denotes a marker interface; that is, an interface with no methods.

Figure 35-7. Extension Object


The code is in Listings 35-30 through 35-41. It is important to understand that I did not simply write this code from scratch. Rather, I evolved the code from test case to test case. The first source file (Listing 35-30) shows all the test cases. They were written in the order shown. Each test case was written before there was any code that could make it pass. Once each test case was written and failing, the code that made it pass was written. The code was never more complicated than necessary to make the existing test cases pass. Thus, the code evolved in tiny increments from working base to working base. I knew that I was trying to build the EXTENSION OBJECT pattern and used that to guide the evolution.

Listing 35-30. BomXmlTest.CS

[TestFixture] public class BomXmlTest {   private PiecePart p1;   private PiecePart p2;   private Assembly a;   [SetUp]   public void SetUp()   {     p1 = new PiecePart("997624", "MyPart", 3.20);     p2 = new PiecePart("7734", "Hell", 666);     a = new Assembly("5879", "MyAssembly");   }   [Test]   public void CreatePart()   {     Assert.AreEqual("997624", p1.PartNumber);     Assert.AreEqual("MyPart", p1.Description);     Assert.AreEqual(3.20, p1.Cost, .01);   }   [Test]   public void CreateAssembly()   {     Assert.AreEqual("5879", a.PartNumber);     Assert.AreEqual("MyAssembly", a.Description);   }   [Test]   public void Assembly()   {     a.Add(p1);     a.Add(p2);     Assert.AreEqual(2, a.Parts.Count);     Assert.AreEqual(a.Parts[0], p1);     Assert.AreEqual(a.Parts[1], p2);   }   [Test]   public void AssemblyOfAssemblies()   {     Assembly subAssembly = new Assembly("1324", "SubAssembly");     subAssembly.Add(p1);     a.Add(subAssembly);     Assert.AreEqual(subAssembly, a.Parts[0]);   }   private string ChildText(     XmlElement element, string childName)   {     return Child(element, childName).InnerText;   }   private XmlElement Child(XmlElement element, string childName)   {     XmlNodeList children =       element.GetElementsByTagName(childName);     return children.Item(0) as XmlElement;   }   [Test]   public void PiecePart1XML()   {     PartExtension e = p1.GetExtension("XML");     XmlPartExtension xe = e as XmlPartExtension;     XmlElement xml = xe.XmlElement;     Assert.AreEqual("PiecePart", xml.Name);     Assert.AreEqual("997624",       ChildText(xml, "PartNumber"));     Assert.AreEqual("MyPart",       ChildText(xml, "Description"));     Assert.AreEqual(3.2,       Double.Parse(ChildText(xml, "Cost")), .01);   }   [Test]   public void PiecePart2XML()   {     PartExtension e = p2.GetExtension("XML");     XmlPartExtension xe = e as XmlPartExtension;     XmlElement xml = xe.XmlElement;     Assert.AreEqual("PiecePart", xml.Name);     Assert.AreEqual("7734",       ChildText(xml, "PartNumber"));     Assert.AreEqual("Hell",       ChildText(xml, "Description"));     Assert.AreEqual(666,       Double.Parse(ChildText(xml, "Cost")), .01); }   [Test]   public void SimpleAssemblyXML()   {     PartExtension e = a.GetExtension("XML");     XmlPartExtension xe = e as XmlPartExtension;     XmlElement xml = xe.XmlElement;     Assert.AreEqual("Assembly", xml.Name);     Assert.AreEqual("5879",       ChildText(xml, "PartNumber"));     Assert.AreEqual("MyAssembly",       ChildText(xml, "Description"));     XmlElement parts = Child(xml, "Parts");     XmlNodeList partList = parts.ChildNodes;     Assert.AreEqual(0, partList.Count);   }   [Test]   public void AssemblyWithPartsXML()   {     a.Add(p1);     a.Add(p2);     PartExtension e = a.GetExtension("XML");     XmlPartExtension xe = e as XmlPartExtension;     XmlElement xml = xe.XmlElement;     Assert.AreEqual("Assembly", xml.Name);     Assert.AreEqual("5879",       ChildText(xml, "PartNumber"));     Assert.AreEqual("MyAssembly",       ChildText(xml, "Description"));     XmlElement parts = Child(xml, "Parts");     XmlNodeList partList = parts.ChildNodes;     Assert.AreEqual(2, partList.Count);     XmlElement partElement =       partList.Item(0) as XmlElement;     Assert.AreEqual("PiecePart", partElement.Name);     Assert.AreEqual("997624",       ChildText(partElement, "PartNumber"));     partElement = partList.Item(1) as XmlElement;     Assert.AreEqual("PiecePart", partElement.Name);     Assert.AreEqual("7734",       ChildText(partElement, "PartNumber"));   }   [Test]   public void PiecePart1toCSV()   {     PartExtension e = p1.GetExtension("CSV");     CsvPartExtension ce = e as CsvPartExtension;     String csv = ce.CsvText;     Assert.AreEqual("PiecePart,997624,MyPart,3.2", csv);   }   [Test]   public void PiecePart2toCSV()   {     PartExtension e = p2.GetExtension("CSV");     CsvPartExtension ce = e as CsvPartExtension;     String csv = ce.CsvText;     Assert.AreEqual("PiecePart,7734,Hell,666", csv);   }   [Test]   public void SimpleAssemblyCSV()   {     PartExtension e = a.GetExtension("CSV");     CsvPartExtension ce = e as CsvPartExtension;     String csv = ce.CsvText;     Assert.AreEqual("Assembly,5879,MyAssembly", csv);   }   [Test]   public void AssemblyWithPartsCSV()   {     a.Add(p1);     a.Add(p2);     PartExtension e = a.GetExtension("CSV");     CsvPartExtension ce = e as CsvPartExtension;     String csv = ce.CsvText;     Assert.AreEqual("Assembly,5879,MyAssembly," +       "{PiecePart,997624,MyPart,3.2}," +       "{PiecePart,7734,Hell,666}"       , csv);   }   [Test]   public void BadExtension()   {     PartExtension pe = p1.GetExtension(       "ThisStringDoesn'tMatchAnyException");     Assert.IsTrue(pe is BadPartExtension);   } }

Listing 35-31. Part.cs

public abstract class Part {   Hashtable extensions = new Hashtable();   public abstract string PartNumber { get; }   public abstract string Description { get; }   public void AddExtension(string extensionType,     PartExtension extension)   {     extensions[extensionType] = extension;   }   public PartExtension GetExtension(string extensionType)   {     PartExtension pe =       extensions[extensionType] as PartExtension;     if (pe == null)       pe = new BadPartExtension();     return pe;   } }

Listing 35-32. PartExtension.cs

public interface PartExtension { }

Listing 35-33. PiecePart.cs

public class PiecePart : Part {   private string partNumber;   private string description;   private double cost;   public PiecePart(string partNumber,     string description,     double cost)   {     this.partNumber = partNumber;     this.description = description;     this.cost = cost;     AddExtension("CSV", new CsvPiecePartExtension(this));     AddExtension("XML", new XmlPiecePartExtension(this));   }   public override string PartNumber   {     get { return partNumber; }   }   public override string Description   {     get { return description; }   }   public double Cost   {     get { return cost; }   } }

Listing 35-34. Assembly.cs

public class Assembly : Part {   private IList parts = new ArrayList();   private string partNumber;   private string description;   public Assembly(string partNumber, string description)   {     this.partNumber = partNumber;     this.description = description;     AddExtension("CSV", new CsvAssemblyExtension(this));     AddExtension("XML", new XmlAssemblyExtension(this));   }   public void Add(Part part)   {     parts.Add(part);   }   public IList Parts   {     get { return parts; }   }   public override string PartNumber   {     get { return partNumber; }   }   public override string Description   {     get { return description; }   } }

Listing 35-35. XmlPartExtension.cs

public abstract class XmlPartExtension : PartExtension {   private static XmlDocument document = new XmlDocument();   public abstract XmlElement XmlElement { get; }   protected XmlElement NewElement(string name)   {     return document.CreateElement(name);   }   protected XmlElement NewTextElement(     string name, string text)   {     XmlElement element = document.CreateElement(name);     XmlText xmlText = document.CreateTextNode(text);     element.AppendChild(xmlText);     return element;   } }

Listing 35-36. XmlPiecePartExtension.cs

public class XmlPiecePartExtension : XmlPartExtension {   private PiecePart piecePart;   public XmlPiecePartExtension(PiecePart part)   {     piecePart = part;   }   public override XmlElement XmlElement   {     get     {       XmlElement e = NewElement("PiecePart");       e.AppendChild(NewTextElement(         "PartNumber", piecePart.PartNumber));       e.AppendChild(NewTextElement(         "Description", piecePart.Description));       e.AppendChild(NewTextElement(         "Cost", piecePart.Cost.ToString()));       return e;     }   } }

Listing 35-37. XmlAssemblyExtension.cs

public class XmlAssemblyExtension : XmlPartExtension {   private Assembly assembly;   public XmlAssemblyExtension(Assembly assembly)   {     this.assembly = assembly;   }   public override XmlElement XmlElement   {     get     {       XmlElement e = NewElement("Assembly");       e.AppendChild(NewTextElement(         "PartNumber", assembly.PartNumber));       e.AppendChild(NewTextElement(         "Description", assembly.Description));       XmlElement parts = NewElement("Parts");       foreach(Part part in assembly.Parts)       {         XmlPartExtension xpe =           part.GetExtension("XML")           as XmlPartExtension;         parts.AppendChild(xpe.XmlElement);       }       e.AppendChild(parts);       return e;     }   } }

Listing 35-38. CsvPartExtension.cs

public interface CsvPartExtension : PartExtension {   string CsvText { get; } }

Listing 35-39. CsvPiecePartExtension.cs

public class CsvPiecePartExtension : CsvPartExtension {   private PiecePart piecePart;   public CsvPiecePartExtension(PiecePart part)   {     piecePart = part;   }   public string CsvText   {     get     {       StringBuilder b =         new StringBuilder("PiecePart,");       b.Append(piecePart.PartNumber);       b.Append(",");       b.Append(piecePart.Description);       b.Append(",");       b.Append(piecePart.Cost);       return b.ToString();     }   } }

Listing 35-40. CsvAssemblyExtension.cs

public class CsvAssemblyExtension : CsvPartExtension {   private Assembly assembly;   public CsvAssemblyExtension(Assembly assy)   {     assembly = assy;   }   public string CsvText   {     get     {       StringBuilder b =         new StringBuilder("Assembly,");       b.Append(assembly.PartNumber);       b.Append(",");       b.Append(assembly.Description);       foreach(Part part in assembly.Parts)       {         CsvPartExtension cpe =           part.GetExtension("CSV")           as CsvPartExtension;         b.Append(",{");         b.Append(cpe.CsvText);         b.Append("}");       }       return b.ToString();     }   } }

Listing 35-41. BadPartExtension.cs

public class BadPartExtension : PartExtension { }

Note that the extension objects are loaded into each BOM object by that object's constructor. This means that, to some extent, the BOM objects still depend on the XML and CSV classes. If even this tenuous dependency needs to be broken, we could create a FACTORY[4] object that creates the BOM objects and loads their extensions.

[4] See Chapter 29.

The fact that the extension objects can be loaded into the object creates a great deal of flexibility. Certain extension objects can be inserted or deleted from objects depending upon the state of the system. It would be easy to get carried away with this flexibility. For the most part, you probably won't find it necessary. Indeed, the original implementation of PiecePart.GetExtention(String extensionType) looked like this.

public PartExtension GetExtension(String extensionType) {   if (extensionType.Equals("XML"))     return new XmlPiecePartExtension(this);   else if (extensionType.Equals("CSV"))     return new XmlAssemblyExtension(this);   return new BadPartExtension(); }


I wasn't particularly thrilled with this, because it was virtually identical to the code in Assembly.GetExtension. The Hashtable solution in Part avoids this duplication and is simpler. Anyone reading it will know exactly how extension objects are accessed.




Agile Principles, Patterns, and Practices in C#
Agile Principles, Patterns, and Practices in C#
ISBN: 0131857258
EAN: 2147483647
Year: 2006
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net