Proxy


Imagine that we are writing a shopping cart system for a Web site. Such a system might have objects for the customer, the order (the cart), and the products in the order. Figure 34-1 shows a possible structure. This structure is simplistic but will serve for our purposes.

Figure 34-1. Simple shopping cart object model


If we consider the problem of adding a new item to an order, we might come up with the code in Listing 34-1. The AddItem method of class Order simply creates a new Item holding the appropriate Product and quantity and then adds that Item to its internal ArrayList of Items.

Listing 34-1. Adding an item to the Object model

public class Order {   private ArrayList items = new ArrayList();   public void AddItem(Product p, int qty);   {     Item item = new Item(p, qty);     items.Add(item);   } }

Now imagine that these objects represent data kept in a relational database. Figure 34-2 shows the tables and keys that might represent the objects. To find the orders for a given customer, you find all orders that have the customer's cusid. To find all the items in a given order, you find the items that have the order's orderId. To find the products referenced by the items, you use the product's sku.

Figure 34-2. Shopping cart relational data model


If we want to add an item row for a particular order, we'd use something like Listing 34-2. This code makes ADO.NET calls to directly manipulate the relational data model.

Listing 34-2. Adding an item to the relational model

public class AddItemTransaction : Transaction {   public void AddItem(int orderId, string sku, int qty)   {     string sql = "insert into items values(" +       orderId + "," + sku + "," + qty + ")";     SqlCommand command = new SqlCommand(sql, connection);     command.ExecuteNonQuery();   } }

These two code snippets are very different, but they perform the same logical function. They both connect an item to an order. The first ignores the existence of a database, and the second glories in it.

Clearly, the shopping cart program is all about orders, items, and products. Unfortunately, if we use the code in Listing 34-2, we make it about SQL statements, database connections, and piecing together query strings. This is a significant violation of SRP and possibly CCP. Listing 34-2 mixes two concepts that change for different reasons. It mixes the concept of the items and orders with the concept of relational schemas and SQL. If either concept must change for any reason, the other concept will be affected. Listing 34-2 also violates DIP, since the policy of the program depends on the details of the storage mechanism.

The PROXY pattern is a way to cure these ills. To explore this, let's set up a test program that demonstrates the behavior of creating an order and calculating the total price. The salient part of this program is shown in Listing 34-3.

The simple code that passes this test is shown in Listings 34-4 through 34-6. The code makes use of the simple object model in Figure 34-1. It does not assume that there is a database anywhere.

Listing 34-3.

Test program creates order and verifies calculation of price.   [Test]   public void TestOrderPrice()   {     Order o = new Order("Bob");     Product toothpaste = new Product("Toothpaste", 129);     o.AddItem(toothpaste, 1);     Assert.AreEqual(129, o.Total);     Product mouthwash = new Product("Mouthwash", 342);     o.AddItem(mouthwash, 2);     Assert.AreEqual(813, o.Total);   }

Listing 34-4. Order.cs

public class Order {   private ArrayList items = new ArrayList();   public Order(string cusid)   {   }   public void AddItem(Product p, int qty)   {     Item item = new Item(p,qty);     items.Add(item);   }   public int Total   {     get     {       int total = 0;       foreach(Item item in items)       {         Product p = item.Product;         int qty = item.Quantity;         total += p.Price * qty;       }       return total;     }   } }

Listing 34-5. Product.cs

public class Product {   private int price;   public Product(string name, int price)   {     this.price = price;   }   public int Price   {     get { return price; }   } }

Listing 34-6. Item.cs

public class Item {   private Product product;   private int quantity;   public Item(Product p, int qty)   {     product = p;     quantity = qty;   }   public Product Product   {     get { return product; }   }   public int Quantity   {     get { return quantity; }   } }

Figures 34-3 and 34-4 show how the PROXY pattern works. Each object to be proxied is split into three parts. The first is an interface that declares all the methods that clients will want to invoke. The second is an implementation that implements those methods without knowledge of the database. The third is the proxy that knows about the database.

Figure 34-3. PROXY static model


Figure 34-4. PROXY dynamic model


Consider the Product class. We have proxied it by replacing it with an interface. This interface has all the same methods that Product has. The ProductImplementation class implements the interface almost exactly as before. The ProductDBProxy implements all the methods of Product to fetch the product from the database, create an instance of ProductImplementation, and then delegate the message to it.

The sequence diagram in Figure 34-4 shows how this works. The client sends the Price message to what it thinks is a Product but what is in fact a ProductDBProxy. The ProductDBProxy fetches the ProductImplementation from the database and then delegates the Price property to it.

Neither the client nor the ProductImplementation knows that this has happened. The database has been inserted into the application without either party knowing about it. That's the beauty of the PROXY pattern. In theory, it can be inserted in between two collaborating objects without their having to know about it. Thus, it can be used to cross a barrier, such as a database or a network, without either participant knowing about it.

In reality, using proxies is nontrivial. To get an idea what some of the problems are, let's try to add the PROXY pattern to the simple shopping cart application.

Implementing PROXY

The simplest Proxy to create is for the Product class. For our purposes, the product table represents a simple dictionary. It will be loaded in one place with all the products. There is no other manipulation of this table, and that makes the proxy relatively trivial.

To get started, we need a simple database utility that stores and retrieves product data. The proxy will use this interface to manipulate the database. Listing 34-7 shows the test program for what I have in mind. Listings 34-8 and 34-9 make that test pass.

Listing 34-7. DbTest.cs

[TestFixture] public class DBTest {   [SetUp]   public void SetUp()   {     DB.Init();   }   [TearDown]   public void TearDown()   {     DB.Close();   }   [Test]   public void StoreProduct()   {     ProductData storedProduct = new ProductData();     storedProduct.name = "MyProduct";     storedProduct.price = 1234;     storedProduct.sku = "999";     DB.Store(storedProduct);     ProductData retrievedProduct =       DB.GetProductData("999");     DB.DeleteProductData("999");     Assert.AreEqual(storedProduct, retrievedProduct);   } }

Listing 34-8. ProductData.cs

public class ProductData {   private string name;   private int price;   private string sku;   public ProductData(string name,     int price, string sku)   {     this.name = name;     this.price = price;     this.sku = sku;   }   public ProductData() {}   public override bool Equals(object o)   {     ProductData pd = (ProductData)o;     return name.Equals(pd.name) &&       sku.Equals(pd.sku) &&       price==pd.price;   }   public override int GetHashCode()   {     return name.GetHashCode() ^       sku.GetHashCode() ^       price.GetHashCode();   } }

Listing 34-9.

public class Db {   private static SqlConnection connection;   public static void Init()   {     string connectionString =       "Initial Catalog=QuickyMart;" +       "Data Source=marvin;" +       "user id=sa;password=abc;";     connection = new SqlConnection(connectionString);     connection.Open();   }   public static void Store(ProductData pd)   {     SqlCommand command = BuildInsertionCommand(pd);     command.ExecuteNonQuery();   }   private static SqlCommand     BuildInsertionCommand(ProductData pd)   {     string sql =       "INSERT INTO Products VALUES (@sku, @name, @price)";     SqlCommand command = new SqlCommand(sql, connection);     command.Parameters.Add("@sku", pd.sku);     command.Parameters.Add("@name", pd.name);     command.Parameters.Add("@price", pd.price);     return command;   }   public static ProductData GetProductData(string sku)   {     SqlCommand command = BuildProductQueryCommand(sku);     IDataReader reader = ExecuteQueryStatement(command);     ProductData pd = ExtractProductDataFromReader(reader);     reader.Close();     return pd;   }   private static   SqlCommand BuildProductQueryCommand(string sku)   {     string sql = "SELECT * FROM Products WHERE sku = @sku";     SqlCommand command = new SqlCommand(sql, connection);     command.Parameters.Add("@sku", sku);     return command;   }   private static ProductData     ExtractProductDataFromReader(IDataReader reader)   {     ProductData pd = new ProductData();     pd.Sku = reader["sku"].ToString();     pd.Name = reader["name"].ToString();     pd.Price = Convert.ToInt32(reader["price"]);     return pd;   }   public static void DeleteProductData(string sku)   {     BuildProductDeleteStatement(sku).ExecuteNonQuery();   }   private static SqlCommand     BuildProductDeleteStatement(string sku)   {     string sql = "DELETE from Products WHERE sku = @sku";     SqlCommand command = new SqlCommand(sql, connection);     command.Parameters.Add("@sku", sku);     return command;   }   private static IDataReader     ExecuteQueryStatement(SqlCommand command)   {     IDataReader reader = command.ExecuteReader();     reader.Read();     return reader;   }   public static void Close()   {     connection.Close();   } }

The next step in implementing the proxy is to write a test that shows how it works. This test adds a product to the database, creates a ProductProxy with the sku of the stored product, and attempts to use the accessors of Product to acquire the data from the proxy. See Listing 34-10.

Listing 34-10. ProxyTest.cs

[TestFixture] public class ProxyTest {   [SetUp]   public void SetUp()   {     Db.Init();     ProductData pd = new ProductData();     pd.sku = "ProxyTest1";     pd.name = "ProxyTestName1";     pd.price = 456;     Db.Store(pd);   }   [TearDown]   public void TearDown()   {     Db.DeleteProductData("ProxyTest1");     Db.Close();   }   [Test]   public void ProductProxy()   {     Product p = new ProductProxy("ProxyTest1");     Assert.AreEqual(456, p.Price);     Assert.AreEqual("ProxyTestName1", p.Name);     Assert.AreEqual("ProxyTest1", p.Sku);   } }

In order to make this work, we have to separate the interface of Product from its implementation. So I changed Product to an interface and created ProductImp to implement it (see Listings 34-11 and 34-12). This forced me to make changes to TestShoppingCart (not shown) to use ProductImp instead of Product.

Listing 34-11. Product.cs

public interface Product {   int Price {get;}   string Name {get;}   string Sku {get;} }

Listing 34-12. ProductImpl.cs

public class ProductImpl : Product {   private int price;   private string name;   private string sku;   public ProductImpl(string sku, string name, int price)   {     this.price = price;     this.name = name;     this.sku = sku;   }   public int Price   {     get { return price; }   }   public string Name   {     get { return name; }   }   public string Sku   {     get { return sku; }   } }

Listing 34-13.

public class ProductProxy : Product {   private string sku;   public ProductProxy(string sku)   {     this.sku = sku;   }   public int Price   {     get     {       ProductData pd = Db.GetProductData(sku);       return pd.price;     }   }   public string Name   {     get     {       ProductData pd = Db.GetProductData(sku);       return pd.name;     }   }   public string Sku   {     get { return sku; }   } }

The implementation of this proxy is trivial. In fact, it doesn't quite match the canonical form of the pattern shown in Figures 34-3 and 34-4. This was an unexpected surprise. My intent was to implement the PROXY pattern. But when the implementation finally materialized, the canonical pattern made no sense.

The canonical pattern would have had ProductProxy create a ProductImp in every method. It would then have delegated that method or property to the ProductImp, as follows:

public int Price {   get   {     ProductData pd = Db.GetProductData(sku);     ProductImpl p =       new ProductImpl(pd.Name, pd.Sku, pd.Price);     return pd.Price;   } }


The creation of the ProductImp is a complete waste of programmer and computer resources. The ProductProxy already has the data that the ProductImp accessors would return. So there is no need to create, and then delegate to, the ProductImp. This is yet another example of how the code may lead you away from the patterns and models you expected.

Note that the Sku property of ProductProxy in Listing 34-13 takes this theme one step further. It doesn't even bother to hit the database for the sku. Why should it? It already has the sku.

You might be thinking that the implementation of ProductProxy is very inefficient. It hits the database for each accessor. Wouldn't it be better if it cached the ProductData item in order to avoid hitting the database?

This change is trivial, but the only thing driving us to do it is our fear. At this point, we have no data to suggest that this program has a performance problem. Besides, we know that the database engine too is doing some caching. So it's not clear what building our own cache would buy us. We should wait until we see indications of a performance problem before we invent trouble for ourselves.

Our next step is to create the proxy for Order. Each Order instance contains many Item instances. In the relational schema (Figure 34-2), this relationship is captured within the Item table. Each row of the Item table contains the key of the Order that contains it. In the object model, however, the relationship is implemented by an ArrayList within Order (see Listing 34-4). Somehow, the proxy is going to have to translate between the two forms.

We begin by posing a test case that the proxy must pass. This test adds a few dummy products to the database, obtains proxies to those products, and uses them to invoke AddItem on an OrderProxy. Finally, the test asks the OrderProxy for the total price (see Listing 34-14). The intent of this test case is to show that an OrderProxy behaves just like an Order but obtains its data from the database instead of from in-memory objects.

Listing 34-14. ProxyTest.cs

[Test] public void OrderProxyTotal() {   Db.Store(new ProductData("Wheaties", 349, "wheaties"));   Db.Store(new ProductData("Crest", 258, "crest"));   ProductProxy wheaties = new ProductProxy("wheaties");   ProductProxy crest = new ProductProxy("crest");   OrderData od = Db.NewOrder("testOrderProxy");   OrderProxy order = new OrderProxy(od.orderId);   order.AddItem(crest, 1);   order.AddItem(wheaties, 2);   Assert.AreEqual(956, order.Total); }

In order to make this test case work, we have to implement a few new classes and methods. The first we'll tackle is the NewOrder method of Db. This method appears to return an instance of something called an OrderData. OrderData is just like ProductData: a simple data structure that represents a row of the Order database table. The method is shown in Listing 34-15.

Listing 34-15. OrderData.cs

public class OrderData {   public string customerId;   public int orderId;   public OrderData() {}   public OrderData(int orderId, string customerId)   {     this.orderId = orderId;     this.customerId = customerId;   } }

Don't be offended by the use of public data members. This is not an object in the true sense. It is simply a container for data. It has no interesting behavior that needs to be encapsulated. Making the data variables private, and providing getters and setters would be a waste of time. I could have used a struct instead of a class, but I want the OrderData to be passed by reference rather than by value.

Now we need to write the NewOrder function of Db. Note that when we call it in Listing 34-14, we provide the ID of the owning customer but do not provide the orderId. Each Order needs an orderId to act as its key. What's more, in the relational schema, each Item refers to this orderId as a way to show its connection to the Order. Clearly, the orderId must be unique. How does it get created? Let's write a test to show our intent. See Listing 34-16.

Listing 34-16. DbTest.cs

[Test] public void OrderKeyGeneration() {   OrderData o1 = Db.NewOrder("Bob");   OrderData o2 = Db.NewOrder("Bill");   int firstOrderId = o1.orderId;   int secondOrderId = o2.orderId;   Assert.AreEqual(firstOrderId + 1, secondOrderId); }

This test shows that we expect the orderId to somehow automatically increment every time a new Order is created. This is easily implemented by allowing SqlServer to generate the next orderId; we can get the value by calling the database method scope_identity(). See Listing 34-17.

Listing 34-17.

public static OrderData NewOrder(string customerId) {   string sql = "INSERT INTO Orders(cusId) VALUES(@cusId); " +     "SELECT scope_identity()";   SqlCommand command = new SqlCommand(sql, connection);   command.Parameters.Add("@cusId", customerId);   int newOrderId = Convert.ToInt32(command.ExecuteScalar());   return new OrderData(newOrderId, customerId); }

Now we can start to write OrderProxy. As with Product, we need to split Order into an interface and an implementation. So Order becomes the interface, and OrderImp becomes the implementation. See Listings 34-18 and 34-19.

Listing 34-18. Order.cs

public interface Order {   string CustomerId { get; }   void AddItem(Product p, int quantity);   int Total { get; } }

Listing 34-19. OrderImpl.cs

public class OrderImp : Order {   private ArrayList items = new ArrayList();   private string customerId;   public OrderImp(string cusid)   {     customerId = cusid;   }   public string CustomerId   {     get { return customerId; }   }   public void AddItem(Product p, int qty)   {     Item item = new Item(p, qty);     items.Add(item);   }   public int Total   {     get     {       int total = 0;       foreach(Item item in items)       {         Product p = item.Product;         int qty = item.Quantity;         total += p.Price * qty;       }       return total;     }   } }

How do I implement AddItem in the proxy? Clearly, the proxy cannot delegate to OrderImp.AddItem! Rather, the proxy is going to have to insert an Item row in the database. On the other hand, I really want to delegate OrderProxy.Total to OrderImp.Total, because I want the business rulesthe policy for creating totalsto be encapsulated in OrderImp. The whole point of building proxies is to separate database implementation from business rules.

In order to delegate the Total property, the proxy is going to have to build the complete Order object along with all its contained Items. Thus, in OrderProxy.Total, we are going to have to read in all the items from the database, call AddItem on an empty OrderImp for each item we find, and then call Total on that OrderImp. Thus, the OrderProxy implementation ought to look something like Listing 34-20.

This implies the existence of an ItemData class and a few Db functions for manipulating ItemData rows. These are shown in Listings 34-21 through 34-23.

Listing 34-20.

public class OrderProxy : Order {   private int orderId;   public OrderProxy(int orderId)   {     this.orderId = orderId;   }   public int Total   {     get     {       OrderImp imp = new OrderImp(CustomerId);       ItemData[] itemDataArray = Db.GetItemsForOrder(orderId);       foreach(ItemData item in itemDataArray)         imp.AddItem(new ProductProxy(item.sku), item.qty);       return imp.Total;     }   }   public string CustomerId   {     get     {       OrderData od = Db.GetOrderData(orderId);       return od.customerId;     }   }   public void AddItem(Product p, int quantity)   {     ItemData id =       new ItemData(orderId, quantity, p.Sku);     Db.Store(id);   }   public int OrderId   {     get { return orderId; }   } }

Listing 34-21. ItemData.cs

public class ItemData {   public int orderId;   public int qty;   public string sku = "junk";   public ItemData() {}   public ItemData(int orderId, int qty, string sku)   {     this.orderId = orderId;     this.qty = qty;     this.sku = sku;   }   public override bool Equals(Object o)   {     if(o is ItemData)     {       ItemData id = o as ItemData;       return orderId == id.orderId &&         qty == id.qty &&         sku.Equals(id.sku);     }     return false;   } }

Listing 34-22.

[Test] public void StoreItem() {   ItemData storedItem = new ItemData(1, 3, "sku");   Db.Store(storedItem);   ItemData[] retrievedItems = Db.GetItemsForOrder(1);   Assert.AreEqual(1, retrievedItems.Length);   Assert.AreEqual(storedItem, retrievedItems[0]); } [Test] public void NoItems() {   ItemData[] id = Db.GetItemsForOrder(42);   Assert.AreEqual(0, id.Length); }

Listing 34-23.

public static void Store(ItemData id) {   SqlCommand command = BuildItemInsersionStatement(id);   command.ExecuteNonQuery(); } private static SqlCommand   BuildItemInsersionStatement(ItemData id) {   string sql = "INSERT INTO Items(orderId,quantity,sku) " +     "VALUES (@orderID, @quantity, @sku)";   SqlCommand command = new SqlCommand(sql, connection);   command.Parameters.Add("@orderId", id.orderId);   command.Parameters.Add("@quantity", id.qty);   command.Parameters.Add("@sku", id.sku);   return command; } public static ItemData[] GetItemsForOrder(int orderId) {   SqlCommand command =     BuildItemsForOrderQueryStatement(orderId);   IDataReader reader = command.ExecuteReader();   ItemData[] id = ExtractItemDataFromResultSet(reader);   reader.Close();   return id; } private static SqlCommand   BuildItemsForOrderQueryStatement(int orderId) {   string sql = "SELECT * FROM Items " +     "WHERE orderid = @orderId";   SqlCommand command = new SqlCommand(sql, connection);   command.Parameters.Add("@orderId", orderId);   return command; } private static ItemData[]   ExtractItemDataFromResultSet(IDataReader reader) {   ArrayList items = new ArrayList();   while (reader.Read())   {     int orderId = Convert.ToInt32(reader["orderId"]);     int quantity = Convert.ToInt32(reader["quantity"]);     string sku = reader["sku"].ToString();     ItemData id = new ItemData(orderId, quantity, sku);     items.Add(id);   }   return (ItemData[]) items.ToArray(typeof (ItemData)); } public static OrderData GetOrderData(int orderId) {   string sql = "SELECT cusid FROM orders " +     "WHERE orderid = @orderId";   SqlCommand command = new SqlCommand(sql, connection);   command.Parameters.Add("@orderId", orderId);   IDataReader reader = command.ExecuteReader();   OrderData od = null;   if (reader.Read())     od = new OrderData(orderId, reader["cusid"].ToString());   reader.Close();   return od; } public static void Clear() {   ExecuteSql("DELETE FROM Items");   ExecuteSql("DELETE FROM Orders");   ExecuteSql("DELETE FROM Products"); } private static void ExecuteSql(string sql) {   SqlCommand command = new SqlCommand(sql, connection);   command.ExecuteNonQuery(); }

Summary

This example should have dispelled any false illusions about the elegance and simplicity of using proxies. Proxies are not trivial to use. The simple delegation model implied by the canonical pattern seldom materializes so neatly. Rather, we find ourselves short circuiting the delegation for trivial getters and setters. For methods that manage 1:N relationships, we find ourselves delaying the delegation and moving it into other methods, just as the delegation for AddItem was moved into Total. Finally, we face the specter of caching.

We didn't do any caching in this example. The tests all run in less than a second, so there was no need to worry overmuch about performance. But in a real application, the issue of performance, and the need for intelligent caching, is likely to arise. I do not suggest that you automatically implement a caching strategy because you fear that performance will otherwise be too slow. Indeed, I have found that adding caching too early is a good way to decrease performance. Rather, if you fear that performance may be a problem, I recommend that you conduct some experiments to prove that it will be a problem. Once proven, and only once proven, you should start considering how to speed things up.

For all the troublesome nature of proxies, they have one powerful benefit: the separation of concerns. In our example, the business rules and the database have been completely separated. OrderImp has no dependence on the database. If we want to change the database schema or the database engine, we can do so without affecting Order, OrderImp, or any of the other business domain classes.

If separation of business rules from database implementation is critically important, PROXY can be a good pattern to use. For that matter, PROXY can be used to separate business rules from any kind of implementation issue. It can be used to keep the business rules from being polluted by such things as COM, CORBA, EJB, and so on. It is a way to keep the business rule assets of your project separate from the implementation mechanisms that are currently in vogue.




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