MSDN Home > MSDN Library > |
Mike McClure Leo Romano Thanks to contributor Peter Joyce August 2000 Summary: Details an approach to designing reusable enterprise components. This article focuses on the Engine-Collection-Class (ECC) design pattern, a flexible model for creating reusable enterprise components for distributed/tiered applications that work with both traditional "rich" clients and scripting clients. (36 printed pages) Download Vs6samples.exe. ContentsIntroduction IntroductionThis article details an approach to designing reusable enterprise components. It focuses on a proven design pattern that has been implemented and refined over multiple projects and customers. The Engine-Collection-Class (ECC) design pattern is a flexible model for creating reusable enterprise components for distributed/tiered applications that work with both traditional "rich" clients and the ever-popular scripting clients. With its simplified interfaces and intuitive coding style, the ECC design pattern reduces development time and allows for consistent programming across multiple developers. Companies and developers can capitalize upon its extensibility when making future enhancements, as well as its maintainability when supporting an application. Since the core of the design pattern is based on COM and other popular design patterns, the implementation of the ECC can be done in either an object-oriented or component-oriented language. This article focuses on a Microsoft Visual Basic implementation of the ECC design pattern. PrerequisitesThis article serves mainly as an Engine-Collection-Class primer. It does not detail some of the more advanced features of the ECC design pattern. The follow prerequisites are recommended to make effective use the ECC design pattern:
OverviewHave you ever completed a project that you believed was reusable and extensible only to find out later that your system must be rewritten to take advantage of the latest technology changes? Have you ever started a project and halfway through realized that using XML or the latest SOAP protocol for interoperability would greatly improve the future system only to find that this would break every component you've written and run costs into the stratosphere? If you answered "yes," you're not alone. In today's competitive market, companies need to make fast-paced, long-term decisions with minimal information. The wrong decision can affect every part of your company and set it back months or even years. Both business executives and technical developers wonder if there is a way to alleviate development risks. This article introduces a design pattern for building reusable enterprise components. The ECC is a simple and intuitive design pattern that was developed with one thing in mind: change. Businesses change, rules change, technology changes, and people change. However, just because things change doesn't mean that you have to bear the full impact of that change. From a business perspective, the ECC design pattern helps lessen the impact of:
From a technical perspective, ECC is a three-tiered design pattern for developing n-tier business components by allowing the creation, storage, and attributes of a component to change with minimal impact to the solution. The ECC reduces the technical impact of changes by using:
Front-end planning with an extensible architecture is critical for next-generation applications and businesses. By using the ECC design pattern in today's projects, you can develop more robust and flexible systems that drive tomorrow's businesses. Developing the SolutionThe following core elements were taken into consideration in developing a reusable design pattern solution:
n-tier distributed design (tier insulation) The application is broken into layers to restrict the cascading effect of changes in one part of the application from affecting another part. Although it requires more front-end design time, tiered insulation reduces operational costs compared to the alternative. Minimizing calls over the network With Web-based solutions (location transparency), applications must be designed to do as much work as possible at either end of the chain and send the least amount of information across the wire. This concept can be extended to include the reduction of calls over process boundaries, thread boundaries, and context boundaries. Flexible architecture Inflexible applications come from inflexible implementations. In striving to envision goals for the design pattern, one of the most important questions was, "What happens if this changes?". The objective was to provide maximizing extensibility, reusability, maintainability, and independence (data source, data location, language). In the end, the goal was to develop a design pattern that would help reduce investment costs, manage scope creep, minimize the impact of changes, and increase reusability and maintainability without increasing complexity of use. Maintainability and extensibility versus performance Maintainability and extensibility usually come at the price of performance but this is offset when enhancements to the system cost very little. When a choice must be made between maintainability and extensibility versus performance, maintainability and extensibility are always the best choice. The implementer of the design pattern ultimately has the ability to forgo one option for the other. Simple and consistent interface Building simple and consistent heuristics for calling objects in the system helps reduce user-training costs and reduces the learning curve and knowledge transfer to other developers. Encapsulating and centralizing external dependencies Components can usually inherit services from external sources such as the operating system. Most Windows components (or applications) depend upon the operating system or some other service (such as MTS or COM+) to provide security access. But if this is not possible, or the costs of using inherited services outweighs the gain, centralizing external dependencies in the components is valuable because it allows a single point of control. Consequently, if these dependencies change, the programmer will not have to determine which modules or code segments will need to change. Modeling real world entities and their relationships (RDBMS, Use Cases, OOP) Many existing methodologies describe real world entities and the processes with which they interact. Some examples include:
The ECC design pattern uses the proven heuristics from these methodologies to build a reusable design. The ECC design pattern also follows Windows DNA models for building extensible, robust, distributed systems and combines them with industry-standard design techniques such as OO design, design patterns, and database design. Knowledge of these techniques is very common among people working with applications, so the transition from one phase to another becomes straightforward and reliable. These core elements are addressed in this discussion about the ECC design pattern. However, it is important to remember that this is not the ultimate design pattern for every distributed application. Rather, it is one choice among many, and implementation issues must be considered before adopting it for use. Engine-Collection-Class Design OverviewThe Engine-Collection-Class design pattern requires a slightly different mindset than architects are generally accustomed to. They tend to approach a project with a procedural designwhat processes must I complete to fulfill the requirements or to manipulate the data? With the ECC design pattern, those processes are secondary and, if the model is correctly designed, will fall logically into the objects themselves. Figure 1 reveals the symbiotic relationship between the Engine, Collection, and Class. From a logical design perspective, the Class is actually the key reason that Engine and Collection exist. Class relates to a real world entity with its attributes (such as first name and last name). Engine and Collection provide the means for managing and extending its behavior. For an entity to be modeled, all three must be created. Figure 1. The ECC design pattern relationship diagram The ECC provides a three-tiered design for business objects by separating the creation, storage, and attributes of an entity. The ECC design pattern uses object-oriented techniques such as encapsulation and some very popular design patterns such as the Object Factory, and the Opaque Object Adapter. Note For more information about these patterns read Visual Basic Design Patterns, which is listed in the References section. In general, the Engine implements a design pattern known as the Object Factory, which defines an interface for creating other objects. In this case, the Engine creates both the Collection and the Class objects. Designing the Engine in this fashion provides the following benefits:
The Collection implements a pattern known as the Opaque Object Adapter, which allows one object to encapsulate and use another object's interface and functionality privately. This technique allows Visual Basic to simulate implementation inheritance even though it currently supports only interface inheritance. As shown in Figure 2, the Collection contains a reference to the ConcreteInternalStorage. This is known as composition. The ConcreteInternalStorage object is used to store Class objects, and depending on the amount of functionality provided, it can be used to provide extended functionality to the Collection. Arrays, Visual Basic's Collection object, the Dictionary object, and ADO Recordsets are all examples of ConcreteInternalStorage objects. Implementing the Collection like this provides the following benefits:
Class implements the notation of encapsulation, which means the internal implementation details are hidden from the client using it. This provides the benefit of allowing the implementation details to change without affecting the client. Figure 2 reveals that the objects in the ECC design pattern are concrete classes. This means that the Visual Basic Implements keyword is not used and that there are no abstract classes (classes with just interfaces and no implementation). The main reason for this is that the ECC design pattern was geared toward supporting both traditional "rich" clients and the ever-popular scripting clients using the same components and without extra coding. This design decision helps reduce the time to market for projects and reduces costs because different components don't have to be created or maintained for internal (or external) applications. Figure 2. The Engine-Collection-Class model The Engine-Collection-Class model is implemented with the Object Factory design pattern and the Opaque Object Adapter pattern. The diagram shows the base methods that each of the objects can implement. Design BenefitsThe ECC design pattern is similar to a three-tiered design for business objects where the separation of an entity's creation, storage, and attributes provides enormous flexibility and benefits such as call interception, enforcement of business rules, data validation, derived attributes, simplified interfaces, and enforcement of object creation. An effective ECC design is driven by the database (data source) design, which in turn is driven by the business requirements. In general, a data source schema mimics the relationship and storage of real world entities and a lot of thought goes into developing a data source or ERD. With the ECC design pattern, the Classes map to database tables, views, stored procedures, or other structured storage for a starting point to inherit the base business rules. Using this strategy leverages the existing work and knowledge and makes the design of the system easier to explain to others. This improves communication and feedback and reduces knowledge transfer issues. Object-Oriented techniques such as Use Cases work well for getting the scenarios to tie the pieces together, and tools such as Microsoft Visual Modeler and Microsoft Visio can be used to document and generate the objects. As the data source is enhanced or modified, ECC can be extended via new properties or methods. This can be achieved by properly versioning the published interfaces in COM. Note You cannot change, modify, or remove the existing public interfaces without breaking COM compatibility. ECC does not solve problems associated with modifications to the system; rather, it helps to facilitate change more easily. Wrapping the data source schema properties with the Classes provides a consistent layer of abstraction for accessing and manipulating the data. But Classes are not limited to the data source schema properties. Derived attributes can be defined to allow for the additional functionality. These attributes could physically exist in another database (data source such as LDAP or the Microsoft Active Directory Service), or they could be custom defined (IsNew, IsDirty), or they could be aggregate functions (Sum, Count). In the end, this layer of abstraction means that the details are hidden and free to change and provides a point of call interception. Call interception is the ability to access a request and handle whatever mediation is required to adjust the request for either the caller or the callee. For example, in Visual Basic when you call the New operator, Visual Basic will create the object the first time it is referenced. The developer doesn't need to do anything for this feature; however, nor can you tap into the call to change it. The type of call interception that developers can capture is at a higher level (property calls, method calls, interface calls, and events). This gives the developer the ability to respond to requests. This is a critical feature because ECC uses this call interception to enforce business rules. For example, a business rule might be that the password field must be at least four characters and no greater then 10 characters long. The Password property becomes a hook to check and enforce this business rule, which in the end maintains data integrity of the underlying data store without making a call across the wire. Another benefit is that the rule is contained within a single placeone that offers better manageability if it were to change. The last, but certainly not the least, benefit of the ECC design pattern is the ability to provide a simplified interface. Interfaces are like a binding contract in that they define what a component can do. But unlike a contract where the details are in one large document, the ECC design pattern divides the functionality into separate objects with fewer interfaces and specific roles. Therefore, each object can support functionality that is different from its own but consistent throughout the application for the role of that object. For example, Engine and Collection will support the same base functionality, although they perform functions differently from one another. The Engine is used to create Collection and Class objects, and the Collection is used to store and manipulate Class objects. Therefore, instead of creating one super-object that supports both interfaces, separating the objects allows for a more simplified interface that is easier to understand. Furthermore, this allows each of the objects to encapsulate specific roles and functionality, as well as allowing each to evolve separately without affecting the other. In this way, developers achieve a flexible application design without sacrificing code simplicity or consistency. Furthermore, a developer who is familiar with this model can begin at various points in the development process without a huge loss of productivity due to a steep learning curve. What follows is a detailed explanation of each of these objects and their meaning in the context of the ECC model. Class DesignThe Class design provides the blueprint for mapping a real world entity to a business component and is the main reason the Engine and Collection objects exist. The responsibilities of the Class are:
If a Class exists in your model, you must have an Engine and a Collection object. In the case where a Class does not exist, no corresponding Collection will exist and, although the Engine may exist, it will be used as a service or utility object. Because business rules are project-specific, enforcement of them is implementation-specific. The main point is that your design architecture should permit the flexibility to define the business rule(s) in one place but enforce them in multiple places. The Class is responsible for enforcing the following:
Unlike the Engine and Collection, the methods and properties of the Class are based on the data schema. Each Class can mirror the entities in the application. The mapping of Class properties to data properties is implementation-specific. For example, you could have a single Class that references three physical tables/views or three separate Classes. Each design needs to analyze the purpose of the object and determine which model is best suited for the implementation. Some things to consider when determining the implementation design are method boundaries, transaction boundaries, security, and data location (SQL Server, COMTI, and so forth). Figure 3 shows the ERD for a sample implementation. In this example, there is a Class for each of the entities: UserInfo, Account, Bill. Furthermore, if the entities are similar in structure, a single Class can be used to expose multiple entities. In the sample implementation, the IOBPLookup Class implements this functionality for the ActivityType, BillType, Status, and UserType. These tables mainly store read-only information that is used to populate controls or down-drop lists. One benefit of grouping the entities into a set of Classes is to share functionality. For example, if the IOBPLookup Class implemented a caching mechanism, the other components could use its functionality. This reduces the amount of code to be developed, tested, and maintained. Figure 3. Online Bill database diagram The sample implementation includes some additional business-specific fields:
The entities in this model are accessed via stored procedures, one for each function: Read, Insert, Update, and Delete. The sample implementation uses a single stored procedure for functions, but with ECC you can access multiple stored procedures to make data changes, which would be implemented at the Collection. They are based on a macro template that is used inside ERwin to auto-generate the entities (see the macro template "database\onlinebills.er1" in the code sample associated with this article). You can use normal SQL statements, but for true flexibility stored procedures are encouraged. If you do decide to use SQL statements, a general Select, Insert, Update, and Delete statement should be built. Note, however, that there are some limitations to using SQL statements. For example:
Figure 4 depicts a sample Class based on the UserInfo entity and the additional properties that should be included for ECC. Visual Modeler was used to generate the initial diagram derived from the database table. Note that the object is prefixed c to denote a Class. Figure 4. Basic Class diagram for the UserInfo entity Each attribute in the database has two properties: Let() or Set(), and Get(). Each property can be marked as Public, Protected (Friend), or Private. Figure 4 shows that UserNumber property is public readable (Get) and protected from writes (Let). The UserNumber property is the primary key for the Class and consequently should not be updated by an external entity because it is auto-generated from the database (an Identity column, for example). By using this technique, the Engine can initialize the Class properties but still provide a mechanism where the value cannot be tampered with or changed. The Class is then self-contained, which is helpful because it can then be persisted and recreated. The base recommendations for the Class interface follow:
Other interfaces that could be added to the Class include:
In the sample implementation each public interface property is defined using the Variant data type for flexibility. Though the properties can be set to other intrinsic data types, the Variant provides the ability for scripting clients to pass items by reference. In Visual Basic Clients, Variant data types can be type cast to a specific data type, which will remove the performance issues. The only problem that still would exist is that the ambiguous type description of a parameter as a Variant can be anything. Documenting the interfaces and publishing this information for other developers to use overcomes this. Another benefit is the ability to change the data type without redefining the interface. This allows the interface to adapt better to changing business requirements. The values for each property are stored in private variables. These can be type cast to the appropriate data types to enforce type checking of the values. Doing so allows the appropriate errors to be raised to inform the user of the invalid data types used. Storing the values in the private variables in their true storage state is also encouraged. This can cause problems when the database can store NULL and the data type cannot, for example Integer. To solve this problem, the sample implementation uses Variant for most of the private variables to handle both a valid data value and the NULL condition. This process is repeated for each entity that needs to be created for the application. Class ImplementationThe Get property is responsible for returning the data to the caller, possibly with special formatting conditions applied. A typical Get statement follows: Public Property Get UserID() As Variant On Error GoTo ErrorHandler If Me.ClassStorage Then 'Used to obtain the raw data (unformatted) UserID = mvarUserID Else 'No special formatting currently required UserID = mvarUserID End If <> End Property The preceding If..Else statement, or some variation of it, should appear in every Get property. The basic rules are:
As you may have noticed, the If..Else statement evaluates to the same condition, because the Get property code structure is laid out in advance; should the case arise where different functionality is required, it can be easily implemented. This property structure is intuitive for code-generation programs like Visio and Visual Modeler. Providing templates for these tools significantly increases productivity and reduces project timelines. The Let property is responsible for checking the data type, enforcing business rules, and setting a local (private) variable to the value passed in. Each property has an associated local variable with the same name as the property, prefixed by the letter m (for member variable) and a type (Hungarian notation). The local copy of the ID property would be: mvarUserID. A typical Let property follows: Public Property Let UserID(ByVal vData As Variant) Dim errNum as long Dim errSrc as string On Error GoTo ErrorHandler If Me.ClassStorage Then 'Used to change the raw data (unformatted) mvarUserID = vData ElseIf Me.DeleteFlag Then Err.Raise ERR_USERINFODELETEFLAGSET, "CUserInfo.UserID LET", _ "Can't change the value because the record is marked " & _ "for deletion." GoTo CleanExit 'Note, this provides a safety net Else 'Disable error handling to capture type mismatches On error Resume Next mvarUserID = vData if err.number <> 0 then 'Use the base error plus applications flags errNum = Err.Number Or OBPExBusinessRule Or OBPUserError errSrc = Err.Source Err.Clear 'reset the error state On Error GoTo ErrorHandler Err.Raise errNum, errSrc, "Please enter a valid value" Else On Error Goto ErrorHandler end if 'Check business conditions here Me.Dirty = True End If <> End Property The first If..Else line above, or some variation of it, should appear in every Let property to make sure that the data received is of a valid data type for this property, and to make sure that the caller actually passed some value in. The basic rules are:
The first step is to determine if the business rule checks should be bypassed. Note that the ClassStorage property of the Class is turned off after it is fully populated. Thus, subsequent calls to the Class will check and enforce the business rules. This reduces the overhead of loading the Collection and Class and allows access to the raw data. Once the business rules are enforced, the next step is to check if the Class has been marked for deletion. If so, there is no point in updating this property's value. This example raises an error on this condition. The last step is to enforce the business rule. You could add a check to enforce that the UserID must be greater then n characters in length. (In reality, the minimum length value should be read from the system parameter table.) If the data passes the tests, it then assigns the local copy the value. In this example, another property variable called Dirty is also set. Dirty indicates whether the data has changed. Notice that the member variable mvarDirty is not used to set the value directly; instead, the Let method for the Dirty property sets it. This ensures that the business rules are enforced even when the property is set from within the class itself. Note There is some overhead involved with setting variables through the Let methods, so it is acceptable to manipulate the private variables directly in some cases. But be careful because this makes debugging and maintenance of the component harder. In the example above, an error called Goto CleanExit is raised. This should never be executed but should be used to show program flow. Our UserInfo Class also contains a method called Account. This demonstrates the ECC design pattern's implementation of both On-Demand loading (the ability to load objects when they are needed) and Hierarchies (the ability to connect different ECC components together). Since the UserInfo and Account entity have a relationship (one-to-many), this implementation allows the developer to retrieve Account objects based on an existing UserInfo object. It therefore permits simple and intuitive code such as: Set objCAccount = objUserInfo.Account.Item(1) In the following example, the Account method will return a Collection (ColAccount), which contains Account Classes (CAccount). Late binding is used and gives better flexibility during the development cycle. Late binding can be avoided by generating TLB files for the components. The Account objects, respectively CAccount and ColAccount, reside in a different DLLs from the UserInfo objects and are marked as PublicNotCreatable, so you must call the Account Engine to create and retrieve these objects: Public Property Get Account() As Object On Error GoTo ErrorHandler Dim engAccount As Object Dim ErrorNum As Long If mcolAccount Is Nothing Then If Not SafeCreateObject(engAccount, OBP_ACCOUNT_ENG, _ ErrorNum) Then Err.Raise ERR_ACCOUNTSAFECREATEFAILED, _ "IOBPUserInfoEng.Account Property", "Unable to " & _ "create " & OBP_ACCOUNT_ENG & ". Return Code: " & _ ErrorNum GoTo CleanExit End If 'Only get the non-deleted records (default condition). Set mcolAccount = engAccount.Search(SecurityToken := _ SecurityToken, UserNumber := Me.UserNumber) End If Set Account = mcolAccount <> End Property Look at this a little closer: First, check the internal variable to see if there is a current reference to an Account Collection. If there is, return the Collection; otherwise, call the Search method on the Account Engine to return the Account(s) associated with the current UserNumber. This means that for each CUserInfo Class in the ColUserInfo Collection there will be a specific Account or Accounts for that individual UserNumberin other words, a hierarchy. Furthermore, the Account Collection did not exist in the CUserInfo prior to the call to the Account method, therefore on-demand loading is achieved. Since the internal call to the Search method is only done once, a parameter such as Refresh could be added to the interface of the object. This would allow for dynamically reloading the contents without destroying and recreating CUserInfo. This demonstrates the reusability built into ECC and the value of loose coupling objects in the hierarchy. It also allows implementation details to change with minimal impact. The consumer would never know that this is occurring under the hood. A sample consumer's line of code follows: strAccountNumber = ColUserInfo.Item(1).Account.Item(1).Number It's important to understand exactly what happens in this line. First, it assumes you have created a UserInfo Collection called ColUserInfo containing a user with at least one account. The first step, ColUserInfo.Item(1), calls the Item method and returns the CUserInfo Class corresponding to that user. The next step, .Account, creates an Account Engine that creates an Account Collection, populates it with a CAccount Class (or Classes), and then returns the Collection. The next step, .Item(1), calls the Item method of the Account Collection that returns the first CAccount Class. The final step, .Number, calls the Number Property Get method and returns a string with the account number, which in turn is set to the strAccountNumber variable. That's a lot of work to get one property, and to access every property of that particular object would mean creating a Collection and Class for each property accessed. In this case, it would be better to write the code as: Dim ColAccounts as colAccount Set ColAccounts = UserInfo.Item(1).Account strAccountCode = ColAcounts.Item(1).Number strPhone = ColAccounts.Item(1).CustPhone strName = ColAccounts.Item(1).CustName With this method, overhead of several calls to the Account method on the UserInfo is avoided. A For..Each construct could be used also. When accessing only one account, you can write: Dim objAccount as CAccount Set objAccount = UserInfo.Item(1).Account.Item(1) strAccountCode = objAccount.Number strPhone = objAccount.Phone strName = objAccount.CustName This is perfectly valid and reduces the overhead of multiple calls to the Items() method for each account property. It should also give you the best performance. Collection DesignThe Collection is a container for Class objects and is a wrapper for an internal storage scheme. The Collection/Class model should be familiar to most object-oriented developers and Visual Basic developers in particular. In the ECC model, the key Collection responsibilities are:
The internal storage for the Collection can be implemented in many different ways: an array, a linked list, a Recordset, XML, or another common data storage model. The important concept here is that the actual storage is hidden from the high-level developer, who will always access the data in the same manner regardless of its storage. As the Class, the interface to the Collection is separated from the implementation, thus reducing the impact of implementation changes. A sample Collection that stores the CUserInfo Class and the additional properties/methods that are recommended by the ECC design pattern follows. Visual Modeler was used to generate the initial diagram. Note The object is prefixed with col to represent a Collection. Having a separate Collection for each Class allows for better flexibility because it allows the interface to be customized appropriately for the Class. An example of this is Add(), which requires specific UserInfo parameters to be passed in. Figure 5. Basic collection diagram for the UserInfo Entity The base recommendations for the Collection interface, which provides Create, Read, Update, and Delete (CRUD) functionality, follow:
Some additional implementation-specific properties/methods follow:
Most of the methods are self-contained in the module, but Delete and Update methods actually make external calls to the data access layer. Some of their key functions include:
Other functionalities that can be added to the Collection include:
This process is repeated for each Collection that needs to be created for the application. Collection ImplementationIn most implementations, the Collection is a wrapper for a Visual Basic collection, which resides in the private portion of the Collection. In most cases, the Collection will contain the following methods: Item(), Count(), Load(), Add(), Delete(), and Update(). A discussion of each method follows. Item():Public Property Get Item(ByVal Index As Variant) As CUserInfo On Error GoTo ErrorHandler Dim mintCodeID As Integer Set Item = Nothing If mCol Is Nothing Then GoTo CleanExit End If If mCol.Count = 0 Then GoTo CleanExit End If If Trim(Index & "") = "" Or Index <= 0 Then GoTo CleanExit Else 'Cast the index to a Integer otherwise sets won't work mintCodeID = CInt(Index) Set Item = mCol.Item(mintCodeID) End If <> End Property Item() returns a CUserInfo that is stored inside the Collection. It first applies the default condition checks: empty, range, and type, then it sets a reference to the Class. This function can vary depending on the internal storage model that is used. For most of the sample implementation, the Visual Basic collection is used but the sample code does include a sample of a different storage model. Note This is an advanced ECC concept that is not covered in this document but is included in the implementation. Count():Public Property Get Count() As Variant On Error GoTo ErrorHandler If mCol Is Nothing Then Count = 0 Else Count = mCol.Count End If <> End Property Count() returns either zero or the number of the internally stored Classes. Note If a Variant Array were used for the internal storage mechanism, a function such as Ubound could be used to determine the number of items. Load():Friend Function Load(ByVal SecurityToken As Variant, ByVal _ FilledStorage As Variant) As Variant On Error GoTo ErrorHandler Load = False mvarSecurityToken = SecurityToken Set mCol = FilledStorage Load = True <> End Function The Load() method assumes the Engine has already filled internal storage with the necessary data. It then just assigns the data to a local variable. Allowing the Engine to fill the internal storage gives better flexibility since it knows the original request. It also stores a copy of the SecurityToken for later requests that might need it for processing. Add():Public Function Add(ByVal UserID As Variant, ByVal UserTypeID As _ Variant, ByVal Password As Variant, ByVal Name As Variant) _ As Variant On Error GoTo ErrorHandler Dim NewTemp As CUserInfo 'Set default return value Add = -1 'Check parameter values If UserID = "" Or UserTypeID = "" Or Password = "" _ Or Name = "" Then Err.Raise ERR_USERINFOFAILEDADDCHECK, _ "ColUserInfo.Add PROC", "You must specify values" GoTo CleanExit End If If mCol Is Nothing Then Set mCol = New Collection End If Set NewTemp = New CUserInfo With NewTemp 'Set the Required Fields .ClassStorage = True .UserNumber = 0 .UserID = UserID .UserTypeID = UserTypeID .Name = Name <> .Password = Password .SecurityToken = SecurityToken .ClassStorage = False End With 'Add the item to the End of the Collection If mCol.Count <> 0 Then mCol.Add NewTemp, , , mCol.Count Else mCol.Add NewTemp End If Add = mCol.Count <> End Function The Add() method first checks the parameters that are passed for conformance to the business rules. It then creates a user Class (CUser), assigns the default values to the properties, and adds the CUser to the internal storage. It also creates the internal collection member if that has not been created. The function returns the item number of the Class in the Collection so that the caller may access the item immediately. Delete():Note Remember that this method goes across the wire to an external source. Public Function Delete(Optional ByVal Index As Variant) As Variant On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim oCUserInfo As CUserInfo Dim vParam() As Variant Dim LowerLimit As Long Dim UpperLimit As Long Dim inx As Long Dim SPName As String Delete = False If Me.Count = 0 Then 'Nothing to Update Update = True GoTo CleanExit End If 'If Index is supplied then Delete only the supplied record If Not IsMissing(Index) Then If Index < 1 Or Index > Me.Count Then Err.Raise ERR_USERINFOINDEXOUTOFRANGE, _ "ColUserInfo.Delete PROC", "Index out of range" GoTo CleanExit Else LowerLimit = Index UpperLimit = Index End If Else LowerLimit = 1 UpperLimit = Me.Count End If If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "ColUserInfo.Delete PROC", "Unable to create " & _ OBP_DA_CONNECTION & ". Return Code was: " & ErrorNum GoTo CleanExit End If For inx = UpperLimit To LowerLimit Step -1 Set oCUserInfo = Me.Item(inx) If Not oCUserInfo.IsNew Then 'Delete from DB If Not New ReDim vParam(PARMUBOUND, 1) With oCUserInfo .ClassStorage = True 'Fill Parameter Array vParam(PARMNAME, 0) = PARMNAMESP_USERINFOUSERNUMBER vParam(PARMTYPE, 0) = PARMTYPESP_USERINFOUSERNUMBER vParam(PARMLENGTH, 0) = 4 vParam(PARMDIR, 0) = adInput vParam(PARMVALUE, 0) = .UserNumber <> .ClassStorage = False End With If Not oDALEng.Execute(SecurityToken, SP_D_USERINFO, _ vParam) Then Err.Raise ERR_USERINFODALDELETEFAILED, _ "ColUserInfo.Delete PROC", _ "Delete Failed. SPName was: " & SP_D_USERINFO GoTo CleanExit End If End If Set oCUserInfo = Nothing 'Remove from Collection mCol.Remove (inx) Next Delete = True <> End Function The Delete() method deletes one or many items from the data storage, which is usually a database. If the item is not new (IsNew = False), then it will be deleted from the database. This is because records that have their IsNew property set to true don't exist in the database. The method accomplishes the delete by packaging up the primary key and the record timestamp; this information is passed to the data access object, which will delete the record. The item is then removed from the Collection to make sure that the client does not access data that doesn't exist in the database. This is useful for "rich" clients where state is maintained. Note The Delete method may cause the ItemIndex to change for the Collection, particularly if it is stored in consumer code. It is advisable to refresh the references after Delete() to make sure they point to the correct item in the Collection. Update():Note Remember that this method also goes across the wire. Public Function Update(Optional ByVal Index As Variant) As Variant On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim rs As ADOR.Recordset Dim oCUserInfo As CUserInfo Dim vParam() As Variant Dim LowerLimit As Long Dim UpperLimit As Long Dim inx As Long Dim SPName As String Update = False If Me.Count = 0 Then 'Nothing to Update Update = True GoTo CleanExit End If 'If Index is supplied then update only the supplied record If Not IsMissing(Index) Then If Index < 1 Or Index > Me.Count Then Err.Raise ERR_USERINFOINDEXOUTOFRANGE, _ "ColUserInfo.Update PROC", "Index out of range" GoTo CleanExit Else LowerLimit = Index UpperLimit = Index End If Else LowerLimit = 1 UpperLimit = Me.Count End If If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "ColUserInfo.Update PROC", "Unable to create " & _ OBP_DA_CONNECTION & ". Return Code was: " & ErrorNum GoTo CleanExit End If For inx = LowerLimit To UpperLimit Set oCUserInfo = Me.Item(inx) If oCUserInfo.Dirty Then ReDim vParam(PARMUBOUND, 16) With oCUserInfo .ClassStorage = True 'Fill Parameter Array vParam(PARMNAME, 0) = PARMNAMESP_USERINFOUSERNUMBER vParam(PARMTYPE, 0) = PARMTYPESP_USERINFOUSERNUMBER vParam(PARMLENGTH, 0) = 0 vParam(PARMDIR, 0) = adInput vParam(PARMVALUE, 0) = .UserNumber <> .ClassStorage = False End With 'Check to see if updating existing record to database If oCUserInfo.IsNew = False Then 'Update the record SPName = SP_U_USERINFO Else 'Inserting the record SPName = SP_I_USERINFO End If If Not oDALEng.Execute(SecurityToken, SPName, vParam, _ rs) Then Err.Raise ERR_USERINFODALUPDATEFAILED, _ "ColUserInfo.Update PROC", _ "Update to Database Failed. SPName was: " & _ SPName GoTo CleanExit ElseIf (Not rs Is Nothing) Then 'Set to True if nothing returned in DB If rs.RecordCount > 1 Then Err.Raise ERR_RETURNTOOMANYRECORDS, _ "ColUserInfo.Update PROC", _ "Update to Database Failed. Returned more " & _ "than one record. SPName was: " & SPName GoTo CleanExit Else 'Update collection with returned data With oCUserInfo .ClassStorage = .UserNumber = rs.Fields(FN_USERINFOUSERNUMBER) .UserID = rs.Fields(FN_USERINFOUSERID) .UserTypeID = rs.Fields(FN_USERINFOUSERTYPEID) <> .IsNew = False .SecurityToken = SecurityToken .ClassStorage = False .Dirty = False End With End If End If End If 'Dirty Set oCUserInfo = Nothing Next inx Update = True <> End Function Update() is similar to Delete() in that it can update one or many items. Another key feature is the ability to choose between insert and update. The IsNew property is again used, but in this case, true means that the method should perform an insert and false means it should perform an update. The information for the Class is packed and sent to the Data Access object where it is either inserted or updated. Once updated or inserted remotely, the internal Class is updated to reflect the changes that might have changed on the database. For example, the Recordtimestamp will change or be created, and therefore the Class needs to reflect the new value. For the other functions, see the code sample associated with this article. Engine DesignThe Engine is a layer above the Collection and the Class and exists as a single point of entry to them. The Engine becomes an insulating layer for both security and control of object creation. Its key responsibilities follow:
The Engine is the public interface for a particular object and is the only public creatable class in the ECC model. This has a lot of benefits. By being the only publicly creatable object, the Engine can stand alone and implement generic business functionality. In this capacity, the Engine is called a service and has no corresponding Collection and Class. An example of a service object would be one that just does business processing and returns a value. The implementation of the IOBPSecurity shows how service object works. In a more powerful use of the Engine, it is combined with a Collection and Class. In this case, the Engine is the main interface to the other objects and can control how they are created. As Figure 2 depicts, the Engine is somewhat coupled to the Collection and Class, since it must know how to retrieve the Class information, pack it, and then place into the Collection. However, in terms of implementation details, the Engine is loosely coupled to these objects. Some of the key reasons for designing the Engine in this manner are:
One of the key roles for the Engine is to provide a way to package and return Classes. This functionality is implemented in one or many methods that start with the Get prefix. The Engine doesn't return the individual Classes. It places them into a Collection and then returns the Collection. Therefore, for an Engine to work in the ECC model, it must provide a way to return Classes. Following is a sample Engine that will create CUserInfo and ColUserInfo and then discuss the additional recommended properties/methods that should be included for the ECC design pattern. Visual Modeler was used to generate the initial diagram. Note The ECC design pattern uses a consistent naming convention for objects. At the Engine level it is a combination of a prefix and suffix around the object name. The prefix has two parts: an "I" for interface, and "OBP" for the application specific code or acronym. The suffix "Eng" is for Engine. This naming convention serves two purposes: objects can be found much easier in the registry, and the objects are grouped together in the Visual Basic IDE References Window. Figure 6: Basic engine diagram for the UserInfo Entity In the example, the key method is called GetUserInfo. The syntax is usually Get<Class name>. This allows an Engine to return one or many Collection and Classes. Another way to implement this is by using more specific methods, for example, GetUsersByEmail. It is quite intuitive that this method will return Users for a particular e-mail address. The method chosen in the sample implementation uses a more generic Search method that takes different optional parameters on which to find and return different users. The implementation details are left up to you. Other business specific functionalities can be added to the UserInfo Engine. For example, an EnrollUser function can be provided as a means to enroll a user into the system. As mentioned before, here is where the processes that the Entity must implement are incorporated into the ECC design. The processes seem to intuitively go where they belongwith the objects that exhibit this functionality. Furthermore, the Engine is a stateless object; therefore, it should not have properties. The base method that should be in this implementation follows:
Additional methods include:
Other functionality that could be added to the Engine includes:
Engine ImplementationThe Engine is in charge of creating a Collection and packing it with one or more Classes. In this capacity, the Engine is known as a class factory, because it is responsible for the creation of other objects. In the ECC design pattern, the Engine is a very specific factory that can create a specific Class. The sample code below demonstrates how a client would code and use the Engine to return a UserInfo Collection (ColUserInfo): Dim UserEngine As UserEng Dim Users As ColUserInfo Dim User as CUserInfo Dim strUserNumber as String Set UserEngine = New IOBPUserInfoEng strUserNumber = 10 Set Users = UserEngine.GetUserInfo("Token", strUserNumber) Set UserEngine = Nothing Set User = Users.Item(Users.Count) MsgBox User.Name & "'s E-Mail address is: " & User.EmailAddress As you can see, the client application only needs to know the user number to access the information about the user and does not need to know the underlining details, such as the field name of the requested data or how to manipulate the internal storage (a recordset or XML DOM object, for example). If the field name or the internal storage were to change, that change would occur in the Engine rather than in the client applications that use that Class. Notice that the Engine returns a Collection of users, and once this is completed the Engine is finished and can be deleted (set to nothing). Now you can use the properties of the items in the Collection. The implementation for the GetUserInfo method in the user Engine appears as follows: Public Function GetUserInfo(ByVal SecurityToken As Variant, _ Optional ByVal UserNumber As Variant, _ Optional ByVal DeletedRecords As DeleteRecordType = _ NondeletedRecords) As ColUserInfo On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim rsUserInfo As ADOR.Recordset Dim oColUserInfo As ColUserInfo Dim oCUserInfo As CUserInfo Dim vParameters() As Variant Dim col As Collection Dim inx As Long Set GetUserInfo = Nothing 'Check security token first If Trim(SecurityToken) = "" Then Err.Raise ERR_USERINFOINVALIDSECURITYTOKEN, _ "IOBPUserinfoEng.GetUserInfo PROC", _ "Invalid security token. Security Token cannot be empty." GoTo CleanExit End If ReDim vParameters(PARMUBOUND, 0) If IsMissing(UserNumber) Then UserNumber = Null ElseIf Trim(UserNumber) = "" Then UserNumber = Null End If 'Start packaging the parameters vParameters(PARMNAME, 0) = PARMNAMESP_USERINFOUSERNUMBER 'Name vParameters(PARMTYPE, 0) = PARMTYPESP_USERINFOUSERNUMBER 'Type vParameters(PARMLENGTH, 0) = 0 'Size vParameters(PARMDIR, 0) = adInput 'Direction vParameters(PARMVALUE, 0) = UserNumber 'Value If DeletedRecords <> AllRecords Then If Not IsEmptyArray(vParameters) Then inx = UBound(vParameters, 2) + 1 ReDim Preserve vParameters(PARMUBOUND, inx) Else inx = 0 ReDim vParameters(PARMUBOUND, inx) End If vParameters(PARMNAME, inx) = PARMNAMESP_USERINFODELETEFLAG vParameters(PARMTYPE, inx) = PARMTYPESP_USERINFODELETEFLAG vParameters(PARMLENGTH, inx) = 1 'Size vParameters(PARMDIR, inx) = adInput 'Direction vParameters(PARMVALUE, inx) = DeletedRecords 'Value End If 'Create the Data Access Layer Object If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "IOBPUserInfoEng.Get PROC", "Unable to create " & _ OBP_DA_CONNECTION & ". Return Code was: " & ErrorNum GoTo CleanExit End If 'Execute the SQL to retrieve the records If Not IsEmptyArray(vParameters) Then If Not oDALEng.Execute(SecurityToken, SP_S_USERINFO, _ vParameters, rsUserInfo) Then Err.Raise ERR_USERINFODALCALLFAILED, _ "IOBPUserInfoEng.Get PROC", _ "Call to Database Failed. SPName was: " & SP_S_BILL GoTo CleanExit End If Else If Not oDALEng.Execute(SecurityToken, SP_S_USERINFO, , _ rsUserInfo) Then Err.Raise ERR_USERINFODALCALLFAILED, _ "IOBPUserInfoEng.Get PROC", _ "Call to Database Failed. SPName was: " & SP_S_BILL GoTo CleanExit End If End If Set oDALEng = Nothing If (Not rsUserInfo Is Nothing) Then 'Start packaging the data into Class objects Set oColUserInfo = New ColUserInfo Set col = New Collection Do Until rsUserInfo.EOF Set oCUserInfo = New CUserInfo With oCUserInfo .ClassStorage = True .UserNumber = rsUserInfo.Fields(FN_USERINFOUSERNUMBER) .UserID = rsUserInfo.Fields(FN_USERINFOUSERID) .UserTypeID = rsUserInfo.Fields(FN_USERINFOUSERTYPEID) <> .SecurityToken = SecurityToken .IsNew = False .ClassStorage = False .Dirty = False End With col.Add oCUserInfo rsUserInfo.MoveNext Set oCUserInfo = Nothing Loop 'Place information into the Collection object If Not oColUserInfo.Load(SecurityToken, col) Then Err.Raise ERR_USERINFOLOADFAILED, _ "IOBPUserInfoEng.GetUserInfo PROC", _ "Load failed for private collection" GoTo CleanExit End If 'Success Set GetUserInfo = oColUserInfo End If <> End Function The GetUserInfo method is a prime example of how the Get Engine methods should be defined. These are the basic recommendations:
Implementation NoteThe ECC model is most effective when the business components are running in the same process space as the client. The extensive use of object properties is the main reason for this. Each call to a property requires a round-trip from the client to the object. If these round trips cross process boundaries, the overhead becomes quite significant. In a Web-based solution, this means that the objects run in the same process space as Web site or a close as possible. In this case, the Web site is best configured to run in a separate memory space. This provides fault isolation, which allows the Web server to restart the process if it should go down. The authors of this article have had great success on multiple projects and clients using this approach. We found that the greatest strength of the ECC design pattern is the template-like approach it encourages. As leaders of projects, we were able to teach developers the pattern and then quickly review the code for exceptions to it. Since it was a pattern proven through previous refinements of the implementation, it allowed us to spend more time on reviewing client-specific requirements. Moreover, most of the code became a cut-and-paste operation; once you create the first business component (template), the other components can use it as a baseline. In general, debugging and designing of the objects became more common across all the components, allowing developers to more easily interchange responsibilities. Changes occurred many times during the construction phase of the projects, ranging from UI to database. We were able to implement many of the changes with minimal impact to the system. These are some examples of the changes that occurred:
These are just a few examples that demonstrate the flexibility of the ECC design pattern. ConclusionThe Engine-Collection-Class design pattern provides an implementation-independent application framework. The architecture consists of the Engine, which controls the creation of Classes; the Collection, which stores the Classes; and the Class, which represents a real-world entity. This objectification provides clarity in object creation as well as a consistent interface for business rule validation, data retrieval, and data manipulation. In addition, it provides a loose coupled architecture, where the client and the components are free to change without affecting one another. Unlike a purely service-based approach, ECC models an entity's data first, and then the processes. This means that a single component can be reused for many different portions of the business logic and that the business rules will be encapsulated in one component. ReferencesBooksEddon, Guy, and Henry Eddon. Programming Components with Microsoft Visual Basic 6.0, 2nd Edition. Redmond, WA: Microsoft Press, 1998. Kirtland, Mary. Designing Component Based Applications. Redmond, WA: Microsoft Press, 1999. Pattison, Ted. Programming Distributed Applications with COM and Microsoft Visual Basic 6.0. Redmond, WA: Microsoft Press, 1998. Platt, David S. Understanding COM+. Redmond, WA: Microsoft Press, 1999. Stamatakis, William. Visual Basic Design Patterns. Redmond, WA: Microsoft Press, 2000. ArticlesHandle Logons in Windows NT and Windows 2000 with Your Own Logon Session Broker The COM+ Security Model Gets You Out of the Security Programming Business Top Windows DNA Performance Mistakes and How to Prevent Them |
Contact Us | E-Mail this Page | MSDN Flash Newsletter |
© 2003 Microsoft Corporation. All rights reserved. Terms of Use Privacy Statement Accessibility |