« Developer vs. Runtime Responsibilities | Main | See us at the Spring Experience »
November 14, 2005
Tradition, Object Identity and DSO - Part 3
posted by pcalThis is the last in a three-part series of articles which examines the notion of object identity in a distributed cache. Terracotta DSO preserves object identity; traditional API-based cache services don't. This is a key differentiator for Terracotta and a big win for developers.
- Part 1 explains what object identity is and why preserving it in a distributed cache is important.
- Part 2 examines how domain modeling suffers when object identity is not preserved.
- Part 3 illustrates how DSO preserves identity in a sample application.
Overview
In part one of this series, we explored the notion of object identity and why it is an important consideration in developing distributed applications. In part two, we saw how most traditional replication frameworks do not preserve object identity and how this in turn can have perverse effects on application design. By contrast, Terracotta's DSO framework can be used to distribute an application in a way that is completely natural.
Now, we are going to explore in some detail how that natural model is presented to the developer. We will write some simple code that demonstrates how a developer using DSO can develop a domain model without having to worry about how it will behave in a distributed operational setting. The operational concerns come later, in a second step which involves making some simple declarations in XML.
Review
In the previous articles, we took as an example an abstraction of an OnlineStore. The store is essentially a big bag of Product abstractions. The store maintains an 'inventory', which was a mapping of Product keys (SKUs) to Product objects. Moreover, the OnlineStore contains a set of Departments which provide an alternate view of the inventory; a Department is simply a named, arbitrary subset of Products in the inventory. We have to be able to update the price of any Product and have it be reflected across the inventory and the Departments.
Some Natural Code
If we take the preceding paragraph as a specification, how would we write Java code to fulfill it? For the moment, let's not worry about what we might have to do to make it work in a real-world system that has to support millions of transactions every day. Just think about the simplest way to model the domain abstractions in Java.
Let's take a look at the Product abstraction. It's a pretty simple bean-like construct with fields for the price, description, and a unique SKU ID:
/** * Encapsulates information about a product in our online store. * * @author Patrick Calahan*/ public class Product { // ====================================================================== // Fields private double price; private final String description; private final String sku; // ====================================================================== // Constructor public Product(String n, double p, String s) { description = n; price = p; sku = s; } // ====================================================================== // Public methods public void setPrice(double p) { synchronized { price = p; } } public String getSKU() { return sku; } public String getDescription() { return description; } public double getPrice() { return price; } // ====================================================================== // java.lang.Object implementation public int hashCode() { return sku.hashCode(); } }
So far, so simple, right? The Department is also very straightforward - it just has a name, a unique department code, and an array of Product objects:
/** * Encapsulates information about a Department in our online store. A department * is essentially a named subset of the product inventory. * * @author Patrick Calahan*/ public class Department { // ====================================================================== // Fields private final String code; private final String name; private final Product[] products; // ====================================================================== // Constructor public Department(String c, String n, Product[] p) { code = c; name = n; products = p; } // ====================================================================== // Public methods public String getName() { return name; } public Product[] getProducts() { return products; } // ====================================================================== // java.lang.Object implementation public int hashCode() { return code.hashCode(); } }
Setting up Shop
Having defined our two bean types, Product and Department, it's now time to tie them together in an OnlineStore object that will present a service to clients. Again, the key components of the store are a mapping of all of the Products (the inventory) as well as a list of Departments which provides an alternate view of the Products. As before, we write this using very natural and simple Java constructs:
/** * Represents a simple online store. The store contains various * products which are grouped into named lists (departments) * and by overall inventory (a mapping of SKU (string) to Product * (object)). * * @author Patrick Calahan*/ public class OnlineStore { // ====================================================================== // Fields // A list of Department objects private List departments = new ArrayList(); // Maps a product's SKU (String) to a Product instance private Map inventory = new HashMap(); // ====================================================================== // Constructor /** * Creates some sample products and departments, then associates the * products with the departments and the inventory map. */ public OnlineStore() { Product warandpeace = new Product("War and Peace", 7.99, "WRPC"); Product tripod = new Product("Camera Tripod", 78.99, "TRPD"); Product usbmouse = new Product("USB Mouse", 19.99, "USBM"); Product flashram = new Product("1GB FlashRAM card", 47.99, "1GFR"); // create some departments which contain arbitrary subsets of the // inventory Department housewares = new Department("B", "Books", new Product[] { warandpeace }); Department photography = new Department("P", "Photography", new Product[] { tripod, flashram }); Department computers = new Department("C", "Computers", new Product[] { usbmouse, flashram, }); departments.add(housewares); departments.add(photography); departments.add(computers); inventory.put(warandpeace.getSKU(), warandpeace); inventory.put(tripod.getSKU(), tripod); inventory.put(usbmouse.getSKU(), usbmouse); inventory.put(flashram.getSKU(), flashram); } // ====================================================================== // Public methods public List getDepartments() { return departments; } public Product getProduct(String sku) { return (Product)inventory.get(sku); } }
A First Customer
We now have a simple OnlineStore service that can give us different ways of looking at Products and Departments. We can also interact with it a little bit by updating prices on Products. Let's take a look at some simple code that uses this service.
The OnlineStoreClient below is going to access the OnlineStore and do something interesting with it. It may allow customers to view all of the Products in the inventory, or perhaps only Products in a given Department. It may also have some functions that we use internally to respond to changes in our supply chain, so that we can adjust the price as needed.
The code below does not show all of the detail about what the client is doing, as it's not really important to the example. What we do see is two utility methods at the bottom of the class. We will be focusing on these to demonstrate the power of preserving object identity. These methods interact with Products in the store by simply getting a reference to one of them and changing it.
The important point to notice about these methods is that they are implemented in a way that assumes the OnlineStore instance is local to the client JVM. They assume that they can access objects in the store by reference. This is why they are so simple - just one or two lines of code.
/** * This class provides a simple command-line interface to our StoreService. * * @author Patrick Calahan*/ public class OnlineStoreClient { // ====================================================================== // Fields // The StoreService instance that the client will interact with. Note that // this field is marked as a root in L1Config.xml, which means that // all instances of Client in all JVMs will share the same instance of // StoreService. private OnlineStore store = new OnlineStore(); // ... // some code that does interesting stuff with the OnlineStore. // ... // ====================================================================== // Private utility methods used in client processing /** * Changes the price of all of the Products in a given Department by * a certain amount. */ private void changePriceByDepartment(Department dept, double increase) { Product[] p = dept.getProducts(); for(int i=0; i < p.length; i++) { p.setPrice(p.getPrice() + increase); } } /** * Updates the price of the Product having the given SKU #. */ private updateProductPrice(String sku, double newPrice) { Product p = store.getProduct(sku); if (p != null) product.setPrice(newPrice); } }
So What?
At this point, many readers may be wondering what is so interesting here. This is all very straightforward Java code. What's the big deal?
The simplicity of the code is exactly what is about to become very interesting. Note that we really haven't thought much about the operational topology of our system. Most likely, we will want the client to be in one JVM (e.g. in a swing app or in a web tier) and the OnlineStore will be in another (in some kind of data tier).
However, our OnlineStoreClient above clearly is not set up for that: it instantiates the OnlineStore object in a member field and just starts working with it as a VM-local object. We haven't written any code to do any kind of RPC or to talk to a distributed hashmap API. We clearly have to write a lot more code if we want to get our OnlineStore running in a distributed fashion. Right?
Actually, "Wrong." You don't need any more code!
Fortunately, if we're using DSO, we don't have to change our single-VM code to make it into a distributed application. All we have to do is provide a few declarations which DSO can use to distribute our application for us. Let's take a look at what that looks like:
<terracotta-config>
<dso enabled="true">
<dso-client>
<roots>
<root>
<field-name>
com.terracottatech.examples.inventory.OnlineStoreClient.store
</field-name>
</root>
</roots>
<locks>
<lock>
<method-expression>
* com.terracottatech.examples.inventory.OnlineStoreClient*.*(..)
</method-expression>
<lock-definition>
<lock-level>write</lock-level>
</lock-definition>
</lock>
</locks>
<included-classes>
<include>
<class-expression>com.terracottatech.examples.inventory..*
</class-expression>
</include>
</included-classes>
</dso-client>
</dso>
</terracotta-config>
What does all of this mean? It basically says three things:
First, the 'roots' section declares the fields which DSO should treat as shared objects. This means that any instance of OnlineStoreClient on any JVM will have an instance of OnlineStore that is logically identical.
Second, the 'locks' section specifies how the shared OnlineStore object should behave in the presence of concurrent accesses. Here, we make a simple declaration which says that all accesses through our client should honor the natural Java synchronization controls that are expressed in the control. Note that in the source for Product, the setPrice() method contains a synchronized block - our declaration here in the XML configuration simply says that this synchronization block should be honored in a distributed context. (For more information about locking options, see the Terracotta Virtualization Server Product Guide).
Finally, the 'included-classes' section is needed to indicate to DSO which types of objects are going to be shared. DSO uses this information to decide which classes should be bytecode-instrumented to be made distributable. Here, we simply instrument all of our classes - this is often the simplest thing to do.
Note that this means the OnlineStoreClient class will also get instrumented. This is technically not necessary, since the client itself does not get distributed. However, including extra classes for instrumentation causes no harm - the instrumentation will simply be ignored.
Wrapping it up
The beauty of this is that our distributed OnlineStore will function correctly regardless of how many OnlineStoreClients are connected to it. The clients can be in *any* VM - DSO will honor our synchronization semantics no matter where the client actually lives.
Granted, this is all a very simple example, but it illustrates the power of DSO. For the price of writing a small bit of XML, we have a distributed application without having to add any extra relational fields to our domain. We simply declare that our objects should exist everywhere; DSO ensures that they do.
This means that we can simply keep references to those objects, and that we can express relationships between the object via regular Java references. This means simpler code and less code, which in turn means faster time to market and lower maintenance costs for your product.
This is the power that DSO's preservation of object identity gives us: natural code that's as simple as it can be.