Thomas Gil - ©2007
In recent years much has been made of multitiered application design. Much of the thinking is applied to server-side structures where one has to deal with a layer of code dealing with presentation. The server implementing a web-application has to incorporate fixtures that have little importance to performance or correctness of the logic but have a great impact on user experience. This is the case with the HTML pages fed to browsers that have to present a good layout decorated nicely with image fixtures.
That kind of code is really akin to web design and its separation from business logic is very desirable since it reduces dependencies it brings the advantages of modularization such as rapid development, ease of modification of deployed modules in production. The separation between the web-tier and business logic induces further modular breakdown inside the business tier. The principal line of module separation is between the actual business logic code and code facilitating the access to external system and network resources - such as files, databases, network connections, messaging systems.
Looking at the reduction of a server system to a three tiered view, so commonly now seen in practical web serving applications, we can observe the main principle of modularization. That principle consists of drawing the separating line in a way that dependencies running across the line are as few as possible and can be easily grasped by an idea expressed by a software design logic. Taking these lessons into account we are ready to tackle the issue of a general application - either server-side process or user-interface driven system.
Every application runs in some environment defined by the OS, by the windowing system, by the framework of an application server container. An application acquires a knowledge about its configuration, the user identity, the authorization implied by the combined parameters of the environment. This is a fundamental function of the top level tier of the application. The top level tier is also the driver of the business functionality of the application but it should not be involved in specifying the detail of the business functionality. The top level tier is expected to ascertain the configuration and set up the lowest-level resources in accordance with it. How can it do that?
By interacting with the lowest tier - the tier of accessors to resources. The top tier is not isolated from the bottom tier - on the contrary it should have a full knowledge of the capabilities offered there. The configuration information at the top tier - like network addresses, database access strings, protocol selections - need to be used to configure the bottom tier.
The middle tier is supposed to be oblivious to the framework and access the low-level resources by wrappers and abstractions. In this way the middle tier which really represents the business logic and the application's business value will remain independent of the execution environment, operating system, external resources and interoperability protocols. In object-oriented applications the business middle layer is written using abstract code and abstract types with instances that are obtained from factories set up by the top layer.
The top layer has thus another function: in addition to setting up low-level accessors and their sundry and various parameters, it also sets up factories where specific knowledge about implementations and configurations of types used by the business layer will reside. That knowledge is of course specified by the configuration information absorbed at the top layer of code.
Here is a table of propagation of knowledge between the three layers of code constituting an application.
Table 1. Propagation of information in application tiers
Which layer | Mission | Scope of knowledge |
---|---|---|
Application driver | configuration factories concrete types selected | knows the purpose of invocation, interprets errors in the general business context |
Middle Layer | business logic, abstract types | knows the business logic, throws appropriate errors up to higher level |
Low level | accessors, OS interface, parsers, abstract and concrete types selected at top level | knows the technique of handling the information, typically errors thrown should be interpreted at the highest level |
Let us consider an application with a graphical UI. It uses some technology needed to make that UI in a given OS environment and utilizes a toolkit - for example Qt. The application driver will initialize the toolkit, establish the user an licensing information, find configuration data on the local machine on via the network. This would enable in turn to configure the low-level accessors correctly - instantiating database connections as well as windowing options including screen size and colors available. When absence of such resources or their shortcomings are detected the highest level of the application should handle the error - and perhaps adapt to a lower screen resolution, lack of certain fonts or to conditions detected in data. After this happens the application driver at the highest level activates the business layer operating oblivious to the nature of low-level facilities. Errors occurring in the middle layer are not system errors and indicate a genuine failure of the application. Errors in the low-level are of such nature that a typical business processing code needs to abort a current operation. In programming languages like C++ and Java this can be elegantly accomplished by automatic stack unwinding upon exception throw. Most errors occurring at the bottom layer can be handled at the top layer thanks to this feature.
An application that runs as a service (aka UNIX daemon), either processing requests or functioning as part of a workflow is similar. It does not need to establish a UI but it needs to find its configuration and environment. The top-layer needs to configure low-level facilities, often including logging, start the business logic and be ready to handle low-level errors. Database connectivity errors would typically stop all business processing, however, certain data conditions detected during the long-lived run of a server program, like configuration changes, should be communicated to the top-layer - either via specific exceptions or via registered listeners. The top-level will have appropriate context to respond to changed conditions - for example by notifying originators of work requests to redirect them.
I have given here a few design principles mainly dealing with organization and distribution of various kinds of logic across aplication code. Adherence to these principles will lead to elegant and portable design, good maintainability, limited code dependencies - attributes of easily manageable projects with minimal consupmtion of development and testing resources over their lifecycle.