This paper explains the importance of slicing a code base into distinct functional areas that can be developed and tested independently.
It is not often that an enterprise Flex application serves a single purpose, like sending a message or editing a customer address. More often features like these are grouped into larger compositions that serve many different purposes. Such an application might consist of a sales dashboard, a content editor, a messaging system and other distinct functional areas.
For multi-functional applications, it's important to organise the code base such that the functional areas are separate from one another. This separation means that the implementation details of each functional area are encapsulated and can change independently. Any integration between functional areas should take place through a thin API. With this approach, each functional area can be developed and tested in relative independence, helping teams to deliver efficiently, even on large-scale projects.
This article discusses how to identify functional areas, then separate them for independent development, and integrate them loosely without introducing direct dependencies.
A functional area is essentially a part of an application that serves a specific purpose for the user. In most cases, a functional area corresponds to a particular region of the user interface, such as the Dashboard or Messaging views shown in Figure 1. However, a functional area may also contain non-visual components, such as an isolated domain model and persistence mechanism.
Figure 1 - An application with 2 functional areas.
The initial functional areas of an application usually become apparent at an early stage of development, as soon as the essential features and basic user interface design is known. Additional functional areas appear later on, as new features are scheduled for delivery, and older functional areas sometimes require splitting into smaller parts. The secret is to always keep functional areas small, so they can be understood, developed and maintained with ease. Even when functional boundaries change, it is easier to refactor a set of smaller, self-contained functional areas than to slice up a monolithic application that may be entangled with dependencies.
Once a functional area has been identified, it needs to be separated from the rest of the code base in some way, so that it can be developed and tested independently. It is this separation that increases developer productivity, making it easy to understand complex applications at different levels of abstraction: from a high-level composition of functional areas, to the finer details of a specific feature.
There are various ways to separate code by following conventions, or better, by using organizational features of the Flex SDK. The goal is to isolate one functional area from another, so that a change made to the implementation detail of the Dashboard does not disrupt the operation of the Messages functional area. The implementation details are encapsulated and any integration between is confined to a thin API, carefully designed and seldom changed.
Some guidelines for separation of functional areas follow:
The code for each functional area should be placed into separate packages. For example:
This is in contrast to package structures popular with Cairngorm 1 and 2 projects, that instead organized code first into architectural groupings, such as myapp.view, myapp.model, myapp.control. This approach is now considered a bad practice, since it spreads closely related code across disparate packages, making functional units difficult to comprehend.
Please refer to the Cairngorm 3 Packaging Guidelines and reference application for more details about recommended package structuring of Flex projects.
It is important that functional areas do not interact directly with one another. Otherwise dependencies become difficult to manage and functional areas cannot be changed independently of one another. A revision to one implementation may have serious implications for the rest of the application.
A thin API should be defined for integrating functional areas. One functional area can use the API of another to communicate and invoke actions. Think of it as one functional areas offering services to one another though its API. These APIs should generally consist only of interfaces, events and data transfer objects (DTOs). For example, the Dashboard might display summary messages and clicking upon one of those could dispatch an OpenMessageEvent, which is a part of the Messaging functional area's API. This would cause the application to navigate to the Messaging area and display the full message details.
An API should be open for extension but closed for modification, and a good API should satisfy the following criteria:
Direct dependencies between the implementation details of distinct functional areas should be prohibited, since such dependencies increases code complexity and leads to brittle applications that are difficult to understand and prone to regression. Instead, the API of one functional area should be used by another to communicate.
There are various ways to prevent developers from introducing prohibited dependencies. The best approach is to store the API classes in a Flex Library project, and encapsulate the implementation details within a Flex Module, stored in a separate Flex Application project. A separate project is typically used for each module and a single, common library for the APIs. In this way, the compiler will enforce the separation between API and implementation so that two functional areas cannot depend on implementation details of one another. This project structure is shown in Figure 2.
Figure 2 - Use modules and libraries to restrict dependencies
An alternative approach is to use packaging conventions and tooling to separate API from implementation. Underneath each functional area package are placed two sub-packages: api and restricted. The api package holds the interfaces, events and data transfer objects (DTOs), while the resticted package contains the implementation details. Returning to the previous example, the following packages would be added:
With this approach, FlexPMD should be used to detect violations where one functional area accesses code within the restricted package of another. This is workable but it is better to enforce the separation with modules and libraries.
Any functional area that uses an API becomes dependent on the design of that API. In a large application, many different functional areas can come to depend on the API classes. For this reason, it is most important that the API classes are well designed and seldom changed. The API should be thin, containing the minimum quantity of classes to express the intention clearly. If the contract of an API method is changed in some way, any dependent class will also need to be changed. However, this contract and the separation provided by the API allow the implementation details of a module to change freely without consequence on other parts of the application.
On larger projects, a process should be established for adding, removing or altering API classes, and tooling can be used to enforce the process. For example, a small subset of the team may have write-access to the Core project holding the API classes. This can be enforced by some version control systems, such as Perforce, that can restrict access to a folder to a particular group of users. A deprecation policy is appropriate where distributed teams are developing separate modules of a common application and the @deprecated ASDoc tag can be used as a marker. On smaller projects, a more relaxed approach is suffice, unless external third parties are depending on the API.
Any code inside a Core project that is widely depended upon becomes most important for the robustness of a system. Once behavior is shared in this way, a thorough test design is called for. Any logic inside code classes should be unit tested and any visual components should be subject to functional tests. A higher-level strategy for testing the integration between modules is also recommended, and can be carried out by a QA team with or without an automation tool.
When functional areas have been identified and separated from one another, there usually remains a need for some kind of integration between them, so, for example, the Dashboard could invoke an operation of the Messaging view, or the Messaging view could locate a contact in the Address Book. This integration needs to be achieved in a loosely-coupled manner if the benefits of separation are to be preserved. Most frameworks provide a mechanism for this purpose.
What follows are some guidelines for integrating functional areas. These are not unbending rules and there are perfectly valid variations to them. The important point is to always separate regions of complexity (i.e. the implementation details of a functional area) from one another. It is beneficial to adopt a consistent approach across a project.
Flex is a very event-driven language and most frameworks provide a form of global event dispatching or routing, sometimes known as a messaging framework. Parsley supports messaging with local, regional and global scoping, Swiz and PureMVC provide a form of global or mediated events, and Cairngorm 1 & 2 provides a singleton CairngormEventDispatcher through which events can be dispatched and heard from elsewhere in the code-base.
Note: Care must be taken when attaching event listeners to singletons, since a reference is created from a singleton that always exists. This will prevent garbage collection, unless weak references are used or the event listener is explicitly removed. Frameworks that provide specialized messaging features, such as Parsley, generally take this into account and clean-up automatically.
The event and messaging systems provide by these frameworks can be used for integrating functional areas. Dispatching an event or sending a message is a convenient and loosely-coupled way of invoking an operation. For example, the Address Book functional area might provide a SearchForContactEvent that can be dispatched by another functional area to perform a search and display the matching contacts.
Events are particularly convenient for invoking asynchronous operations that may not happen immediately. Returning to the earlier example, the Address Book functional area might be modularized and loaded only on demand, only when the first SearchForContactEvent is dispatched.
Interfaces provide another means of loosely-coupled communication. They are best suited to synchronous operations that return immediately, but like events, they can also accommodate asynchronous operations, using call-back functions, responder interfaces or the asynchronous token design pattern.
An interface can be used to group a number of related methods together, which is convenient if each clients needs to use multiple of these methods, but problematic if the methods are intended for different clients. In this case, the interface should be split up according to the Interface Segregation Principle.
The other consequence of using interfaces is that one functional area needs a way to access the interface onto another. This is most simply achieved using an inversion-of-control framework that supports injection-by-type. For example, using Parsley, the client of a Products functional area could access the interface by placing the Inject metadata above a property:
[Inject]
public var products:IProductService;
The IProductService interface might define a group of methods for searching and filtering a global set of products in different ways. Parsley would then automatically locate the implementation of this interface and inject it onto the property after construction of the object. If you are developing a modular application, the integration classes need to be accessible to the module. With Parsley, this would normally involve building a hierarchy of inversion-of-control contexts and placing the integration classes into a root context, inherited by the modules. Using a global context instead would be risky for large-scale applications due to object identity and class conflicts.
In general, Cairngorm favours events for communication between functional areas because they focus on a single operation, don't require any kind of "look-up", and are naturally asynchronous, supporting lazy-loading of modules and other deferred operations. However, interfaces are recommended where a set of related operations is needed, particularly when communicating with a shell application or code components. For example, an interface might be defined for accessing a User Profile functional area.
In some cases, functional areas can communicate through events and interfaces that only require simple parameterization, with strings and numbers. At other times, it's necessary to send or receive more structured data between them. In this case, the data can be grouped onto data transfer objects (DTOs), or the functional area can provide interfaces for its clients.
For example, a System Status functional area might provide a ShowStatusMessageEvent which is parameterized with a StatusMessage DTO:
public class StatusMessage
{
public static const INFO:int=0;
public static const ERROR:int=1;
[Bindable]
public var level:int;
[Bindable]
public var title:String;
[Bindable]
public var message:String;
}
Or a News functional area might define a SearchNewsEvent that takes a call-back function and passes as the result a list of objects implementing an INewsSummary interface:
public interface INewsSummary
{
function get publicationDate():Date;
function get headline():String;
function get summary():String;
}
In this way the concrete class for news summaries remains under control of the News functional area and any consumers use the contract provided by the interface.
When using DTOs, it is recommended to keep them pure, free from any behavior except for storage and retrieval of data. If an elaborate domain model is shared instead, then the dependencies between functional areas are deepened. Changes to that domain model can have consequences that are difficult to manage.
Functional areas can also be encapsulated inside "smart" components. This name is given to distinguish from a standard reusable view component, such as the controls and containers of the Flex SDK. A "smart" component serves a business purpose instead of just providing a general mechanism for interaction and presentation. But like a standard view component, a "smart" component is configured directly in MXML or ActionScript through its properties, and integrated through the events that the top-level class for the component dispatches.
For example, an AddressBook functional area might provide a top-level AddressBook component that can be declared in MXML and configured with an IAddressService interface:
<a:AddressBook width="100%"
height="100%"
service="{ myService }"
searchStart="doSomething()"
searchComplete="doSomething()"/>
This approach has a certain appeal in its simplicity. However, it does tend to bring dependencies onto other architectural layers into the presentation layer. Additionally, if the AddressBook component is implemented in MXML, then it will not be properly encapsulated. Clients will be able to reach into the child-components and interact with them directly. The separation has not been achieved and clients can start to depend on implementation details.
Sub-applications have not been covered in this article, but their use is another valid way to separate functional areas, even more strictly than modularization. There are formal rules enforced by the Flash Player regarding communication between sub-applications. These govern the regions of the stage that a sub-application can interact with and the manner by which data can be exchanged. Sub-applications are most suitable for portal type applications that contain functional areas developed by unknown entities. Since communication between sub-applications takes place with loosely-typed, dynamic data, contracts for communication need to be established outside of the code-base and cannot be enforced by the Flex compiler. For more information about sub-applications, refer to the Marshal Plan.
Cairngorm recommends that the code for a Rich Internet Application is separated into distinct functional areas, with integration between them taking place over a thin API consisting of events, interfaces and data transfer objects (DTOs). Changes to the API classes need to be carefully managed, since they are the most widely depended upon.
Functional areas can usually be identified early in a project, but will continue to appear and evolve during development. There are various ways to separate functional areas, including packaging conventions and modularization. As well as the encapsulation provided by a module, the efficiency of a team can be improved with modularization, since modules can be developed, profiled and tested in isolation. Integration of distinct functional areas needs to be loosely-coupled and many frameworks provide features to assist with this, such as inversion-of-control containers and messaging systems.
The following other parts of Cairngorm complement the material covered in this paper:
X Joshua Bloch, "How to Design a Good API and Why it Matters"
X Robert Martin, "The Open-Closed Principle"
X Robert Martin, "The Interface Segregation Principle"
X Douglas C. Schmidt, "Asynchronous Completion Token", 1998.
X Marshall Plan, http://opensource.adobe.com/wiki/display/flexsdk/Marshall+Plan