After the requirements analysis is complete, the design phase can begin. You do not have to wait until all the requirements have been documented before beginning the design phase. During design, you will identify areas where the requirements are incomplete and where more information is needed. Requirements, design, and implementation should be done in small increments to create the service. In each phase, new information will require a change to previous phases. During the design phase for an application,
The services that make up the application must be designed.
The design of the services in the service layer must support QoS requirements identified during requirements analysis.
The design must also support the functional requirements that were identified.
The design must support the feature list and use cases.
The design must map to the business architecture and the conceptual service model.
Existing services that can be reused must be identified.
Organizations that set up a services product line establish a set of core assets for building services. For purposes of discussion, we group those assets into three distinct architectural viewpoints, shown in Figure 3.5: application architecture, technical architecture, and process architecture. Each architectural viewpoint consists of artifacts that address that particular concern.
Application architecture for the component-based services layer consists of the artifacts that guide and provide the vision for the design and implementation of services. Some of these artifacts include conceptual architecture, frameworks and libraries, baseline architecture, developer guides, and blueprints. These artifacts determine the software architecture of the services in the service layer. The conceptual architecture provides a description and diagrams of the various layers within a component-based service and how those layers collaborate to provide a functioning service.
When an organization sets out to develop a services layer for applications, it usually creates a set of frameworks to help developers create services. These frameworks implement the conceptual architecture. The frameworks give the service developer plug points to provide functional code in each layer of the service. They also dictate the way messages pass between layers. The framework provides additional features that separate the functional developer from many platform details, so a developer who writes functional code does not need to worry about too many technical details of the execution environment. A good services framework will let the functional service developer focus on implementing the solution to the business problem rather than creating solutions to technical problems.
Once the framework is complete, the architect selects a set of "architecturally significant" use cases to implement the baseline architecture. The use cases should be sufficient to eliminate technical risks from the project. They should be selected to demonstrate that the nonfunctional requirements could be met. In most cases, only a few use cases will be selected, and the baseline architecture will fully implement them. They should demonstrate a thread through all layers and tiers of the architecture and provide enough of a demonstration of the architecture to permit successful development of the system.
In addition to conceptual architecture, frameworks, and architecture baseline, the architecture should also include detailed developer documentation. This documentation gives functional developers the information they need to implement the rest of the use cases for the component-based services. Also, in a typical product line, a set of blueprints should be developed that contains a sample application for developing services. These blueprints show the best practices for using the frameworks to develop services using the product line assets provided.
Layered Architecture A conceptual architecture for a services layer is a model that depicts a layered architectural style. Each layer in a layered architectural style is a package of software that has a well-defined interface and a few well-known dependencies with other layers. Each layer implements one technical function within the service. For instance, a data access layer is responsible for encapsulating the technical means for accessing a database. All data access requests to a database go through the data access layer for that database. The data access layer has the responsibility of hiding the data access mechanism from upstream layers.
In a closed layer system, a layer may only access adjacent layers. In an open layer system, layers may access any other layer in the system, not just the ones they are adjacent to. We recommend the open layered architectural style for J2EE service development. This gives developers the ability to interchange layers, which greatly improves the service's maintainability. If one layer changes, and the new layer supports the same interface contract as the one it is replacing, adjacent layers don't necessarily need to change. The layered architectural style also facilitates parallelism in development. Once the contracts have been defined for each layer, multiple development teams can implement each layer independently. As long as the contract for each layer is not broken, an integration task at the end of the project is all that is necessary to complete development. The conceptual architecture for a typical component-based service consists of six layers, shown in Figure 3.6.
Proxy Layer The proxy layer follows the classic proxy design pattern from the Gang of Four book (Gamma et al. 1994). The J2EE patterns book (Alur, Crupi, and Malks 2001) calls this layer the "Business Delegate." The proxy layer consists of local objects that the consumer of the service uses to access the service. The proxy abstracts all the details of looking up the service and invoking the remote method. It provides a strongly typed interface for the service client, which will not allow the client to send incorrectly typed data. If a service requires an integer and a string, the proxy will ensure that the client uses those data types.
The client uses local native method invocation on a local object. For instance, a bank account service proxy will have a method called debit account with a bank account number String and a BigDecimal amount argument. The proxy looks up the URL of the bank account service in a registry, translates the arguments into a SOAP XML message, and posts it to the service. The service proxy handles all the complexities of registry lookup-SOAP, XML, and URL connections. The service developer creates proxies for any client who wishes to use the service. For instance, if a Java client, Visual Basic client, and Perl script all need to access the service, the developer provides a proxy for each one, to make accessing the service as easy as possible.
An alternative to creating a different proxy for each service is to use a dynamic proxy. A dynamic proxy uses a WSDL contract specification to dynamically provide strongly typed methods that match the WSDL specification. It uses reflection to create methods that match the specification in the WSDL document. This technique allows the developer and consumer to use a single proxy for all available services.
Service Façade The service façade receives the service request, transforms it into a native method call, and invokes the proper method on the component. It adapts the component for use in a service-based environment. For instance, JAX-RPC is itself a service façade layer. It accepts a SOAP message and calls a stateless session bean with the arguments from the SOAP message. It also formats the results from the call to the stateless session bean into SOAP and returns it to the proxy. The service façade understands the protocol and data format of the services environment in a Web services environment; this is typically SOAP and HTTP. The service may have multiple service façades for each protocol type the service supports. For instance, a service might use both JAX-RPC to expose services via HTTP as well as a message-driven bean that accepts a SOAP message over JMS. The service façade layer may also translate the SOAP message into a ValueObject passed to the session façade for execution.
Session Façade The session façade layer implements the distributed interface for the component. For instance, a stateless session bean that executes the logic for a bank account service has a session façade. It supports Remote Method Invocation (RMI) for the component interface. The session façade does not implement the business logic itself. Instead, the session façade dispatches the request to the business logic layer for execution. The session façade understands the native protocol and data format for the component.
Business Logic The business logic layer contains objects that execute the business functions. The Command pattern should be considered to implement these objects. With the Command pattern, each use case in the requirements document is implemented as a separate command or set of commands executed in the business logic layer. Each command object implements a command interface. The command interface has a simple execute (ValueObject) method. A value object contains just data, no business logic, and is meant to transfer data from one tier or layer to another. It has simple attributes and getters and setters. It is built from the SOAP message passed in to the session façade layer.
Each command object has business logic in its execute method. The value object argument for the execute method contains the request data required for executing the use case command. The typical execute method performs functions such as accessing the data access layer, executing the business logic, and returning a value object that contains the results of the use case to the session façade.
Figure 3.7 is a typical UML diagram for a session façade and business logic layer. Here, the business logic commands are executed by the session façade, which implements a CommandInvoker interface. Command invokers are responsible for creating the correct command, executing it, and returning the results to the upstream layer.
Data Access Layer The business logic layer commands do not access resource systems directly. They do so via the data access layer, which contains data access objects. Each data access object wraps a single data resource. The object is responsible for encapsulating the data access mechanism from the resource's clients. The objects map their interfaces directly to a system, but they do not expose any of the details of the data access mechanism to their clients.
For example, a savings account database has a corresponding savings-account data-access object that wraps the database with a set of methods for manipulating the database, such as update account, add account, and delete account.
The data access object can be used by a service's business logic or any data access client that needs to access the resource. For instance, if time is at a premium and it is not possible to create a full-scale checking account service but a checking account database exists, a checking-account-database data-access object could be created. A servlet could use the checking-account-database data-access object directly to manipulate checking account data. When the schedule permits, this design could be refactored to provide a full-fledged checking account service that uses the same checking-account-database data-access object directly.
The data access object can also implement the Command pattern, and ifso, it also implements the CommandInvoker interface. The object creates and executes a data access command to fulfill the requests of the business logic layer.
When the business logic needs to access a data resource, it sends a ValueObject request to the data access object that represents the function it needs to access. The data access object finds the command that will execute the request, executes the command, and returns the results as a ValueObject.
By splitting up the responsibilities of business logic and data access, each layer can vary independently. For instance, if a system moves from an Oracle database on a mid-tier system to DB2 on a host system, the only layer that must change is the data access layer, not the business logic.
Resource Layer The resource layer is the "system of record" for the data. It might be a database accessed via JDBC and entity beans. It might be a legacy mainframe system accessed via MQSeries or a packaged application such as Seibel or Peoplesoft. The data access objects encapsulate the resource, so if the implementation of the resource changes, only the data access layer must change, not the business logic, session façades, or service façades, and especially not the service's clients.
The technical architecture for the services layer is the runtime environment for service execution. It is the J2EE application server, the database servers, the directory servers and any other machines, networks, and operating systems that enable the service layer to run. The technical architecture impacts the application architecture in terms of the versions and configuration of the servers. For instance, if clustering is used, that has an impact on the way the frameworks must use the technical environment. The frameworks can't hold on to bean references, because the reference may switch from one server to another if the server goes down. The software versions also have an impact on application architecture. The frameworks and libraries will use a specific version of the J2EE environment. If the application servers are upgraded, then so must the frameworks that run on them.
Although application architecture has a major impact on the modifiability, reusability, integrability, and testability of the services layer, the technical architecture in large part impacts the performance, security, availability, reliability, and portability of services. Performance, availability, and reliability are to a large extent a result of the technical architecture's scalability. A robust technical architecture makes it easy to add application and database server nodes to a cluster and hubs to the network. If a node fails, the ability to dynamically move the processes on that node to another node is essential to the service layer's reliability. If the technical architecture does not perform well and batch cycles take hours, this will degrade the availability of the system. If processes cannot be allocated dynamically to multiple nodes for execution, performance will be impacted.
The application architecture and technical architecture must have a cohesive design to fulfill all the QoS requirements for the service built on the architecture. As shown in Figure 3.8, there are two classes of services: functional services and technical services.
Technical Services A technical service is a full-fledged service which, when coupled with a vendor platform, makes up the full execution environment for services. Common services include those for logging, transformation, configuration data, transaction management, reference data, and so on. These common services are used by other services and applications to perform specific technical functions. Although a business service might implement the functions of a bank account, a technical service implements functions around a technical problem domain.
Technical services use all the same assets of the services product line. They are built using the same frameworks, libraries, and platforms as business services. The technical services and frameworks also serve as "separation layers" between the business services and specific aspects of the platform. For instance, a logging service publishes an interface used by business services. The interface supports the requirements of the business services and has no relation to the particular technology used to implement the logging service. Internally, the logging service might use Log4J to log messages. Externally, this is not known by any of the business services. If the implementation of the logging service changes, as long as it supports the same interface contract, none of the services needs to know about the change.
Technical services are accessed like any other service across a network. However, it is possible, for performance reasons, to implement all of the technical service in the proxy. Therefore, a logging service proxy could log to a local file. If a more robust implementation is necessary, the file could be swept periodically and dumped into a database for reporting. If performance becomes less of an issue and real-time logging is necessary, the proxy could be exchanged for one that performs real-time logging to a logging service. In each case, the interface to the business service remains the same.
To make this design even more flexible, the proxy could be retrieved from a proxy factory. The proxy factory is configured in the runtime environment to return a proxy of a particular type that implements the same logging proxy interface. The runtime configuration for the service might one day return a proxy that implements a file-based scheme and the next day return a proxy that logs across the network. This can be accomplished without changing any code, only a configuration file.
Entire books are available that describe the processes by which applications and architectures are built. Although we won't go into detail for any one process, it is important to note the distinction between creating and maintaining the business services themselves and creating and maintaining the architecture for the services. Building services is one part of building an application. The steps necessary to gather requirements for, design, code, and maintain a service are similar to those of any application. Processes such as the Rational Unified Process and OPEN and more agile methods such as XP and SCRUM are appropriate for the development of services. However, architecture has a different lifecycle and thus requires a different process. The product line process provides a means of developing and maintaining core assets for services.
Product Line Process The architecture for service design and execution has a more iterative lifecycle and therefore requires a different process. The product line process from the Software Engineering Institute is a method for maintaining core assets for service development. The development of a functional service has a discernible inception-through-deployment lifecycle. When developing an architecture, the tasks are much more iterative and do not have a discernible endpoint. Architectures need to be carefully maintained to minimize the impact on existing services while still taking advantage of updates to vendor platforms and new design techniques.
Core assets-machines, licenses, frameworks, libraries, and so on-are harvested from an organization's inventory of assets. The gaps that remain in the platform must be filled in. For instance, if there is no solution for logging, one must be built. Or if there is a solution but it does not perfectly fit the needs of the service platform, it must be adapted.
Technical and business changes drive iterations of the architecture. An event such as a platform upgrade will cause an update to the architecture in terms of frameworks and libraries. A new business process not supported by the platform will necessitate a change to the architecture. A new quality requirement, such as 24/7 availability, will require an update. The service platform is a product line in which all elements of the service share the same core assets. This improves return on investment. However, any business or technical change to the architecture affects all services built on that product line. Because of this risk and the potential to disrupt a large number of services, it is important to have a sound process to manage this change.
Designing interfaces for services is critical to successful service development. The service supports a distinct business function, a clear and autonomous business concept. For instance, services such as a credit-card validation service, savings account service, and stock ticker service all support this concept. The modularity and interface definition of a modular service was discussed in the previous chapter. The process of defining the interface and attributes of the interfaces that must be captured is best addressed through the principles of the "design by contract" method from Bertrand Meyer (1997) and expanded upon by Richard Mitchell and Jim McKim (2001).
Design by Contract The design by contract method of interface contract design was developed for designing the interfaces of classes, but the principles fit service interface design equally well, although with a different slant. Design by contract for interface design is based on six principles:
Separate basic queries from derived queries. Derived queries can be specified in terms of basic queries. A derived query is a query that uses other queries to obtain an ultimate answer. A basic query goes directly after the data source to get the data. Consider a bank account service that supports all the account activity for a bank, including checking and savings accounts. If a client needs to get the balance of the account, it calls the GetBalance service. This method inquires about the account holder's checking and savings account balances, sums them, and returns the result. If all the client needs is the total balance, there would never be a problem. If, however, a client needs just the checking account balance, the service will not fulfill this need. Therefore, it is necessary to separate the "basic" queries of GetBalance for savings and checking accounts from the composite derived query for both accounts. A correct design would consist of three service interfaces: GetBalance on a checking account service, on a savings account service, and on a composite BankAccount service. The composite derived service method would use the basic queries to derive the data on the client's behalf.
For each command, write a postcondition that specifies the value of every basic query. A postcondition specifies the effect of calling a feature of the service. For instance, a Withdraw command on a checking account service will guarantee that if the method succeeds, the GetBalance query will return the previous balance minus the withdrawal amount.
For every query command, decide on a suitable precondition. For methods that require multiple steps, specify the preconditions necessary for the multiple steps to succeed. For instance, consider a credit-card validation sequence where the first step is a validation that the cardholder has not exceeded his or her credit limit for the transaction. The second method actually confirms the transaction. The precondition for the confirm transaction has with it a precondition that the validate transaction has already occurred.
For each derived query, write a postcondition that specifies what result will be returned in terms of one or more basic queries. If we know the values of the basic queries, we will know the values of the derived queries. For instance, in the previous example, the postcondition for the GetBalance method on the derived BankAccount service specifies that the result is the sum of the GetBalance method on the checking account service and that on the savings account service.
Separate queries from commands. The first principle of writing interface contracts is to separate queries from commands. A query inspects the state of the service, and a command updates the state of a service. For example, consider a bank account service. A GetBalance query retrieves the state of the bank account service. The command Withdraw updates the state of the balance in the account. A bad design would return the new balance in the account when Withdraw was called. This would combine the query with the command. The reason this is undesirable is that a user will be tempted to call the Withdraw method to query the balance of the account and thus inadvertently cause the state of the service to change. The Withdraw method should return only the result of the service call, such as "OK" or "Fail."
Write invariants to define unchanging properties of objects. For instance, the account balance in the savings account service can never be less than zero. Having these invariants specified somewhere helps define all possible states of the service, so proper error handling can be built.
When implementing a service, each service method must be subtransactional, which means that it should not perform a commit on the data. One of the benefits of services is their composability, their ability to be assembled into a new composite service. As indicated in Chapter 2, this is sometimes called service "orchestration."
Consider our checking-account service example. Suppose a customer wanted to pay a credit card bill directly from his or her checking account. This requires two methods to execute: a "pay credit card" transaction and a "debit checking account" transaction. If either transaction fails, the other must be rolled back. If either is written such that as soon as it occurs, the transaction is committed, the transaction cannot be rolled back if the other transaction fails.
Sometimes it is not possible to make a method subtransactional. This is true where a service wraps a legacy system. Sometimes, the technology used to access the resource does not have transactional capabilities. In this case, the service should have "compensating" transactions, which reverse the effect of a transaction. For instance, on a checking account service, a deposit request would have a compensating undoDeposit request. The logical compensating transaction for deposit would be withdrawal. However, a withdraw request would be a logged request on the customer's account. An undoDeposit request would not log the transaction on the customer's account. In addition, the undoDeposit request would probably need the transaction ID for a previous deposit request.