ARM Yourself for Enterprise App Dev
An architectural reference model (ARM) can help you increase application stability in the face of changing requirements.
by Mark Collins-Cope
July 15, 2005
If you've been involved in large-scale software development for any length of time, you'll recognize these symptoms of application architecture and design decay: You must cut and paste repetitive code (along, of course, with any associated bugs), as proper reuse isn't a viable option. Poor code factoring makes reuse impossible, so any simple functional change must be made everywhere that code is duplicated.
Also, the chaotic package structure of the application means your staff faces a prohibitively expensive learning curve. There's no clarity of responsibility in what package does what. Everyone keeps treading on everyone else's toes when making changes to the system, and the lack of consistent packaging rules makes intelligent work scheduling difficult. Automated testing is difficult as well. It's impossible to test any package in isolation due to poor dependency management; fixing bugs is difficult; and spaghetti-like class dependencies make it difficult to track the source of an error.
All in all, the whole application seems to be unstable, simple functional changes require reworking large amounts of code, and everything breaks all the time.
In this article, I'll discuss an architectural reference model (ARM) for large-scale applications that I've successfully employed on three large enterprise application developments. The ARM is made up of five architectural strata: interface, application, domain, infrastructure, and platform (see Figure 1). The overriding purpose of the ARM is to provide a clear set of rules for large-scale application decomposition that encourages separation of concerns, maximizes reuse of code (primarily) within the application and (secondarily) across applications, leads to good code factoring without duplication, and increases application stability in the face of changing requirements.
To achieve this, the ARM has an easy-to-apply set of rules that bring structure to your application, improved code factoring, increased consistency in packaging rules, and more manageable dependencies between packages.
Each stratum acts as a placeholder for the packages that together will make up the source code to your application. It's important to note that the stratum are not themselves packages, butas per the rules belowhelp to determine what should and shouldn't be in any individual package.
The ARM has general rules, which apply across the whole model, and stratum-specific rules, which apply to each stratum. These are the general rules:
- Strata are divided according to functionality and dependencies. Each stratum has an associated set of responsibility guidelines that determine what the packages and classes within it are allowed to do. Each stratum depends on the strata below it; the functionality of the classes within it will be built using the facilities provided by lower stratum classes and packages.
- Depend downward. Packages in a given stratum are allowed to depend only on packages in the same or lower stratum. Avoid dependencies to packages in the same stratum.
- Packages can live only in one stratum. A package that contains some classes belonging to one stratum and some belonging to another is designated to live in the higher stratum. Well-factored applications tend to have a greater number of classes within the lower strataone objective of the ARM is to put a focus on this type of factoringso if a package crosses strata boundaries, you should consider restructuring it. Applications with all or most of the packages in the higher strata tend to be poorly factored and often exhibit the problems I discussed at the beginning of this article.
These are the stratum-specific rules:
- The platform underpins the application development. The platform stratum contains packages or utilities that are acquired externally to support the development. Typical examples include Jakarta Struts, java.lang.*, Swing, Microsoft Foundation Classes, .NET GUI libraries, and so on. Choice of platform technology is a key to ensuring project success, although it's beyond the scope of this article.
- Infrastructure neither contains nor depends on domain-specific code. Packages in the infrastructure stratum contain general-purpose (nondomain-specific) classes that provide utility functionality applicable to many types of applications. Infrastructure might only depend on (import from) platform. Typical examples include general-purpose object/relational mapping code (persistence), general-purpose observer mechanisms, general-purpose group-based security mechanisms, and thin wrappers imposing a restricted API on platform functionality.
- Domain contains domain-specific classes (often called entities). Packages in the domain stratum contain domain-specific abstractions you'd typically find in an entity relationship or domain model. User and/or external system interface/presentation code is specifically prohibited. In the context of enterprise applications, the domain stratum should provide the illusion (abstraction) that all domain classes are in memory, hiding the details of any persistence mechanism. The domain might also hide other infrastructure concerns, where this does not introduce needless complexity.
The upper domain stratum contains packages made up of "reference" objects, those typically persisted in their own right in a relational database. Typical examples include a customers package (providing access/update functionality to the set of customers) and an accounts package. The lower domain stratum often contains packages that provide "value" objects, meaning those typically contained by value as attributes of upper domain classes. Examples include telephone number, date, and money.
- Application provides a service-oriented architecture. Packages in the application stratum provide a set of application-specific transactional services that the interface stratum uses to query and/or update the application state. Application packages typically "wire-up" or provide the "linking glue" for decoupled domain packages. The upper application stratum provides transaction services such as createCustomer, getAllCustomers, createAccount, and getAccountsByCriteria. Lower application packages, if present, will contain utility subservices that are non-transactional but are used to construct the transactional services provided by the upper application.
- Interface packages make the application do something. At its most fundamental, the purpose of the interface stratum is to interact with the outside world, call the application stratum to change the application's internal state. Most commonly, the interface stratum holds packages that provide an application-specific UI; that is, UI classes particular to the application in question (not general-purpose UI toolkit classes). The interface stratum might also contain code to parse application-specific file-interchange formats such as a custom XML format; deal with requests from external systems (Web-enabling code); or perform application-specific, timer-related activities.
The upper interface typically contains complete UI dialogs such as the "create a new customer" screen, or the "find a video" screen. Lower interface, if present, might contain application-specific UI "widgets" such as a "find video by name" pane (one used on multiple screens), or an account-type dropdown listbox (containing values such as "current" and "savings"). Lower interface widgets are used to build upper interface UIs.
Video Storesa Case Study
I'll now show you an example based on a video store application. Note that the apparently simple set of requirements is sufficient to pose some architectural complexity:
- Customers can register for e-mail or text message alerts that tell them when a video is available to rent.
- Customers can reserve a video by replying to a text message alert.
- Customers can search for videos by partial title, and reserve a video using the GUI.
The interface stratum contains three packages, each of which is responsible for kicking the application into life (see Figure 2):
- InterfaceReservationGUI: responsible for the UI to reserve a video.
- InterfaceIncomingSMS: responsible for reserving videos in the event that a customer responds to an SMS alert, parsing the SMS text and creating a reservation for the video.
- InterfaceAlertControl: responsible for sending alerts periodically.
Notice that all three of these packages depend either on infrastructure or platform packages, due to either inheriting from a class or implementing an interface. In all three cases, control is passed to interface code from infrastructure or platform code through a callback (using a technique called Inversion of Control or Dependency Injection). This situation, not uncommon, occurs when code is factored to remove application- and domain-specific dependencies. This leads to better code factoring and making the SMS package usable in multiple contexts. The ARM gives context to this type of code factoring.
The application stratum contains four decoupled packages, each of which provides a set of services that interface code can use. For example, ReservationServices code is used by both the SMSParser and the InterfaceReservationGUI packages. Each package provides a different UI to the same underlying functionality. This type of code factoring is a key motivation in separating interface and application concerns.
The domain stratum contains four packages, one for each entity identified in the domain model. Each package follows a common project pattern and is completely decoupled from the others. None of the domain packages depends on any other: Videos don't know about Customers, Customers don't know about Videos, and perhaps most surprisingly, Reservations don't know or depend directly on either.
The DomainAlerts package keeps track of alerts and sends them when asked to do so. It doesn't actually know the mechanism(s) by which an alert can be sent, so it provides an interface (of the Java type), which is implemented by ApplicationAlertServices. If only one alert mechanism were needed, this might be considered needless complexity. However, alerts can be sent by e-mail or SMS, so this design requires less code overall and has the additional benefit of making automated testing easier (see Figure 3). The design is now also inherently extensible.
It is not atypical to see examples of domain packages having their behavior customized, in this fashion, by application packages. Indeed, this is one of the drivers for the separation of domain and application.
The infrastructure stratum contains two packages. The InfrastructurePersistence package is worthy of an article in its own right, but for our purposes I'll just say that it manages the interface to a relational database, exports the Key class (and probably a Versioned Key class) and Transaction (unit of work) class, and ensures things are stored in the database. Both application and domain classes rely on persistence facilities directly. In particular, each service exported by application packages will use Transaction class facilities.
Of more interest here is the InfrastructureSMS package. A package providing general-purpose SMS facilities is an obvious candidate for inclusion in the infrastructure stratum, and can be reused across many projects. Here, an SMSListener class is instantiated to listen in on an appropriate number (that is, the number on which we're expecting to receive incoming text messages). Incoming messages are forwarded to the SMSActioner interface, which in this example is realized by the SMSParser class in the InterfaceIncomingSMS package (see Figure 2).
The platform stratum contains the building blocks that underpin the whole development. If you assume the video store's application is written in Java, it will most likely use standard Java libraries to build the GUI, interface with the database through Java Database Connectivity (JDBC), perform the basic timing mechanism, and send e-mail.
As you can see, the reference model has been conceptual, in terms of helping you think about application structure. It has also been practicalin terms of concrete package subdivision visible in source codein getting you to a well-structured, well-factored application with a coherent and manageable set of dependencies.
Good Application Structure
So what exactly does it mean that one stratum is "on top of" another in this model? The ARM pulls together three threads of reasoning about good application structure:
- Dependencies. Compile-time dependencies are most simply understood. Packages in a higher stratum import from packages in the same or lower stratum. Put another way, the dependencies always point downward. Understanding and managing your dependencies is a key feature of flexible application architecture, and the ARM helps you do this.
- Functional specificity/neutrality. The higher the stratum, the more application-specific and functionally powerful the operations provided, and the nearer you get to a complete application. Put another way, the higher you go, the easier it should be to build your specific application with the facilities provided. Conversely, the lower you go, the more work you'll have to do to build a specific application. But going lower opens up the range of possible applications you could write.
- Stability. The higher the stratum, the less stable packages become in the face of changing customer requirements. It's far more likely that an interface package will change than a infrastructure packageassuming you've factored your packages correctlyand the whole point of factoring out domain-specific functionality from infrastructure is to make it stable against change. But given the state of programming language technology, changes in nonfunctional requirements (say, changing a single-user, in-memory application to a multiuser database application) can still pull our foundations out from under us. So you want to tie down nonfunctional issues as early as possible in a project lifecycle.
Dependency management and functional power/specificity are integrally related concepts, for the pure and simple reason that we build higher-level functionality out of lower-level functionality. Stability is a byproduct of factoring out higher-level functionality that is more likely to be subject to change. In combination, these factors make the ARM a powerful weapon for enterprise architects.
Why Five Strata?
As you've seen, the platform stratum is the home of the technology base that underpins your application development. Getting your platform components right can be a project in itself, even though platform and infrastructure share a lot of similarities. The distinction between them: If it is externally sourced and nondomain-specific, it's platform. If it's written internally, it's infrastructure, assuming no domain dependencies have crept into it.
The distinction between interface and application is also fairly clear: If a class is directly related to an application-specific UI, then it's interface code; if some code is there to deal with external system integration (such as Web services), then it's interface. If the code is dealing with parsing an application-specific file format, then it's interface. But note that in the same way UI code splits into general-purpose (platform) and application-specific (interface) categories, most XML parsers are platform. XML is domain-neutral, but specific DTDs will require custom interface code to be written to interpret domain-specific concepts.
The distinction between application and domain can be confusing. As I said earlier, application code provides a set of transactional services for interface code to use. Domain code provides decoupled domain-level abstractions such as Book, Account, and so on, which are then combined by application code to provide application functionality. Without application packages, decoupled domain packages would remain forever decoupled, and there would be no application.
The application/domain divide is also important from a reuse perspective, specifically reusing domain code within a single application. In our example, the DomainAlerts package is customized by the application layer in two different ways: one to provide text message alerts, the other to provide e-mail alerts. Pushing the common alerts code down into the domain stratum assists reuse through improved code factoring, and also localizes most alert-related code into the DomainAlerts package. This, in turn, reduces the cost of functional change in the way in which alerts are supposed to operate.
One final motivation for the application/domain divide is testability (see Figure 3). Although is it both possible and advisable to undertake automated "functional" testing using the service-oriented interface provided by the application stratum, it will be difficult to get a high degree of test coverage. Testing domain packages in isolation enables you to greatly improve the overall test coverage and overall application reliability.
Q & A
We do agile development. How can we use the ARM if we don't do up-front design? Design guidance and development process are orthogonal concepts. You might arrive at an architecture by months of forethought, or you can let the ARM assist you in evolving your architecture in an incremental fashion.
Is there a high cost (in code terms) because of all these strata? The ARM is about packaging. Every line of code you write using the ARM should be absolutely necessary to meet the needs of your customers, the needs of automated testing, and the needs of delivering a high-quality, well-factored application that shows intent at a code level. You don't have to write any code because of the ARM; you just have to put the right code into the right package.
Dependencies seem to go right across the strata. Shouldn't there be a rule saying one stratum can use only the stratum below it? No. Consider the ReservationVideoButton, which inherits directly from the PlatformGUI package. Applying a rule like this means it would be necessary for each of the three intervening strata to provide a wrapper hiding the facilities provided by the stratum below it. To mandate this would create needless complexity.
Is persistence always infrastructure/platform? No, not always. It takes some considerable effort and skill to build a general-purpose persistence mechanism, and this might be overkill for a single project. In such circumstances, it is not uncommon to write domain-specific persistence mechanisms (sometimes in the form of brokers for various domain classes). This might lead to some code duplication (it's the duplicate code you would ideally factor out into an infrastructure package), but might be necessary.
Can the ARM be used in conjunction with code generation? Code generation, such as generating domain-specific persistence code in the EJB style, is an orthogonal concern, but it can cause confusion. If you're confused, take a look at the generated code to see where it fits into the ARM.
What about vertical subdivision of the application? The ARM deals only with horizontal subdivision, doesn't it? That's right. As I alluded to earlier, there is a one-to-many relationship between a stratum and the packages it contains. For completeness, you need some sort of guidance in this subdivision. Help is at hand here in the form of two packaging principles:
- The Common Closure Principle (CCP) states: "Package things together that change together." DomainAlerts contains the Alerts, Alert, and AlertSender packages precisely because they are highly interdependent (highly coupled), and a change to any of them is likely to affect the others.
- The Common Reuse Principle (CRP) states: "Package things together that are used together." ApplicationAlertServices contains three classes that appear to be unrelated, but the AlertServices.SendNecessaryAlerts method can't be used without supplying one or more concrete AlertSendersthe EmailAlertSender and SMSAlertSender classes, in this case.
What practical steps can I take to apply this model to my project? It's remarkably simple to use the ARM to keep track of your project's package structure. All you need is a large whiteboard, which you divide into five horizontal sections as per the strata. Then draw a symbol with the package name for each package in your system. Show the dependencies between packages as arrows, all going downward. This type of diagram is known as a package map (see Figure 2).
In this article I have presented an architectural reference model that I've used successfully on a number of enterprise applications. Following this model can assist you in improving the structure of your code, in particular ensuring greater clarity of responsibility at the package level; improving overall code factoring; reducing duplication within code; and enabling you to manage your package dependencies effectively.
About the Author
Mark Collins-Cope has been working in software development for more than 20 years. He is author of Agile Software Development with Iconix Process, along with Doug Rosenberg and Matt Stephens (Apress, 2004). You can reach Mark at mark@ratio.co.uk.
|