At this point, you and your customer should have a pretty good idea of the problem you're trying to solve. More important, you and your customer should agree what the problem is!
Once the problem is defined, you can turn your attention to the conceptual design of the application. The four key tasks to accomplish during conceptual design are as follows:
We will discuss each of these tasks in detail in the sections that follow.
You should begin the conceptual design process by examining the requirements documents for information about persistent data. These documents provide information that will help define the databases the application will use. These could be relational databases or object-oriented databases, but here we'll focus on issues related to relational databases since these are far more prevalent. In some cases, the databases you need to use will already be defined. This certainly simplifies the task, but it doesn't leave us much to talk about, so for the sake of discussion let's assume that you're starting from scratch.
Even if the exact database tables you need don't currently exist, you do need to consider how your data fits in with other data maintained by the organization. You need to consider where the data will reside and who can access it. Will the data be stored on centrally located servers or on individual workstations? Will data need to be replicated to multiple locations in order to meet performance requirements? If so, will the data be partitioned, or do you need to worry about conflicting updates from multiple sites? For the Island Hopper classified ads application, the database is maintained on a single database server. Your application might have more complex requirements.
One very common way to model persistent data is a technique known as entity-relationship modeling. To begin data modeling, look for real-world things, or "entities," in the problem definition. An easy way to start is to look for nouns in the problem definition—these often correspond to entities in the model. An entity has a set of attributes that describe it. A particular entity has specific values for each attribute. All the entities that share a set of attributes define an entity type.
In object-oriented terms, "entity" = "object," "entity type" = "class," and "attribute" = "property."
When you are using a relational database, entity types are modeled as database tables. Attributes correspond to columns in the database table. A complete definition of the entity type, including the names and meanings of all attributes and any constraints is called a schema. One of the main constraints to consider is which attribute(s) can be used to identify each entity. These attributes are used to define database keys. A database key is one or more columns in a table used to identify a record; database keys are commonly used as an index for the table.
This chapter merely scratches the surface of data modeling. For more information, see the sources listed in the bibliography.
After examining the problem definition and functional specification for the Island Hopper classified ads application, you should be able to identify the following three main entity types:
Customer type: Includes name, mailing address, password, and customer ID attributes. The customer ID attribute uniquely identifies a customer. The name and mailing address attributes might need to be divided into separate elements—for example, first name, last name, street, city, state, and zip code.
Advertisement type: Includes category, title, body text, start date, end date, customer, and advertisement ID attributes. The advertisement ID attribute uniquely identifies an advertisement.
Invoice type: Includes customer, list of ads, date, total amount, and invoice ID attributes. The invoice ID uniquely identifies an invoice. The list of ads is a multivalued attribute. We'll see how to deal with multivalued attributes in the section "Data object methods" later in this chapter.
Some implicit relationships exist between these entities. Advertisements and invoices both refer to customers. Invoices refer to a list of advertisements.
Relationship types define associations between entity types. Most relationships are between two entity types. Each entity type plays a particular role in the relationship type. Normally, you don't need to explicitly call out role names when the entity types are distinct, but the names can be very helpful when you are defining relationships between different entities of the same type. (These are called recursive relationships.) For example, two employee entities might be related by a supervises relationship, one entity in the role of supervisor and the other in the role of supervisee.
To locate the relationship types in your data model, you should look for attributes in your entities that seem to correspond to other entities. For example, both the advertisement and invoice entities mentioned above contain a customer attribute. You should also refer back to the problem definition. Verbs in the problem definition can point out relationships that you haven't recognized yet.
Relationship types usually have a structural constraint associated with them. This constraint defines how many relationship instances an entity can participate in. Usually, the constraint is specified as one-to-one (1:1), one- to-many (1:N), or many-to-many (M:N). For example, if one customer can submit many advertisements, each advertisement is submitted by exactly one customer, and a customer is deleted if no advertisements exist, the relationship between customers and advertisements is 1:N. You can also specify the minimum and maximum number of instances for each entity.
Relationship types can also have attributes associated with them. For 1:1 and 1:N relationships, these attributes can be stored in the entity representing the "1" side of the relationship. However, for M:N relationships, the attributes are a characteristic of the relationship itself.
In the relational database, relationship types can be represented as attributes or tables. For 1:N relationships, the table representing the "N" side of the relationship includes a column corresponding to the key of the other table. (1:1 is just a special case of 1:N.) Attributes for the relationship itself would be stored as columns in the table on the "1" side of the relationship. For example, the Island Hopper advertisement entity should include a customer ID column to represent the 1:N relationship between customers and advertisements. For M:N relationships, a separate table is usually used to represent the relationship. This table includes columns corresponding to keys for each of the tables in the relationship, as well as columns for each attribute of the relationship itself.
Figure 7-4 shows a data model for the Island Hopper classified ads application that includes both entity and relationship types. Notice that some of the attributes have been refined to reflect the relationships between the entities.
Figure 7-4. First cut at a data model for the Island Hopper application.
In a relational database, data is usually factored according to the following data normalization rules. These rules help prevent common data problems related to synchronization and consistency of changes.
OK, those aren't the formal rules…but you get the idea. (If you really want to know the rules, check out any book on database design.) Your first cut at a data model might not be normalized (unless you've been doing data modeling for a while and these rules are burned into your subconscious). After you've created the basic tables and relationships, normalize the database. But keep in mind that performance requirements might require denormalizing the database.
Figure 7-5 shows the final data model for the Island Hopper classified ads application. A new entity type, category, has been defined. This entity type will remove the name of the category from every advertisement, making it easier to maintain a category list. Customer passwords have been split into a separate entity to help secure the passwords. The product and payment entities have been introduced to help with invoicing.
Figure 7-5. The final data model for the Island Hopper application.
In addition, the multivalued list of ads attribute in the invoice entity type has been converted to a 1:N relationship between invoices and advertisements, with an invoice ID attribute stored in each advertisement. This is how multivalued attributes are normalized in relational databases: the attribute is converted to a separate entity that has a 1:N relationship with the original entity.
You might also have noticed that the customer entity includes a balance attribute. The customer balance could be computed by querying invoices and payments, but this process would be very slow. So the database is slightly denormalized to include the outstanding balance in the customer entity.
Many tools are available for documenting a data model. In this book, we use the Microsoft Visual Database Tools to create our data models. With the Visual Database Tools, you can connect to and explore any ODBC-compliant database. The Database Designer tool provides a graphical environment for the following tasks:
Other data tools provide a graphical user interface that lets you add, update, and delete data in the database; design and execute complex queries; and so on.
The Microsoft Visual Database Tools are included with Microsoft Visual Studio Enterprise Edition, Microsoft Visual InterDev, Microsoft Visual C++ Enterprise Edition, Microsoft Visual Basic 6.0 Enterprise Edition, and as an add-in for Microsoft Visual Basic 5.0 Enterprise Edition.
Once you have your database schema in place, you can start looking at the COM classes you'll use to implement your application. Remember that the recommended application architecture for MTS applications is a three-tier, component-based architecture. As we've seen, distributed applications follow a common usage pattern. It turns out that this pattern maps quite nicely to the three-tier model, as shown in Figure 7-6. Users are presented with data by the presentation layer. The user can request more data through the presentation layer. The presentation layer uses services of the business layer, commonly called business objects, to retrieve the data. In addition to viewing the data, the user can update the data through the presentation layer. The presentation layer uses business objects to update the data. The business objects in turn will rely on services of the data access layer, called data objects, to interact with persistent data.
As you'll recall from Chapter 1, several reasons exist for introducing a layer of business objects between the presentation and data access layers. A key reason is to reduce the number of database connections required, to improve scalability. When presentation and data access layers are directly connected, each client maintains one or more connections to the database—and database connections are very expensive! A second reason is to isolate the presentation layer from details of the database schema. The presentation layer works with a logical view of the data exposed by the business objects and does not need to worry about where or how the data is actually stored. In general, the presentation layer depends only on the business layer, the business layer depends only on the data access layer, and the data access layer encapsulates knowledge of the physical database structure and data access mechanisms. This separation makes it easier to maintain applications since the presentation, business, and data access layers can evolve independently over time.
Figure 7-6. A common usage pattern for MTS applications.
The following four basic types of objects are found in three-tier applications:
You might be wondering what the difference is between the query and update objects and a data object. After all, query and update do sound like they have something to do with data access. Business objects (query and update) are defined in terms of real-world business operations and are oblivious to actual data sources. A data object encapsulates access to the actual data. A business object is a client of a data object.
When most people talk about three-tier, component-based applications, they'll say that applications are implemented from "business objects" and "data objects." However, as we saw in Chapter 2, COM objects are run- time instances of COM classes, which are packaged in COM components. So what we're really talking about here is modeling objects as COM classes. We'll discuss packaging COM classes into COM components in the section "Grouping Classes into Components" later in this chapter.
Many tools are available that enable you to document your object design. In this book, we use Microsoft Visual Modeler. Visual Modeler is an entry-level design tool based on UML and specifically designed for documenting designs for component-based applications that will be implemented using Visual Basic.
Visual Modeler is available as an add-on for Visual Basic 5.0 and will be included with Microsoft Visual Studio starting with version 6.0.
Visual Modeler supports a subset of UML. It lets you create class diagrams, component diagrams, and deployment diagrams. Class diagrams describe the logical structure of your application—the classes, logical packages, and the relationships between them. Component diagrams describe the physical structure of your application—how classes are grouped into physical DLLs and executables. Deployment diagrams show how the application processes will be distributed among machines in your system.
Visual Modeler is a round-trip engineering tool. You can create models and then generate Visual Basic code based on those models. You can also generate models from existing Visual Basic code. This capability is particularly useful for maintenance projects and for documenting the final design of your application.
For more information about UML, see the references listed in the bibliography.
To begin documenting your design using Visual Modeler, simply start up the application. A blank project window will appear, with a Logical View/Three- Tiered Service Model diagram window. By expanding Logical View in the Browser pane, you can view the different layers of the three-tier model.
You document your data and business objects as classes in this view. To define a class, use the Visual Modeler Class Wizard, shown in Figure 7-7. The wizard lets you specify which tier the class belongs to, as well as its properties and methods.
Figure 7-7. Using the Class Wizard to define a class in Visual Modeler.
You'll continue to update this diagram and other diagrams in the project as you proceed through the application design process. We'll discuss the component and deployment diagrams in the section "Documenting the Physical Architecture" later in this chapter.
A good place to begin your conceptual design is to model the data objects your application will use. Note, however, that design is an iterative process. You don't need to get the data objects exactly right before you move on to business objects or the presentation layer. As you look at the other tiers, you'll probably find an essential piece of data that you just can't get to or a query that you didn't anticipate. Then you can take another look at the data objects and adjust their interfaces appropriately. It's a lot easier to make these adjustments during the design process than after you've started implementation!
Data objects are responsible for pieces of data. The data object is the only thing that should be reading and writing its piece of data to the persistent store. The persistent store can be anything, but it is most commonly a relational database. The unit of data managed by a data object is usually a single table in the database. Data objects are responsible for the accuracy, completeness, and consistency of the data they own. If any invalid data is stored, the data object is at fault. Data objects can rely on services provided by the underlying store to ensure data integrity. For example, if the data is stored in a relational database, the DBMS's referential integrity rules might be used to ensure consistency between tables. The important thing is to keep each rule in one place so that it can easily be modified if necessary.
To begin modeling you first decide which data objects you'll need. For most projects, a good way to start is to define one data object for each database table. Then you can look at the details of each object.
In the traditional object-oriented approach to modeling, you would define a data variable, or property, for each element of the data encapsulated by a data object—for example, one property per column in a database table. Then you would define property accessor methods, as well as methods for the remaining behavior of the object. Things aren't quite this simple when you start working with components that will be used in a distributed environment, particularly those that will be used in MTS.
Recall from Chapter 4 that MTS provides many run-time services to manage object state and resource usage, based on transaction boundaries, as well as services to manage secure object access, based on package, component, and interface boundaries. To get the most benefit from these services, your objects need to be designed with the MTS application server programming model in mind. In particular, you'll need to consider the following object characteristics:
Although we'll look at these items one by one, they can't really be treated independently. How you decide to manage object state will influence the parameter lists of methods you expose. And how you manage state, resources, and transactions will influence which methods you need. Security requirements will influence how those methods are grouped into COM interfaces. The expected clients —in particular, whether there are script-based clients—and how they will use an object influences its state, resource, and transaction management, as well as how methods are grouped into interfaces.
Let's start by looking at state management. After all, the only reason we have data objects is to encapsulate access to persistent data. State management is a complex topic—no one way is correct for all situations. We'll look at some of the issues here and at an approach that will work for most simple applications. As you gain experience with designing applications for MTS and work on more complex applications, you might decide that a different approach is more appropriate for your application.
One approach would be to read data from the persistent store into a data object and then provide property accessor methods to let clients get to the data. This approach presents a number of potential problems. First is the possibility of an enormous communication overhead if the calling application or component is running in a different process or on a different machine. Each call to an accessor method results in an expensive interprocess or network call to return a small piece of data, limiting performance. Second, if the calling application or component can update the data, resource management becomes very complicated. How long do you need to hold onto database connections and locks? For the duration of each accessor method call? Across calls? To be safe, you probably need to hold onto them for the lifetime of the object—or you'll need to rely on the client to call a special method to indicate that it has finished making updates and that the data should be consistent. This solution can also lead to scalability problems. And it makes writing correct applications harder than necessary.
For a similar reason, this property-based approach might make it difficult to take advantage of the MTS Just-In-Time (JIT) activation feature, object pooling, or automatic transactions. Remember that MTS reclaims object instances at transaction boundaries. Callers cannot rely on the in-memory state of an object across a transaction boundary. If data objects are instantiated only for the lifetime of a business object method call and the business object method call defines the transaction scope, this approach might be perfectly acceptable. In any case, if you design your data objects to retain transient per-object state, you need to be very, very aware of all the issues surrounding the objects' lifetimes.
A more straightforward approach for most data objects is to simply pass all the data through the object back to the client. Objects that use this approach are sometimes called stateless objects because they don't keep any state in object data members. With this approach, the issues mentioned above go away. All the data is transferred in a single method call. Resource management is simplified. As far as the object is concerned, when a method call completes, all the resources used by the object are no longer needed. In fact, the object itself isn't needed between method calls—as far as the client is concerned, any object can be used to make a method call because all the required inputs are passed to the object with the call. So stateless objects work extremely well with JIT activation and object pooling.
In general, stateless data objects produce highly scalable solutions. However, you might find situations in which the object state is extremely expensive to compute, or in which you have a priori knowledge of how the object will be used and are willing to take the extra care needed to work with stateful data objects. Consider the trade-offs carefully before you decide how to manage state in your data objects.
For the remainder of our discussion of data object design, we'll assume that the data objects are stateless—in other words, all data required for a particular action must be passed to an object from the client and all data created by the action will be passed back to the client.
Next let's look at the object behavior. Every data object will expose four basic types of methods: Add, Update, Delete, and Query. Usually, only one Add method and one Delete method are present, but a data object might have multiple Update and Query methods. As mentioned, for each method the client needs to pass in all the information required to complete the action. However, if clients are calling across a slow communications link, you want to be certain that you aren't requiring a lot of extraneous data in each call. For example, if customer address changes are relatively rare and customer password changes are frequent, you wouldn't want to have one customer Update method that forced the client to pass in all the information about the customer's address just to update the password. Instead, you should have separate UpdateAddress and UpdatePassword methods. You'll probably also want a separate Query method for each way of retrieving individual records and lists of records.
You can use several techniques to pass information to and from your methods. The simplest approach is to pass each data element as a parameter, but this can become unwieldy if you have a lot of data elements. Another approach is to stuff all the data into a Variant array. The advantage of this approach is that COM automatically handles the marshaling details. But constructing the array and pulling information back out of it can be complicated—especially for clients written in C++. Finally, it is possible to pass an entire object back and forth. Normally, COM does not marshal objects by value; you just get an interface pointer. So accessing data from the object involves remote calls. However, it is possible to have an entire object marshaled to the client. The ADO disconnected recordsets discussed in Chapter 3 use this technique to pass the entire recordset to the client.
For most data object methods, you can use the following guidelines for method parameter passing:
At this point, we have a pretty good first cut at our data objects. Let's look briefly at the remaining characteristics on our list that we should consider when modeling data objects.
With stateless objects, it's possible for MTS to eliminate transient state between method calls. It's also possible to ensure that database connections, locks, and so on are not held open between method calls. Your methods should acquire the resources they need as late as possible, hold them for the shortest possible amount of time, and let MTS know when the resources and object state can be reclaimed. The mechanism you use to let MTS know when resources and object state can be reclaimed is the ObjectContext SetComplete method. We'll discuss this method in further detail in Chapter 8, when we discuss implementing data objects.
Another important aspect of data objects is their transactional requirements. Remember from Chapter 4 that transactions are how you ensure that data updates are performed consistently across multiple databases or data objects. In general, at a minimum data objects should be marked as "supports a transaction." When they are marked in this way, data objects will participate in existing MTS transactions. However, if no transaction is currently in progress, MTS will not create a transaction for the data object. As long as the data object method called issues only one database statement, you can usually rely on the underlying database to provide transaction protection. If your data object issues multiple database statements or uses subordinate objects, you should mark the data object as "requires a transaction" to ensure that all actions are rolled back in the event of a failure. If you need your object methods to succeed or fail independent of other object methods, you should mark your data object as "requires a new transaction."
The final two considerations, security and client use, primarily affect how you group methods into COM interfaces. In general, security isn't a major issue during data object design. (It does become important when you deploy the application, as access to the actual data stores must be enabled.) Remember that the only things calling your data objects are business objects. Typically, these will be trusted to perform all operations on the data object. Authorization checking would be performed on calls to the business object rather than on calls to the data object. If for some reason you do need to restrict access to some methods on your data objects, however, you should split those methods out into a separate interface, as this is the smallest authorization unit in MTS.
A potential problem with defining multiple interfaces is that some clients can access only one interface per object—the default IDispatch. If your clients are implemented using languages with this restriction (like VBScript), you might need to split the data object into two separate objects, not just two separate interfaces.
Client implementation languages can also affect the data types you can use for method parameters. Most languages support only Automation-compatible data types. Unless you know that all your clients will be implemented in C or C++, you should stick with the Automation-compatible types.
Enough theory. Let's look at an example. The data model for Island Hopper contains seven tables: Categories, Customers, Products, CustomerPasswords, Invoices, Payments, and Advertisements. We'll start by modeling each table as a separate data object, as shown below.
|db_AdC||Stores information about an advertisement|
|db_CategoryC||Stores the main descriptive category for advertised items|
|db_CustomerC||Stores information about a specific customer for billing and contact purposes|
|db_CustomerPasswordC||Stores customer password|
|db_InvoiceC||Stores information about a particular invoice|
|db_PaymentC||Stores information about a payment from a customer|
|db_ProductC||Stores product/billing codes for invoicing|
As a rough start, we can define Add, Delete, and Update methods for each object; the Add and Update methods will include parameters for each data element provided by the client. We can use the functional requirements to get a pretty good idea of what kind of Query methods we'll need too. Island Hopper uses the naming convention GetBy<X> to name methods that retrieve a single record using <X> as the key and ListBy<X> to name methods that retrieve a set of records using <X> as the key.
From the functional requirements, we can guess that CustomerPasswords will need to retrieve single records by customer ID. We'll also need a way to add, delete, and update passwords. Thus we'll need a db_CustomerPasswordC object with four methods, as shown here:
Public Sub Add(ByVal lngCustomerID As Long, _ ByVal strPassword As String) Public Sub Delete(ByVal lngCustomerID As Long) Public Sub Update(ByVal lngCustomerID As Long, _ ByVal strPassword As String) Function GetByID(ByVal lngCustomerID As Long) As ADODB.Recordset
We can also anticipate that we might need to retrieve single customer records by customer ID or e-mail address or a list of customers by last name, so we'll define three Query methods for db_CustomerC. And we'll need to be able to update a customer's outstanding balance or contact information, so we'll define two Update methods, as shown here:
Public Sub Add(ByVal lngCustomerID As Long, _ ByVal strEmail As String, ByVal strLastName As String, _ ByVal strFirstName As String, ByVal strAddress As String, _ ByVal strCity As String, ByVal strState As String, _ ByVal strPostalCode As String, ByVal strPhone As String) Public Sub Delete(ByVal lngCustomerID As Long) Public Sub Update(ByVal lngCustomerID As Long, _ ByVal strEmail As String, ByVal strLastName As String, _ ByVal strFirstName As String, ByVal strAddress As String, _ ByVal strCity As String, ByVal strState As String, _ ByVal strPostalCode As String, ByVal strPhone As String) Public Sub UpdateBalance(ByVal lngCustomerID As Long, _ ByVal dblBal As Double) Public Function GetByID(ByVal lngCustomerID As Long) _ As ADODB.Recordset Public Function GetByEmail(ByVal strEmail As String) _ As ADODB.Recordset Public Function ListByLastName(ByVal strLastName As String) _ As ADODB.Recordset
We can repeat this process for each of the data tables to produce a first cut at methods for each data object. Now would also be a good time to identify any relationships between the data objects. Does one data object need to use methods of another object to do its work? Is there some common behavior that you want to pull out into a separate interface or base component? At this stage, the answer to both questions is probably no, unless you have a complex data model. But if relationships exist, you should document them. The Island Hopper application has no relationships between its data objects.
The complete component design for the Island Hopper classified ads application is contained in the CLASSIFIED.MDL file, which is located in the \IslandHopper\Design directory on the companion CD. You can use Microsoft Visual Modeler or Rational Rose to view this model.
With the data objects under control, we can turn our attention to the business objects. This is where things really start to get fun!
Business objects encapsulate real-world business operations, independent of how the data they use is actually stored. Your business objects will use the data objects you've already defined. (Of course, if you find something missing from the data objects while you're looking at the business objects, go back and update the data object model.) Business objects usually encapsulate multiple- step operations and can access multiple data objects.
Defining a good set of business objects is hard work. The goal here is to define objects that do all the "thinking" in the application. Business objects control sequencing and enforcement of business rules, as well as the transactional integrity of the operations they perform. They are the heart and soul of your application. Great business objects are important assets that can be reused in many applications.
The allure of reuse and the desire to define a perfect business object can lead to "analysis paralysis." It's important to design your components around a small number of known clients—one or two immediate uses, perhaps one down the road. Don't try to design components that will be all things to all clients.
As you begin to create a repository of components and work on additional projects, you'll begin to see patterns that can help you refine your components and make them useful to a broader range of clients. The wonderful thing about COM is that you can implement these refinements as new interfaces on existing components, continuing to support existing applications while making the components useful to new clients as well.
The six design considerations for data objects mentioned in the section "Modeling the Data Objects" earlier in this chapter apply to business objects as well. But there isn't as obvious a starting point for determining what business objects to apply the considerations to. A reasonable way to choose business objects is to examine the scenarios in your functional specification for real-world objects that are acted on by the application. For example, in the scenario "Place a Classified Ad," the real-world object is the advertisement. Each business object should encapsulate functionality related to one real-world object.
Less well-defined is whether each real-world object should correspond to exactly one business object. For objects with a small number of actions, one object is probably just fine. But for objects with many actions, you might want to split up the actions—not just into separate interfaces, but into separate objects. This helps make your business objects more reusable. Business objects that try to be all things to all people can easily end up as nothing to anyone. These "kitchen-sink" objects can have such a large working set or per- object memory footprint that no one wants to use them. At this point, if you see an obvious split in functionality for different types of clients or in how frequently different functionality will be used, it's probably worth defining a separate business object for special client types or for the more rarely used functionality. Otherwise, just start with one business object per real-world object.
Closely related to the question of what business objects you define is the question of what methods those objects support. The general rule here is that each method should do exactly one unit of work and each unit of work should be implemented in exactly one method. Fine-grained methods that do a single unit of work are easily composed into higher level operations. This is just good old- fashioned structured design…with a twist. Composition across component boundaries introduces some interesting issues that we'll look at in the section "Composition, resource management, and transactions" later in this chapter.
You can start looking for business object methods by examining the scenarios in your functional specification. High-level business object methods typically encapsulate each piece of a scenario between collecting information from the user and returning information to the user. These methods are usually the root of a distributed transaction and use services from several other business and data objects. As you go through the high-level, scenario-driven methods, you'll start to see common behavior. That behavior should be split out into separate methods, to help ensure consistency across your application(s).
Business object methods also encapsulate and enforce business rules. Each rule can be implemented as a separate method or as logic within a larger method. The important thing is to ensure that each rule appears in only one place, just in case that rule changes.
Finally, business object methods can encapsulate and enforce data rules. In particular, business objects encapsulate rules that span multiple data objects. For example, an invoice component in our Island Hopper application could ensure that invoices are generated only when both the header and at least one line-item have been provided.
State management can be more complicated for business objects than it is for data objects—mainly because you have more choices. Recall the four basic types of state from Chapter 4:
Now we know that business objects never directly interact with persisted state—they use the data objects. What about the other choices? Let's think about how business objects are used.
First, business objects can be created and used directly by the presentation layer. The presentation layer usually runs on individual user workstations, not on the machines on which the business objects are running. The workstations may be connected to the business object servers by slow links. Minimizing network traffic between the machines is important. In addition, users can take a long time to do things. Holding shared resources or keeping transactions open until the user finishes a task probably isn't feasible. For this type of object, you should not maintain per-object transient state. Each method should stand alone, using the object context to indicate when it has finished with the object's state.
Business objects can also be created and used from other business objects. In this case, the communication overhead might not be a factor and the object might exist only within the scope of a single transaction. For this type of object, you might elect to maintain per-object transient state. This state should only be maintained within a transaction boundary, so that you can take advantage of MTS JIT activation and object pooling.
There will be a few cases in which you need to share information across multiple objects (or across multiple transactions) and the overhead of saving the information in persistent storage is just too high. A good example of this situation is a Web page counter. If the counter is read from storage, incremented, and written back to storage every time a Web page is visited, the system will not be very responsive. Instead, you could cache the counter in shared memory for speedy access and update and periodically write the new value to persistent storage. To share transient state across transaction or object boundaries in MTS, you use the Shared Property Manager (SPM). We'll discuss the details in Chapter 9, when we talk about implementing business objects.
The way your business objects are used also affects resource management and transactional behavior. You must remember that most business objects will be composed with other components to produce higher-level behavior. Your objects must behave correctly whether they are used directly or in conjunction with other objects.
As with data objects, if your business objects use resources, they should grab the resources as late as possible and release them as quickly as possible. Your objects should use the object context method to tell MTS when object transient state and resources can be reclaimed. Your objects should also use the object context method to create any subordinate objects that are hosted in MTS.
When you use the object context method to create subordinate objects, context information flows from your object to the newly created object. One very important piece of this information is the transaction that the object is a part of. As we saw in Chapter 4, objects that participate in a single transaction all get to vote on the outcome of the transaction. If any object votes to abort the transaction, the transaction is aborted and any managed resources are rolled back. This capability makes error recovery for actions composed from several objects much easier. Thus, it is important to specify the transactional requirements of your business objects correctly.
In general, business objects whose methods use multiple subordinate data objects should be marked as "requires a transaction" to ensure that all actions are correctly rolled back on failures. Business objects that do not access multiple data objects can be marked as "supports transactions," which ensures that if the object is ever composed with a transacted object, it correctly participates in the transaction but does not force transaction overhead if none is needed. If the success or failure of a business object should be independent of the success or failure of its clients, the business object should be marked as "requires a new transaction." Objects that don't access any data objects can be marked as "does not support transactions." If your objects need to retain per-object transient state across method calls, consider the impact of the transaction setting carefully.
Security issues are also more complex for business objects than they are for data objects. Recall from Chapter 4 that MTS role-based security authorizes access to objects only on entry to a package and that authorized roles can be specified for packages, components, and interfaces. Calls from one object to another within the same package are not checked.
You should look at the security requirements defined during requirements analysis to determine whether any methods on your objects require authorization checks or whether any other data protection or authentication requirements apply. If so, group methods with similar requirements into interfaces. If any methods have extremely stringent security requirements, consider splitting off those methods into a completely separate class. You should also split off methods into a separate class if the class will be called via IDispatch —for example, from script code in an ASP page. You cannot define roles specific to IDispatch. If your classes will be accessed via IDispatch , you will need to define roles at the package or component level.
Let's look at the Island Hopper application again. If we examine the functional specification, a few real-world objects jump out: advertisements, customers, invoices, and payments. We'll start by defining a business object for each real- world object, as shown in the table below.
|bus_AdC||Places, updates, and deletes advertisements|
|bus_CustomerC||Validates customer information|
|bus_InvoiceC||Generates customer invoices for advertisements|
|bus_PaymentC||Processes payments from customers|
Let's look at the bus_CustomerC component in more detail. We can use the functional requirements to get a pretty good idea of the kinds of methods we need. We'll need methods to add a new customer, delete an existing customer, and update an existing customer. We'll also need to be able to look up customers by customer ID, customer e-mail name, and customer last name. And we'll need a method to validate customer login information. The necessary methods are shown in the following code:
Public Function Add(ByVal strEmail As String, _ ByVal strLastName As String, ByVal strFirstName As String, _ ByVal strAddress As String, ByVal strCity As String, _ ByVal strState As String, ByVal strPostalCode As String, _ ByVal strCountry As String, ByVal strPhone As String, _ ByVal strPassword As String) As Long Public Sub Delete(ByVal lngCustomerID As Long) Public Sub Update(ByVal lngCustomerID As Long, _ ByVal strEmail As String, ByVal strLastName As String, _ ByVal strFirstName As String, ByVal strAddress As String, _ ByVal strCity As String, ByVal strState As String, _ ByVal strPostalCode As String, ByVal strCountry As String, _ ByVal strPhone As String, ByVal strPassword As String) Public Sub GetByID(ByVal lngCustomerID As Long, _ rsCustomer As ADODB.Recordset, _ Optional rsPassword As ADODB.Recordset) Public Sub GetByEmail(ByVal strEmail As String, _ rsCustomer As ADODB.Recordset, rsPassword As ADODB.Recordset) Public Function ListByLastName(strLastName As String) _ As ADODB.Recordset Public Function Validate(ByVal strEmail As String, _ ByVal strPassword As String) As ADODB.Recordset
The bus_CustomerC object will be used directly by the presentation layer, to maintain customer information. Thus, we will not maintain per-object state. Instead, we will pass all the information required by each function as input parameters and return all information generated by each function as output parameters or return values. The object relies on the db_CustomerC object to access the persistent data and is itself used by the bus_AdC object, so we'll mark bus_CustomerC as "requires a transaction."
From a security perspective, it's reasonable to assume that we might want to restrict access to the Add, Delete, and Update methods. The GetByID, GetByEmail, and ListByLastName methods also group naturally. These two groups of methods are good candidates for splitting off into separate interfaces. However, we know that one of the client applications for this component is an Internet application that will be implemented using ASP. The Internet application needs to use the Update methods and the lookup methods, but scripts in ASP can use IDispatch only to access objects. Furthermore, ASP applications usually run as the same identity, regardless of who requested the page. There doesn't appear to be much point in splitting the methods into separate interfaces or separate components after all.
The complete component design for the Island Hopper classified ads application is contained in the CLASSIFIED.MDL file, which is located in the \IslandHopper\Design directory on the companion CD. You can use Microsoft Visual Modeler or Rational Rose to view this model.
The final task in application modeling is modeling the presentation layer. Our primary focus in this book is the middle tier of the three-tier architecture, so we won't spend a lot of time on presentation layer modeling. The key task in this phase is documenting what the user interface of your application will look like (roughly) and how the user will work through the various screens.
You should also consider the technology constraints on your presentation layer design. Does the application need to run in a Web browser, or can it be a full- featured Windows application? If it's a Web application, can you assume that the browser supports anything other than pure HTML?
If your client application must be pure HTML, you can't really do anything on the client machine at all. You'll need to call your business objects from server-side scripts. Recall from Chapter 5 that you can use ASP to format HTML pages and run server-side scripts that use your business objects.
On the other hand, if your client application can use objects directly, it might be possible to move some of the work from your server machines to the client machine. You can pass disconnected recordsets all the way to the presentation layer for display and user interaction. You might want to create specialized ActiveX controls or data validation components to run in the presentation layer.
We'll look at the presentation layer in a little more detail in Chapter 11.