- Overview
- Chained Service Factory
- Unchained Service Factory
- Product Manager
- Service Façade
- Abstract Packet Pattern
- Packet Translator
Intent
Provide an abstract container used to pass parameters to any objects within a framework. This will also serve to package discrete parameters into a single and more efficient data-marshaling package.
Problem
Whether you are working with Web services or any publicly available business function, eliminating unnecessary data traffic is important. Most server-based services will take a variety of parameters to perform any one business function. Variables anywhere from strings to bytes to arrays will need to be passed to these business services, and they should be passed in the most efficient manner. Most object-oriented designs call for some form of encapsulation. This may be data objects providing “accessors” (getters) and “mutators” (setters) used to interact with the business data. Interaction with these objects occurs through “hydration” using mutators or through “extraction” using accessors. This type of multiple round trip get/set interaction is fine when an object is local or when the interaction is simple. Multiple gets and sets across the network would not be good. In general, where this scenario falls short is when the objects involved are separated by some form of boundary.
Boundaries come in many forms. There are network boundaries, process boundaries, domain boundaries (.NET), and storage boundaries (I/O), etc. The problem is that as a developer, interacting with objects using several round trips to set and get data can become a problem—a performance problem. This is especially apparent when calling business objects across network boundaries where each get or set call adds a significant performance hit to the overall transaction. Aside from being wasteful in network usage, it also forces the server object to maintain state in between accessor/mutator invocations. In some cases, holding state may be necessary but this should be used carefully.
An option in avoiding multiple round trips during object interaction is to pass all parameters into the method at once. For some designs, this is perfectly fine. In those cases, the parameter list may include only one or two data elements that you must pass to an object. Most of time, however, this is not sufficient. One or two parameters can quickly become three, four, five, and many more. Maintaining business methods with long parameter lists (although done) is not recommended. This is where the Abstract Packet comes into play. It is simply a container for those parameters. It is a generic container with the ability to hold as many parameters as are necessary to facilitate any business object, as long as that business can receive that packet's data type (Figure 4.9). This also simplifies the signature of most business methods because now a business method can be typed with a single parameter. This also applies to return values. The return value can be of the same type, as long as that type is generic enough to contain data that will be returned from any business function.
Figure 4.9. Abstract Packet implementation class diagram.
Forces
Use the Abstract Packet pattern when:
-
Web services will be used that contain more than two or three parameters.
-
Business functions need to contain a standard signature contract to isolate future changes.
-
Parameter types change frequently for business methods.
-
Working with services crossing expensive boundaries (process, network, etc.).
Structure
Figure 4.10. Abstract Packet generic class diagram.
Consequences
-
Improves the parameter-passing efficiency. When all parameters are bundled into one object, the developer will have more control on how to marshal those parameters. This includes any serialization that may take place. This also provides a controlled means of retrieving or unpackaging those parameters within the containing object.
-
Provides a container in which to build dynamic parameter sets. For those scenarios where the set of parameters can vary frequently, the Abstract Packet provides the container in which to build such a facility. In our example, the Packet class simply aggregates an already dynamic data type in the form of an ADO.NET DataSet object. Using the DataSet data member, the Packet can take any shape and contain any data. As long as the business method that receives this packet knows how to interact with it, the packet can be used throughout the framework. The packet then can contain any data and be passed to each tier of the framework without implementing specific behavior for each representation of the packet. Only the business method that must directly interact with the packet's data must know what data elements it requires. For example, at this stage, the business method would call the packet's GetData() and request specific fields from the packet. The packet, in turn, delegates the lookup to the aggregated DataSet. To the rest of the system, this is just a generic packet.
Look Ahead
Another option to this pattern is to bind a “type-strong” Data Service object that will be a child of a DataSet and, thus, can also be bound to the packet when the packet is built or translated. This new option provides a type-strong DataSet that any business method wishing to interact with the packet can use instead of using the packet's delegation methods. Using a type-strong DataSet is one way to avoid boxing/unboxing and can improve performance. Not to mention that it provides a much friendlier development environment for Visual Studio .NET users, especially those who love Intellisense. Using a type-strong DataSet will be fully discussed in the Chapter 5.
-
Eliminates the binding of business methods to technology-specific data types, such as those in ADO.NET (DataSet). This simply avoids forcing business methods from including ADO.NET types in their signatures and provides another level of abstraction.
-
Hides the implementation details of the aggregated inner type (a DataSet, in this case). Business methods, even those directly interacting with the data, do not require any of the details for manipulating types such as a DataSet. Those services directly interacting with the packet can simply use the methods provided by the packet. Methods such as GetData() require only parameters such as a field name to retrieve the underlying information. Keep in mind that a DataSet does not have to be bound to an actual database; a field name can be just a name of a column from the DataSet that could have been generated dynamically. As mentioned earlier in the Look Ahead sidebar, there is also another means of interaction (see Chapter 5).
Participants
-
Packet (Same name as implementation)— This is the Abstract Packet itself. This class acts as a form of “flyweight” in that its job is to contain data that can be shared efficiently with the rest of the system. It will act as a container of both extrinsic (passed-in) and intrinsic (static) data used by the rest of the system to route a request and perform an action.
-
Packet Translator (same)— This includes any packet translation that constructs a packet using an overloaded method called Translate(). The Translator constructs the destination object and maps the appropriate values into the new data object. This construction and translation logic is business-specific. The goal is to simplify and abstract this logic. The client does not know or care how the construction or translation takes place or which Translate method to call. The client simply invokes Translate(), and the overloaded method takes care of invoking the appropriate method based on the type passed. Refer to the Packet Translator section later in this chapter for details.
-
DataSet (same)— This is a standard ADO.NET DataSet object. This can represent any data schema, whether it is based on a persistent data model or not. This becomes the actual data container with a callable wrapper. This wrapper is the Packet class. The packet holds descriptive data elements to identify the packet, which can then be used for routing or other logic. Any other generic container such as an ArrayList, object[], etc., can be used as well.
-
ProductDataSet (CreditCardDS)— This is the business-specific data services class (data access object). This is a classic data services object that directly represents a view on a database or other persistent set. This class is strongly typed to the specific data elements of a particular business service or database. It inherits from a DataSet to gain any ADO.NET features, such as serialization and XML support, to allow it initialize or extract the data once it is hydrated.
Implementation
The Abstract Packet was implemented primarily to aggregate a DataSet. In fact, the DataSet type in .NET can be used as an Abstract Packet with and of itself. For a technology backgrounder on ADO.NET and DataSets in particular, please refer to Chapter 5. Those already familiar with DataSets will understand that a DataSet is a generic object that can hold just about any data representation in memory. A DataSet can be dynamically built and hydrated from a database or, as is the case in this example, be hydrated from an XSD schema. The beauty of our Abstract Packet example is the fact that it does not “reinvent the wheel.” The Packet class does not try to duplicate functionality that a DataSet already provides. It simply delegates to it and acts as an aggregator of an existing Dataset. The other data members of the Packet class are simply used to identify the packet for use by the architecture as this packet gets passed from service to service.
To build a packet, one must first have a DataSet object. In our example, a Web service receives and returns the DataSet type. When a DataSet is passed into our Web service, it instantiates and builds an appropriate Packet object. The building step can become complex and, therefore, should be farmed out to another service, such as a Packet Translator (covered later in this book). The primary step in building a packet is simply to set the DataSet as a data member of the packet. This is done using the Data property of the packet. The remaining properties of the packet are optional. More properties can be added to the packet as needed by the business requirements. The point is that the DataSet, now a member of the packet, still contains most of the data. When data needs to be extracted from a packet, its GetData methods or indexers can then be called, which delegates to the DataSet. The Packet class can now become the primary parameter passed to all business methods. This is similar to the functionality of an Adapter Pattern (GoF).
A DataSet could have been passed instead, but using a Packet class provides another level of abstraction. This abstraction will safeguard those methods from change and provide a high-level interface to those services that may not need to know how to manipulate a DataSet directly. The DataSet can be as simple as representing a single table or as complex as representing an entire database with constraints and all. By using a DataSet, all data can be treated as though directly contained within an actual database. This is true even if the DataSet is strictly represented in memory. Within the Packet class, methods can be designed to manipulate the DataSet in any way it sees fit. One caveat to this particular implementation, however, is the fact that the Packet class does not contain any type-specific methods. For example, each overloaded SetData() method takes an object as one of its parameters. Although this facilitates setting any data type of any field in the DataSet, this also introduces what .NET refers to as boxing. It is recommended that for performance-intensive implementations, type-specific methods should be created to avoid this side effect.
Technology Backgrounder—Boxing/Unboxing
Those already familiar with details behind value types, reference types, and the process of boxing/unboxing can skip this section. For those wanting more information, read on.
In the .NET CLR, you have two general types: value types and reference types. Value and reference are similar in that they both are objects. In fact, everything in the CLR is an object. Even value types are objects in that they have the System.ValueType as a parent class, which has System.Object as its parent. Each primitive type is represented by an equivalent class. For example, the primitive types of int and long in C# both alias the System.Int32 and System.Int64 classes, respectively, both of which have System.ValueType as parent. Other value types include structs and enumerations (enums). If it inherits from System.ValueType, it is treated as a value type in the CLR.
Value types are handled a bit differently than reference types in that they are passed by value. Passing by value means that a copy of the value is made prior to calling the function. For most value types, the cost of making this copy is small and usually outweighs the performance issues that arise when dealing with reference types. Value types represent a value that is allocated on the stack. They are never null and must contain data. Any custom value type can be created simply by deriving from System.ValueType. When creating your own value types, however, keep in mind that a value type is sealed, meaning that no one else can derive from your new type.
Reference types are based on the heap and can contain null values. They include types such as classes, interfaces, and pointers. These types are passed by reference, meaning that when passed, the address of the object (or pointer) is passed into the function. No copy is made. Unlike value types, when you make a change, the original value is changed, as well, because you are now dealing with a pointer. Reference types can be used when output parameters are required or when a type consumes a significant chunk of memory (remember that structs are value types, and they can grow quite large). However, they also must be managed by the CLR. This means that they must be kept track of and garbage collected. This also will add a performance penalty. Value types should be used wherever possible to improve performance and to conserve memory. If your object consumes a lot of memory, a reference type should be used, bearing in mind that any destruction or finalization of your type is going to be nondeterministic.
Once you understand the technical differences of how value types and reference types are treated, you will understand how unboxing and boxing work. Values types can become reference types, and the opposite is true as well. This can be forced or this can be automatic. The CLR will automatically convert a value type into a reference type whenever needed. This is called boxing. Boxing refers to converting a stack-allocated value into a heap-based reference type. An example of this would the following:
int nFoo = 1;// nFoo is a value type object oBar = nFoo; // oBar is a reference type of type // System.Object
Here, a box is created and the value of nFoo is copied into it. To translate, heap space is allocated, and the value of nFoo is copied into that memory space and now must be temporarily managed. When a value is boxed, you receive an object upon which methods can be called, just like any other System.Object type (e.g., ToString(), Equals(), etc.). The reverse of this process is called unboxing, which is the just the opposite. A heap-based object is converted into its equivalent stack-based value type, such as:
int nFoo = (int)oBar;// oBar is a reference type
Unboxing and boxing, although convenient, can also become a small performance bottleneck and should be used with care. For methods that will be called extremely often, as will our Packet data object, using a System.Object type as a parameter where value types will be expected should anticipate a low performance. This is due to boxing. Methods such as these can be changed to support a System.ValueType but you must also create methods to except other types, including strings (which, by the way, are not value types).
Most of the methods and indexers defined in this class delegate to the Data property. The Data property simply returns the m_dsRawData member variable of this class (which is the DataSet we are wrapping). The Packet class uses this property to delegate most of the calls to the wrapped DataSet to return data, set data, and so on. This uses the DataSet for the heavy lifting. Wrapping the DataSet in this aspect gives the Abstract Packet its “Adapter” qualities, allowing it to be passed to all business services that accept a Packet data type. Listing 4.8 contains code for a typical Abstract Packet implementation.
Listing 4.8 Typical Abstract Packet implementation.
public class Packet : IDisposable { private DataTableCollection m_oData = null; private DataTable m_dtMeta = null; private DataSet m_dsRawData = null; private string m_sType; private string m_sService; private string m_sAction; private PacketTranslator m_oTranslator = null; public Packet() { RawData = new DataSet(); Translator = new PacketTranslator(); } public Packet(PacketTranslator oTranslator) : this() { m_oTranslator = oTranslator; } public static bool operator == (Packet p1, Packet p2) { if ((object)p1 == null && (object)p2 == null) return true; if ((object)p1 != null && (object)p2 == null) return false; else return p1.Equals((Packet)p2); } public static bool operator != (Packet p1, Packet p2) { if ((object)p1 == null && (object)p2 == null) return false; if ((object)p1 != null && (object)p2 == null) return true; else return !p1.Equals((Packet)p2); } public DataSet RawData { get { return m_dsRawData; } set { m_dsRawData = value; } } public DataTableCollection Data { get { return m_oData; } set { m_oData = value; } } public DataTable Meta { get { return m_dtMeta; } set { m_dtMeta = value; } } public string Type { get { return m_sType; } set { m_sType = value; } } public string Action { get { return m_sAction; } set { m_sAction = value; } } public string Service { get { return m_sService; } set { m_sService = value; } } public PMPacketTranslator Translator { get { return m_oTranslator; } set { m_oTranslator = value; } } public string TransId { get { return m_sTransId; } set { m_sTransId = value; } } // assume first table in collection public object GetData(string sColumn) { DataRow dr = null; object oReturn = null; if (Data[0].Rows.Count > 0) { dr = Data[0].Rows[0]; oReturn = dr[sColumn]; if (oReturn == System.DBNull.Value) oReturn = null; } return oReturn; } public object GetData(string sTable, string sCol) { return GetData(sTable, sCol, 0); } public object GetData(string sTable, string sCol, int nRow) { DataRow dr = null; object oReturn = null; if (Data[sTable].Rows.Count > 0) { dr = Data[sTable].Rows[nRow]; oReturn = dr[sCol]; if (oReturn == System.DBNull.Value) oReturn = null; } return oReturn; } public object[] GetRowData(string sTable, int nRow) { object[] oRowArray = null; if (Data[sTable].Rows.Count > 0) { oRowArray = Data[sTable].Rows[nRow].ItemArray; } return oRowArray; } public void SetRow(string sTable, int nRow, object[] oaRow) { Data[sTable].Rows[nRow].ItemArray = oaRow; } public void SetData(string sCol, object oVal) { DataRow dr = null; if (Data[0].Rows.Count > 0) { dr = Data[0].Rows[0]; dr[sCol] = oVal; } } public void SetData(string sTable, string sCol, object oVal) { SetData(sTable, sCol, 0, oVal); } public void SetData(string sTable, string sCol, int nRow, object oValue) { DataRow dr = null; if (Data[sTable].Rows.Count > 0) { dr = Data[sTable].Rows[nRow]; dr[sColumn] = oValue; } } public string this[string sColumn] { get { return Convert.ToString(GetData(sColumn)); } set { SetData(sColumn, value); } } public string this[string sTable, string sColumn] { get { return Convert.ToString(GetData(sTable, sColumn)); } set { SetData(sTable, sColumn, value); } } public string this[string sTable, string sColumn, int nRow] { get { return Convert.ToString(GetData(sTable, sColumn, nRow)); } set { SetData(sTable, sColumn, nRow, value); } } public static string SafeCastString(object oValue) { string sReturn; if (oValue != null) sReturn = (string)oValue; else sReturn = ""; return sReturn; } public static decimal SafeCastDecimal(object oValue) { decimal dReturn; if (oValue != null) dReturn = (decimal)oValue; else dReturn = (decimal)0.0; return dReturn; } ... public void Dispose() { RawData.Dispose(); } } }
Related Patterns
-
Value Object (Alur, Crupi, Malks)
-
Adapter (GoF)
-
Composite (GoF)