Chapter 5: Layered Application Guidelines
For more details of the topics covered in this guide, see Contents of the Guide.
This chapter discusses the overall structure for applications in terms of the logical grouping of components into separate layers that communicate with each other and with other clients and applications. Layers are concerned with the logical division of components and functionality, and do not take into account the physical location of components. Layers can be located on different tiers, or they may reside on the same tier. In this chapter, you will learn how to divide your applications into separate logical parts, how to choose an appropriate functional layout for your applications, and how applications can support multiple client types. You will also learn about services that you can use to expose logic in your layers.
|It is important to understand the distinction between layers and tiers. Layers describe the logical groupings of the functionality and components in an application; whereas tiers describe the physical distribution of the functionality and components on separate servers, computers, networks, or remote locations. Although both layers and tiers use the same set of names (presentation, business, services, and data), remember that only tiers imply a physical separation. It is quite common to locate more than one layer on the same physical machine (the same tier). You can think of the term tier as referring to physical distribution patterns such as two-tier, three-tier, and n-tier. For more information about physical tiers and deployment, see Chapter 19 "Physical Tiers and Deployment."|
Logical Layered Design
Irrespective of the type of application that you are designing, and whether it has a user interface or it is a services application that only exposes services (not to be confused with services layer of an application), you can decompose the design into logical groupings of software components. These logical groupings are called layers. Layers help to differentiate between the different kinds of tasks performed by the components, making it easier to create a design that supports reusability of components. Each logical layer contains a number of discrete component types grouped into sub layers, with each sub layer performing a specific type of task.
By identifying the generic types of components that exist in most solutions, you can construct a meaningful map of an application or service, and then use this map as a blueprint for your design. Dividing an application into separate layers that have distinct roles and functionalities helps you to maximize maintainability of the code, optimize the way that the application works when deployed in different ways, and provides a clear delineation between locations where certain technology or design decisions must be made.
Presentation, Business, and Data Layers
At the highest and most abstract level, the logical architecture view of any system can be considered as a set of cooperating components grouped into layers. Figure 1 shows a simplified, high level representation of these layers and their relationships with users, other applications that call services implemented within the application’s business layer, data sources such as relational databases or Web services that provide access to data, and external or remote services that are consumed by the application.
These layers may be located on the same physical tier, or may be located on separate tiers. If they are located on separate tiers, or separated by physical boundaries, your design must accommodate this. For more information, see Design Steps for a Layered Structure later in this chapter.
As shown in Figure 1, an application can consist of a number of basic layers. The common three-layer design shown in Figure 1 consists of the following layers :
- Presentation layer. This layer contains the user oriented functionality responsible for managing user interaction with the system, and generally consists of components that provide a common bridge into the core business logic encapsulated in the business layer. For more information about designing the presentation layer, see Chapter 6 "Presentation Layer Guidelines." For more information about designing presentation components, see Chapter 11 "Designing Presentation Components."
- Business layer. This layer implements the core functionality of the system, and encapsulates the relevant business logic. It generally consists of components, some of which may expose service interfaces that other callers can use. For more information about designing the business layer, see Chapter 7 "Business Layer Guidelines." For more information about designing business components, see Chapter 12 "Designing Business Components."
- Data layer. This layer provides access to data hosted within the boundaries of the system, and data exposed by other networked systems; perhaps accessed through services. The data layer exposes generic interfaces that the components in the business layer can consume. For more information about designing the data layer, see Chapter 8 "Data Layer Guidelines." For more information about designing data components, see Chapter 15 "Designing Data Components."
Services and Layers
From a high level perspective, a service-based solution can be seen as being composed of multiple services, each communicating with the others by passing messages. Conceptually, the services can be seen as components of the overall solution. However, internally, each service is made up of software components, just like any other application, and these components can be logically grouped into presentation, business, and data layers. Other applications can make use of the services without being aware of the way they are implemented. The layered design principles discussed in the previous section apply equally to service-based solutions.
When an application must provide services to other applications, as well as implementing features to support clients directly, a common approach is to use a services layer that exposes the business functionality of the application, as shown in Figure 2. The services layer effectively provides an alternative view that allows clients to use a different channel to access the application.
In this scenario, users can access the application through the presentation layer, which communicates either directly with the components in the business layer; or through an application façade in the business layer if the communication methods require composition of functionality. Meanwhile, external clients and other systems can access the application and make use of its functionality by communicating with the business layer through service interfaces. This allows the application to better support multiple client types, and promotes re-use and higher level composition of functionality across applications.
In some cases, the presentation layer may communicate with the business layer through the services layer. However, this is not an absolute requirement. If the physical deployment of the application locates the presentation layer and the business layer on the same tier, they may communicate directly. For more information about designing the services layer, see Chapter 9 "Service Layer Guidelines." For more information about communication between layers, see Chapter 18 "Communication and Messaging."
Design Steps for a Layered Structure
When starting to design an application, your first task is to focus on the highest level of abstraction and start by grouping functionality into layers. Next, you must define the public interface for each layer, which depends on the type of application you are designing. Once you have defined the layers and interfaces, you must determine how the application will be deployed. Finally, you choose the communication protocols to use for interaction between the layers and tiers of the application. Although your structure and interfaces may evolve over time, especially if you use agile development, these steps will ensure that you consider the important aspects at the start of the process. A typical series of design steps is the following:
- Step 1 – Choose Your Layering Strategy
- Step 2 – Determine the Layers You Require
- Step 3 – Decide How to Distribute Layers and Components
- Step 4 – Determine If You Need to Collapse Layers
- Step 5 – Determine Rules for Interaction between Layers
- Step 6 – Identify Cross Cutting Concerns
- Step 7 – Define the Interfaces between Layers
- Step 8 – Choose Your Deployment Strategy
- Step 9 – Choose Communication Protocols
Step 1 – Choose Your Layering Strategy
Layering represents the logical separation of an application’s components into groups that represent distinct roles and functionality. Using a layered approach can improve the maintainability of your application and make it easier to scale out when necessary to improve performance. There are many different ways to group related functionality into layers. However, separating an application into too few or too many layers can add unnecessary complexity; and can decrease the overall performance, maintainability, and flexibility. Determining the granularity of layering appropriate for your application is a critical first step in determining your layering strategy.
You must also consider whether you are implementing layering in order to achieve purely logical separation of functionality, or in order to potentially achieve physical separation as well. Crossing layer boundaries imposes a local performance overhead, especially for boundaries between physically remote components. However, the overall increase in the scalability and flexibility of your application can far outweigh this performance overhead. In addition, layering can make it easier to optimize the performance of individual layers without affecting adjacent layers.
In the case of logical layering, interacting application layers will be deployed on the same tier and operate within the same process, which allows you to take advantage of higher performance communication mechanisms such as direct calls through component interfaces. However, in order to maintain the advantages of logical layering and ensure flexibility for the future, you must be careful to maintain encapsulation and loose coupling between the layers.
For layers that are deployed to separate tiers (separate physical machines), communication with adjacent layers will occur over a connecting network, and you must ensure that the design you choose supports a suitable communication mechanism that takes account of communication latency and maintains loose coupling between layers.
Determining which of your application layers are likely to be deployed to separate tiers, and which are likely to be deployed to the same tier, is also an important part of your layering strategy. To maintain flexibility, always ensure that interaction between layers is loosely coupled. This allows you to take advantage of the higher performance available when layers are located on the same tier, while allowing you to deploy them to multiple tiers if and when required.
Adopting a layered approach can add some complexity, and may increase initial development time, but if implemented correctly will significantly improve the maintainability, extensibility, and flexibility of your application. You must consider the trade off of reusability and loose coupling that layers provide against their impact on performance and the increase in complexity. Carefully considering how your application is layered, and how the layers will interact with each other, will ensure a good balance between performance and flexibility. In general, the gain in flexibility and maintainability provided by a layered design far outweighs the marginal improvement in performance that you might gain from a closely coupled design that does not use layers.
For a description of the common types of layers, and guidance on deciding which layers you need, see the section "Logical Layered Design" earlier in this chapter.
Step 2 – Determine the Layers You Require
There are many different ways to group related functionality into layers. The most common approach in business applications is to separate presentation, services, business, and data access functionality into separate layers. Some applications also introduce reporting, management, or infrastructure layers.
Be careful when adding additional layers, and do not add them if they do not provide a logical grouping of related components that manifestly increases the maintainability, scalability, or flexibility of your application. For example, if your application does not expose services, a separate service layer may not be required and you may just have presentation, business, and data access layers.
Step 3 – Decide How to Distribute Layers and Components
You should distribute layers and components across separate physical tiers only where this is necessary. Common reasons for implementing distributed deployment include security policies, physical constraints, shared business logic, and scalability.
- In Web applications, if your presentation components access your business components synchronously, consider deploying the business layer and presentation layer components on the same physical tier to maximize performance and ease operational management, unless security restrictions require a trust boundary between them.
- In rich client applications, where the UI processing occurs on the desktop, you may prefer to deploy the business components in a physically separate business tier for security reasons, and to ease operational management.
- Deploy business entities on the same physical tier as the code that uses them. This may mean deploying them in more than one place; for example, placing copies on a physically separated presentation tier or data tier where that logic makes use of or references the business entities. Deploy service agent components on the same tier as the code that calls the components, unless security restrictions require a trust boundary between them.
- Consider deploying asynchronous business components, workflow components, and services that have similar load and I/O characteristics on a separate physical tier so that you can fine tune that infrastructure to maximize performance and scalability.
Step 4 – Determine If You Need to Collapse Layers
In some cases, it makes sense to collapse or relax layers. For example, an application with very limited business rules, or one that uses rules mainly for validation, might implement both the business and presentation logic in a single layer. In an application that pulls data from a Web service and displays that data, it may make sense to simply add a Web service references directly to the presentation layer and consume the Web service data directly. In this case, you are logically combining the data access and presentation layers.
These are just some examples of where it might make sense to collapse layers. However, the general rule is that you should always group functionality into layers. In some cases, one layer may act as a proxy or pass-through layer that provides encapsulation and loose coupling without providing a great deal of functionality. However, by separating that functionality, you can extend it later with little or no impact on other layers in the design.
Step 5 – Determine Rules for Interaction Between Layers
When it comes to a layering strategy, you must define rules for how the layers will interact with each other. The main reasons for specifying interaction rules are to minimize dependencies and eliminate circular references. For example, if two layers each have a dependency on components in the other layer you have introduced a circular dependency. As a result, a common rule to follow is to allow only one way interaction between the layers using one of the following approaches:
- Top-down interaction. Higher level layers can interact with layers below, but a lower level layer should never interact with layers above. This rule will help you to avoid circular dependencies between layers. You can use events to make components in higher layers aware of changes in lower layers without introducing dependencies.
- Strict interaction. Each layer must interact with only the layer directly below. This rule will enforce strict separation of concerns where each layer knows only about the layer directly below. The benefit of this rule is that modifications to the interface of the layer will only affect the layer directly above. Consider using this approach if you are designing an application that will be modified over time to introduce new functionality and you want to minimize the impact of those changes, or you are designing an application that may be distributed across different physical tiers.
- Loose interaction. Higher level layers can bypass layers to interact with lower level layers directly. This can improve performance, but will also increase dependencies. In other words, modification to a lower level layer can affect multiple layers above. Consider using this approach if you are designing an application that you know will not be distributed across physical tiers (for example, a stand-alone rich client application), or you are designing a small application where changes that affect multiple layers can be managed with minimal effort.
Step 6 – Identify Cross Cutting Concerns
After you define the layers, you must identify the functionality that spans layers. This functionality is often described as crosscutting concerns, and includes logging, caching, validation, authentication, and exception management. It is important to identify each of the crosscutting concerns in your application, and design separate components to manage these concerns where possible. This approach helps you to achieve of better reusability and maintainability.
Avoid mixing the crosscutting code with code in the components of each layer, so that the layers and their components only make calls to the crosscutting components when they must carry out an action such as logging, caching, or authentication. As the functionality must be available across layers, you must deploy crosscutting components in such a way that they are accessible to all the layers—even when the layers are located on separate physical tiers.
There are different approaches to handling crosscutting functionality, from common libraries such as the patterns & practices Enterprise Library to Aspect Oriented Programming (AOP) methods where metadata is used to insert crosscutting code directly into the compiled output. For more information about crosscutting concerns, see Chapter 17 "Crosscutting Concerns."
Step 7 – Define the Interfaces between Layers
When you define the interface for a layer, the primary goal is to enforce loose coupling between layers. What this means is that a layer should not expose internal details on which another layer could depend. Instead, the interface to a layer should be designed to minimize dependencies by providing a public interface that hides details of the components within the layer. This hiding is called abstraction, and there are many different ways to implement it. The following design approaches can be used to define the interface to a layer:
- Abstract interface. This can be accomplished by defining an abstract base class or code interface class that acts as a type definition for concrete classes. The type defines a common interface that all consumers of the layer use to interact with the layer. This approach also improves testability, because you can use test objects (sometimes referred to as mock objects) that implement the abstract interface.
- Common design type. Many design patterns define concrete object types that represent an interface into different layers. These object types provide an abstraction that hides details related to the layer. For example, the Table Data Gateway pattern defines object types that represent tables in a database and are responsible for implementing the SQL queries required to interact with the data. Consumers of the object have no knowledge of the SQL queries, or the details of how the object connects to the database and executes commands. Many design patterns are based on abstract interfaces but some are based on concrete classes instead, and most of the appropriate patterns such as Table Data Gateway are well documented in this respect. Consider using common design types if you want a fast and easy way to implement the interface to your layer, or if you are implementing a design pattern for the interface to your layer.
- Dependency inversion. This is a programming style where abstract interfaces are defined external to, or independent of, any layers. Instead of one layer being dependent on another, both layers depend upon common interfaces. The Dependency Injection pattern is a common implementation of dependency inversion. With dependency injection, a container defines mappings that specify how to locate components that another component may depend upon, and the container can create and inject these dependent components automatically. The dependency inversion approach provides flexibility and can help to implement a pluggable design because the dependencies are composed through configuration rather than code. It also maximizes testability because you can easily inject concrete test classes into different layers of the design.
- Message-based. Instead of interacting directly with components in other layers by calling methods or accessing properties of these objects, you can use message-based communication to implement interfaces and provide interaction between layers. There are several messaging solutions such as Windows Communication Foundation, Web services, and Microsoft Message Queuing that support interaction across physical and process boundaries. However, you can also combine abstract interfaces with a common message type used to define data structures for the interaction. The key difference with a message-based interface is that the interaction between layers uses a common structure that encapsulates all the details of the interaction. This structure can define operations, data schemas, fault contracts, security information, and many other structures related to communication between layers. Consider using a message-based approach if you are implementing a Web application and defining the interface between the presentation layer and business layer, you have an application layer that must support multiple client types, or you want to support interaction across physical and process boundaries. Also, consider a message-based approach if you want to formalize the interaction with a common structure, or you want to interact with a stateless interface where state information is carried with the message.
To implement the interaction between the presentation layer of a Web application and the business logic layer, the recommendation is to use a message-based interface. If the business layer does not maintain state between calls (in other words, each call between the presentation layer and business layer represents a new context), you can pass context information along with the request and provide a common model for exception and error handling in the presentation layer.
Step 8 – Choose Your Deployment Strategy
There are several common patterns that represent application deployment structures found in most solutions. When it comes to determining the best deployment solution for your application, it helps to first identify the common patterns. Once you have a good understanding of the different patterns, you then consider scenarios, requirements, and security constraints to choose the most appropriate pattern or patterns. For more information on deployment patterns, see Chapter 19 "Physical Tiers and Deployment."
Step 9 – Choose Communication Protocols
The physical protocols used for communication across layers or tiers in your design play a major role in the performance, security, and reliability of the application. The choice of communication protocol is even more important when considering distributed deployment. When components are located on the same physical tier, you can often rely on direct communication between these components. However, if you deploy components and layers on physically separate servers and client machines—as is likely in most scenarios—you must consider how the components in these layers will communicate with each other efficiently and reliably. For more information on communication protocols and technologies, see Chapter 18 "Communication and Messaging."