Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Distributed Object Design


June 1999: Features: Distributed Object Design

CORBA. COM+. Enterprise Java Beans (EJB). Three leading-edge technologies based on the concept of distributing objects, often in component form, across a multi-node target environment. There is a seemingly endless supply of magazine articles and books telling you how wonderful these technologies are, telling you about their inner secrets, telling you why one is better than the other. But, how do you actually approach the design process for a distributed environment?

This article presents a process, based on the techniques presented in Designing Object-Oriented Software (Prentice Hall, 1990) by Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener, for refactoring a traditional object design to deploy it to a distributed object environment. This process is appropriate for organizations that take a peer-to-peer approach to networking, often referred to as n-tier client/server, but don’t use mobile agent technology. Although you can apply this approach to two- and three-tier client/server environments, you’ll quickly discover that it’s overkill in those environments. Mobile agent technology focuses on different design issues than statically distributed objects (issues such as security, persistence of mobile objects, and versioning, to name a few) that are beyond the scope of this article.

I will also not address the differences between the current implementation technologies (COM+, CORBA, EJB, and so on) for distributed object development. My experience is that although the underlying technologies change over time, the design process stays the same. Although some of the terminology has changed since the Wirfs-Brock gang first wrote their book, the fundamentals are still the same. I expect that when today’s implementation technologies go the way of the dodo, as have the vast majority of implementation technologies before them, this approach will still be relevant.

The Process of Distributing Your Object-Oriented Design

The basic idea behind this process is to identify domain components, large-scale components that encapsulate collections of business/domain classes, and then to deploy those components intelligently within your hardware environment. I use a bank, a common example, throughout this article to help illustrate this process. Figure 1 presents a high-level overview of the bank’s class diagram, using the Unified Modeling Language (UML) notation, which shows the main classes and their interrelationships for a simple teller application.

Figure 1: Initial Class Model for a Bank

There are nine steps, typically performed iteratively, to the process of distributing your object design.

• Handle non-business/domain classes

• Define class contracts

• Simplify inheritance and aggregation hierarchies

• Identify domain components

• Define domain-component contracts

• Simplify interfaces

• Map to physical hardware design

• Add implementation details

• Distribute the user interface.

Step 1: Handle non-business/domain and process classes. Figure 2 depicts a common layering of an application’s classes, indicating that there are five types of classes you will build your applications from.

Figure 2: Layering within Class Models

User interface classes, such as the Screen hierarchy in Figure 1 encapsulate the screens and reports that make up your system’s user interface. Business/domain classes, also known as entity classes, implement the fundamental domain types within your application; for example, the Account and Customer classes in Figure 1. Process classes, such as Interest Applicator in Figure 1, implement complex business logic that pertains to several classes. Persistence classes encapsulate access to your persistent stores (relational databases, flat files, object bases), and system classes encapsulate operating system features such as your approach to interprocess communication. Persistence and system classes, not shown in Figure 1 for the sake of simplicity, are important because they make your system more robust by promoting portability and reusability throughout the systems that your organization develops.

The easiest way to identify subsystems is to deal with user interface classes first, because they are generally implemented on client machines (such as personal computers, HTML browsers, and Java terminals). Although I include the Screen class hierarchy in Figure 3, by relabeling it the Teller Workbench, I generally choose to leave the deployment of the user interface classes until later to reduce the complexity of the problem, and in my experience, this approach works well.

Figure 3: Simplified Collaboration Diagram for a Bank

There are two basic strategies for handling persistence classes, depending on your general approach to persistence. First, systems that use “data classes” to encapsulate hard-coded SQL for simple CRUD (create, read, update, delete) behavior, such as ActiveX data objects (ADOs), should deploy their data classes along with their corresponding business classes. For example, this persistence strategy would create an AccountData class that corresponds to Account, a CustomerData class that corresponds to Customer, and so on. Wherever you choose to deploy Account, you will also automatically deploy AccountData. Second, for systems that take a more robust approach to persistence, perhaps using a persistence framework or persistence layer, will typically deploy this layer on the same node as the persistent store. Organizations using several persistent stores will likely deploy the layer on its own node, which, in turn, accesses the persistent stores. For example, if your organization uses a single instance of Oracle, you will likely put your persistence layer code on the same server as Oracle. An organization that has an Oracle database, a couple of Sybase data marts, and a Versant object base will likely deploy its persistence layer or framework on its own server to provide a single source for persisting objects.

You must think hard about how you intend to deploy system classes because they comprise many critical behaviors. Obviously, you must deploy your interprocess communication classes, components, and frameworks to all nodes within your network. Other system classes, such as your security classes, will likely form a large-scale component that you may choose to deploy on its own node. You can also apply some of the following techniques for refactoring your business/domain classes and system classes to identify potential system components, such as a security manager component.

Step 2: Define class contracts. A contract is any service or behavior of an object that other objects request. In other words, it is an operation that directly responds to a message from other classes. The best way to think about it is that contracts define the external interface, also known as the public interface, of a class. For example, the contracts of the Account class within a banking system likely include operations such as withdraw(), deposit(), open(), and close(). You can ignore operations that aren’t class contracts because they don’t contribute to communication between distributed classes, simplifying your problem dramatically.

Step 3: Simplify hierarchies. For identifying servers, you can often simplify inheritance and aggregation hierarchies. For inheritance hierarchies, a rule of thumb is that if a subclass doesn’t add a new contract, then you can ignore it. In general, you can consider a class hierarchy as a single class. In Figure 3, you see that the Account class hierarchy has been simplified as well as the Screen class hierarchy (now called Teller Workbench). For aggregation hierarchies, you can ignore any “part classes” that are not associated with other classes outside of the aggregation hierarchy. In Figure 3, I couldn’t collapse the area/branch aggregation because account and transaction objects have associations to branch objects. Collapsing aggregation and inheritance hierarchies lets you simplify your model, so it’s easier to analyze when you define subsystems.

Step 4: Identify potential domain components. A domain component is a set of classes that collaborate among themselves to support a cohesive set of contracts that you can consider blackboxes. The basic idea is that classes, and even other domain components, can send messages to domain components to either request information or request that an action be performed. On the outside, domain components appear simple, like any other type of object. On the inside, they are often complex because they encapsulate the behavior of several classes.

You should organize your design into several components in such a way as to reduce the amount of information flowing between them. Any information passed between components, either in the form of messages or the objects that are returned as the result of a message send, represents potential traffic on your network. Because you want to minimize network traffic to reduce your application’s response time, you want to design your domain components so that most of the information flow occurs within the components and not between them.

To determine whether or not a class belongs in a domain component, you need to analyze its collaborations to determine its distribution type. A server class receives messages but doesn’t send them, a client class sends messages but doesn’t receive them, and a client/server class sends and receives messages. In Figure 4, each object’s distribution type is indicated with the label “c,” “s,” or “c/s.”

Figure 4: Analyzing the Collaboration Diagram

Once you identify the distribution type of each class, you can start identifying potential domain components. One rule of thumb is that server classes belong in a domain component and will often form their own domain components because they are the “last stop” for messages flowing within an application. The Transaction class in Figure 3 is a server class, but is a potential domain component in Figure 4. The volume of transactions, potentially millions per day, would probably motivate you to deploy Transaction to its own server (or set of servers).

If you have a domain component that is a server to only one other domain component, you may decide to combine the two components. Or, you can connect the two machines on which the domain components reside via a high-speed private link, eliminating the need to put the one machine on your regular network. In Figure 4, you see an example of this between Transaction and Account, indicating that you may want to go with one single domain component (indicated as box number 3) instead of two separate components. You want to consider other issues such as security (do you want to hide the existence of Transaction?), performance (will having Transaction on its own machine or machines prove to work faster?), and scalability (will you need to support a quickly growing number of transactions in the future?) when choosing between these two alternatives.

A third rule of thumb is that client classes do not belong in a domain component because they only generate but don’t receive messages, whereas the domain component’s purpose is to respond to messages. Therefore, client classes have nothing to add to the functionality a component offers. In Figure 4, Interest Applicator is a pure client class that was not assigned to a potential domain component.

A fourth rule, when two classes collaborate frequently, especially two large objects (either passed as parameters or received as return values), they should be in the same domain component to reduce the network traffic between them. Basically, highly coupled classes belong together.

A related heuristic is that client/server classes belong in a domain component, but you may have to choose which domain component they belong to. This is where you must consider additional issues, such as the information flow going into and out of the class and how the cohesiveness (how well the parts fit together) of each component would be affected by adding a new class. A fundamental design precept is that a class should be part of a domain component only if it exists to fulfill the goals of that domain component.

Finally, you should also refine your assignment of classes to domain components based on the impact of potential changes to them. One way to do this is to develop change cases (“Architecting for Change,” Thinking Objectively, May 1999), which describe likely changes to the requirements that your system implements. If you know that some classes might change, then you probably want to implement change cases in one or two components to limit the scope of the changes when they occur. For example, knowing that your organization structure is likely to change over time should strengthen your resolve to encapsulate Area and Branch in a single component.

Step 5: Define domain-component contracts. Domain-component contracts are the collection of class contracts that are accessed by classes outside of the domain component (not including class contracts that are only used by classes within the subsystem). For example, the contracts for subsystem 3 (Account-Transaction) would be the combination of the class contracts from the Account class hierarchy. Because transaction objects collaborate with only account objects, you can ignore the class contracts of Transaction. The collection of domain-component contracts form a domain component’s public interface. This step is important because it helps to simplify your design, so users only need to understand each component’s public interface to learn how to use it, and to decrease the coupling within your design.

You should consider several rules when defining domain-component contracts. When all of a server class’s contracts are included in the contracts its domain component provides, you should consider making the server class its own domain component, external to the current component. The implication is that when the outside world needs the server class’s entire public interface, encapsulating it within another domain component isn’t going to buy you anything. On the same note, if none of a server class’s contracts are included in the subsystem contract (for example, these contracts are only accessed by classes internal to the subsystem), you should then define the server class as a subsystem internal to the present subsystem. This heuristic would motivate you to consider including Transaction in the Account component; however, for performance and scalability reasons mentioned previously, you might go with the component model presented in Figure 5, which separates them.

Figure 5: A Component Diagram for a Bank

The contracts of a subsystem should be cohesive. If they don’t fit well together, then you probably have multiple subsystems. If your initial contracts for the Account component include openAccount(), makeDeposit(), makeWithdrawal(), and orderLunchForExecutives(), you likely have a problem because the lunch-ordering contract doesn’t fit well with the other ones.

Step 6: Simplify interfaces. Depending on your initial design, you may be able to collapse several contracts into one to reduce the number of different message types sent to a domain component. By reducing the number of contracts for a component, you simplify its interface, making it easier to understand and hopefully use. For example, to reduce the number of contracts for Account, you could collapse makeWithdraw() and makeDeposit() into a single changeBalance() contract whose parameter is either a positive or negative amount.

Grady Booch, Ivar Jacobson, and James Rumbaugh, designers of the UML, suggest you “manage the seams in your system” (The Unified Modeling Language User Guide, Addison-Wesley, 1999) by defining the major interfaces for your components. They mean you should define the component interfaces early in your design, so you can work on the internals of each component without worrying about how it will affect other components—you just have to keep the defined interface stable.

Booch et al. also suggest that components should depend on the interfaces of other components and not on the component itself. In Figure 5, dependencies are drawn from components to the interfaces of other components. Also, notice that the Account component offers two different interfaces, one for the Interest Applicator component (likely a batch job) and one for the Teller Workbench and Account Statement components. This approach lets you restrict access to a component, making your system more secure and robust.

Step 7: Map to physical hardware design. Once you identify the domain components that will comprise your system, you can map them to your physical hardware design. You will do this using the UML’s deployment model, which shows your hardware nodes, the middleware used to connect them, and the components that will be deployed to each node. Figure 6 shows a deployment diagram for one potential configuration for this application.

Figure 6: A Deployment Diagram for a Bank

Mapping domain components to hardware nodes is ideally an iterative process—you identify some candidate components, then identify some candidate hardware nodes, then you map the components to the hardware nodes, and depending on how well this works you modify either your components or your approach to the hardware configuration. You typically must map your domain components to an existing hardware configuration. You may have the option to make some changes, such as upgrading to existing machinery, but you are mostly forced to make do with what you have. In this situation, you must modify the part of your system that you still control—the design of your software.

You must consider many hard-core technical issues at this point. I discussed network performance earlier, particularly, the need to minimize network traffic. This issue now becomes critical. When you are mapping components to hardware nodes, you have partial control over what will be transmitted across your network—if two components have significant traffic between them, you will want to deploy them to the same node or at least to nodes with good connections between them. You must also consider which components can run in parallel, and ideally put those components on different nodes or at least on nodes with multiple processors. Parallel processing brings us to the issue of load balancing, spreading the processing across several hardware nodes, which makes your system configuration more complex but provides better performance.

Some behaviors must run under the scope of an atomic transaction, typically a multi-part action that must either fully succeed or fully fail. An example of this is transferring funds between two bank accounts—the system makes a withdrawal from one account and posts a transaction recording it, the system then deposits those funds into the target account and posts a transaction for that, and finally charges a fee for performing the transfer. There are five separate actions to this behavior, each of which must be performed successfully or not at all. In this case, all instances of Account and Transaction objects are affected, indicating that you may want to deploy these two components to the same hardware node. By having Transaction on a separate machine from Account, you add the risk of that connection or hardware node being unavailable.

Redundancy within your system may also be an important issue for you, improving the quality of service that you provide to your users. Do you need to deploy a single component to several nodes to support failover within your system? Failover means if one node fails, the other node with the same component can take over while the first node is being fixed. Do you have to support disconnected users? Disconnected users work with their application offline, then connect at a later time and replicate their work to the system, and then download any updates made since the last time they connected. If this is the case, then you must deploy the requisite components to these users’ client machines, as well as to your network’s internal nodes.

At this point, you will want to finalize where you deploy your persistence and system classes, as I indicated in step one. You should deploy persistence classes close to the persistent stores—relational databases, flat files, object databases—that they interact with, either on the same nodes or on the nodes of the domain components that need to access them directly. You should deploy system classes across all your system’s nodes to manage basic behaviors, such as security and interprocess communication.

You can’t do most of this step’s work on paper—you must prototype the components, deploy them to a representative hardware configuration, and test them to see how well the configuration works. This is called technical prototyping and it is a critical technique for ensuring that your system design actually performs as required. You should begin technical prototyping as early in design as possible because if your design isn’t going to work you want to discover this as soon as possible so that you can change your solution as required. Everything works on paper, but not always in practice.

Step 8: Add implementation details. You’ve identified potential subsystems within your design and then simplified them. So what? You’ve drawn some bigger bubbles around some smaller bubbles. It’s obvious you can’t deploy bubbles, you must add something to your software to implement those bubbles. To implement domain components you need to define router classes, also known as facade classes (Design Patterns by Erich Gamma et al., Addison-Wesley Professional Computing, 1995), that accept incoming messages sent to the server and pass them to the appropriate classes. A router class encapsulates the server’s functionality, implementing the operations for the domain-component contracts and providing an interface that other components can interact through.

A key implementation issue is whether or not your team will follow industry or organization-accepted standards and guidelines to develop your software. The Internet is successful because it was built by people following industry-accepted standards, such as HTTP and POP. Had individual development teams decided to invent “better” approaches, the Internet would never have gotten off the ground. The point is that you don’t need to reinvent the wheel, you can often purchase or obtain for free significant portions of your work.

Step 9: Distribute the user interface. The final step is to distribute your user interface appropriately, the basic goal is to deliver the appropriate screens and reports to the people who need them. You do this by looking at the actors in your use case model and seeing which use cases they interact with. You may decide to develop a user interface component that encapsulates the appropriate user interface classes, such as the Teller Workbench in Figure 5, for each actor. In the case of the bank, you would likely need to develop a user interface component that implements the user interface provided at an ATM machine and perhaps one available to senior management for operational reporting. The result of this strategy is that you will develop what appears to be several systems, the teller workbench application, the ATM application, and the management reporting application, by reusing the same domain components for each.

I’ve presented an approach to creating a distributed object design that starts with your non-distributed object design, analyzes it, and reworks it to be distributed. You could just as easily take an architectural driven approach where you first develop an enterprise use-case model, identify candidate classes, assign them to candidate domain components, and then flesh out the domain components via detailed modeling. I presented a bottom-up approach to distributed object design, but you can take a top-down, architectural approach.

An important lesson that you should learn is that distributed object design is doable, you just need to take several new design considerations into account. You can design robust, distributed software for mission-critical applications: the largest system in the world is the Internet, and it’s highly distributed.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.