-
Notifications
You must be signed in to change notification settings - Fork 129
Layered Architecture
This document describes a concrete architecture for .NET / client applications. This architecture doesn't claim to be the preferred solution in every scenario, but you might find the one or other part described here useful for your own software systems.
Layering is a very useful tool for structuring object-oriented software systems. It helps to organize the types and namespaces into a large-scale structure. The layered architecture defines some rules to ensure that the structuring is done right. One of the rules is that the "lower" layers are low-level and generic services, and the "higher" layers are more application specific. This allows you to separate the software artifacts with related responsibilities together into discrete layers. Another rule is that "higher" layers can call types of "lower" layers, but not vice versa. This rule helps to reduce the coupling and dependencies between the types and so increase the reusability, testability, and clarity of the software system.
The layering scheme shown in this article (Figure 1) is based on the one presented in Larman's book Applying UML and Patterns [Larman04].
Figure 1: Layers in a .NET based software system.
1. General vs. Application Specific Types
Separate the types into the different layers. Put the low-level and generic service types in "lower" layers, and the more application specific types in "higher" layers. This rule is shown by the "more app specific" arrow in Figure 1.
2. Dependencies
The characteristic of a layered architecture is that "higher" layers can call services from "lower" layers, but not vice versa. This way we avoid circular references between types of different layers and we reduce the number of dependencies. This guideline can be seen by the "dependency" arrow in Figure 1.
3. Applicability
One result of the first and second guidelines is that "lower" layers are more reusable than "higher" layers. Figure 1 shows this through the width of the UML packages and the "Width implies range of applicability" arrow.
4. Relaxed Layering
This is also known as layer skipping. Relaxed layering means that layers are allowed to call layers deeper than the layer directly below them. For example, a view in the Presentation layer can bind a text box to a property of a business class from the Domain layer.
5. White-box reuse
A class is allowed to inherit from a class defined in a lower layer. The same is true for implementing interfaces. However, white-box reuse tightens the coupling between the layers and should be avoided whenever possible. One of the object-oriented design principles in the book Design Pattern [GHJV95] is "Favor object composition over class inheritance".
6. Namespaces
The layer name can be a part of the namespace. A common naming scheme for namespaces is:
Schema: [Company].[Product].[Layer].[Subsystem]
Example: Microsoft.Word.Presentation.Ribbon, Microsoft.Outlook.Domain.AddressBook
Tip 1:
Avoid using the name Application inside a namespace because .NET comes with a class named Application
. Therefore, I use the plural name Applications
inside my namespaces to avoid naming conflicts.
Tip 2:
Avoid assemblies that contain types from different layers. If each assembly is associated with only one layer, you have the advantage that assembly references don't allow cyclic dependencies. This way the compiler helps you to ensure that all the dependencies are from "higher" to "lower" layers. Also, the internal access modifier restricts the access to types inside the same layer. Avoid accessing internal
types or members across layer boundaries.
This layer is responsible for the user experience and the system integration of the application. It is the highest layer in this schema.
It may contain:
- Views, Controls, Styles, Templates
- Resources (e.g., images, sounds)
- Converters (convert data from lower layers into UI related data)
- Reporting
- Operating system integration (e.g., file system, database)
- Web service communication.
Common Issues
- Implementing business logic in the Presentation layer instead of the Domain layer.
- Implementing the UI workflow in in the Presentation layer instead of the Application layer. Example: The click event handler of the Options button creates and shows the Options window.
- Implementing the validation logic in the Presentation layer instead of the Domain layer. The Presentation layer is only responsible for triggering the validation, but it is not the place for the validation logic itself. If the validation fails, the Presentation layer must show a user-friendly message.
Automated Testing
The Presentation layer is difficult to test because the code is tightly coupled with UI frameworks and other external systems (e.g., operating system, web service). Two common approaches for testing this layer are:
- Automated UI testing using a UI automation framework. With such a framework, you can simulate user interactions and determine the response of your application.
- Writing unit tests for the Presentation layer code can be done to some extent.
The Application layer is responsible for the application workflow. A common way to model a workflow is to use Controller classes. In this case, you might have an ApplicationController that is initialized during the startup sequence. When a program function is triggered, the ApplicationController might delegate the process of that function to a use-case controller.
This layer has the same width as the Presentation layer in Figure 1. The width implies the range of applicability of the layer. Thus, the Application layer is not more reusable than the Presentation layer. This is because the Application layer is not completely independent of the Presentation layer.
The motivation for this layer is to separate the presentation from the workflow concerns. The introduction of the Application layer improves the testability and maintainability of software systems.
Common Issues
- Implementing business logic in the Application layer instead of the Domain layer.
- UI framework types are implemented or used within the Application layer, although they belong to the Presentation layer.
Automated Testing
This layer should be designed in a way so that you are able to unit test all the code. It shouldn't contain unit test barriers like the Presentation layer does.
The domain layer is responsible for the business logic and only for the business logic. It is essential to separate the concerns here, because in my experience the business logic is more than complex enough without mixing it with other aspects. For example, any UI specific code here would violate the principles of the layered architecture.
Another reason for separating concerns in this layer is given by Martin Fowler: "Since the behavior of the business is subject to a lot of change, it's important to be able to modify, build, and test this layer easily. As a result you'll want the minimum of coupling from the Domain Model to other layers in the system. [Fowler03]"
Unfortunately, the separation of concerns isn't so easy to follow with data access strategies. Ideally, a developer working in the domain layer shouldn't care about how the data is stored in a database or a file. The separation of business logic and data access strategies is also known as Persistence Ignorance (PI) or Plain Old CLR Objects (POCO). Modern persistence frameworks try to support this concept, but all of them have some limitations in modeling business objects.
Software systems usually require some kind of validation to ensure that the business logic is working with the correct data. These validation rules are defined by the business model and so the Domain layer is the right place to implement them. Just keep in mind that you should avoid duplicating the validation code between the business objects.
Common Issues
- The validation logic code or the business logic code is duplicated in other layers.
- You can't unit test the business logic decoupled from the data source. For example, when writing unit tests for your business logic, you need to set up a database with predefined test data.
Automated Testing
The domain layer is the heart of your application. In this layer you have the code that is important to your customer. However, I believe that writing unit tests for your business logic is essential to ensure a high quality software system. Unfortunately, some persistence frameworks make unit testing isolated from the data source very difficult. This has a negative impact on the time and effort required to write all unit tests for your business logic. I would keep this in mind when choosing a persistence framework for your application.
The Foundation layer provides technical services that are independent of the business logic. Foundation is the most depended layer because all layers can use types from it. Therefore, it must be more stable than other layers. Stable in this context means that existing public members are not changed in their signature and behavior.
Examples
- Collection classes (e.g., List)
- Logging (e.g., TraceSource)
- Validation Frameworks (e.g. System.ComponentModel.DataAnnotations)
Common Issues
- This layer must not depend on any UI frameworks or system interfaces.
- Fowler03. Fowler, M. 2003. Patterns of Enterprise Application Architecture. Addison Wesley.
- GHJV95. Gamma, E., Helm, R. Johnson, R., Vlissides, J. 1995. Design Patterns. Elements of Reusable Object-Oriented Software. Addison Wesley.
- Larman04. Larman, C. 2004. Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development, Third Edition. Addison Wesley.