I'd like to share some of the design highlights of a large-scale content distributing system I worked on a while back. Some of the highlights may seem trivial; some may be a little more complicated. To me, software design is a matter of finding a balance between applying available technologies and fulfilling real-world requirements and constraints. The goal of design is always to ensure both the runtime and development-time quality of the software.
I'll be using two of the components from the project, Scheduling Management Component (SMC) and the Data Access Layer (DAL), for purposes of illustration. The function of SMC is to monitor the database and detect any newly added or updated content, and schedule that content for delivery based on some business logics. The DAL, as its name implies, provides data access services.
Extensibility by OOP Patterns and Principles
In the early design phase of SMC, we quickly lay out some of the candidate classes, as well as the relationships among them. In UML notation, class details are omitted at this stage. Figure 1 shows some of the major classes. The classes are by no means final, and neither are the relationships. We're trying, however, to capture the business requirements as much as possible.
We've designed the Schedule-Manager class as the façade of this component. It exposes external APIs. Other components that need to interact with SMC do so through this class. ScheduleManager is also in charge of managing the lifecycles of classes inside SMC.
One of the business requirements is that the first production release of the software won't support clustered deployment, but it will in later releases. One of the keys to supporting clustering is managing the states of the component instances deployed across the clustered nodes so that they're always in sync. Keeping this in mind, we decide to centralize the cluster-sensitive states of all the SMC classes into one class rather than having each class manage its own states. This results in a SchedulingContext class. This class knows how to save and retrieve various states of the component. When the time to support the cluster comes along, instead of having to open up each class and make changes, all we have to do is change the SchedulingContext class - that modifyies the way this class accesses states so that all the clustered instances share the same states, virtually or physically.
The UML diagram in Figure 2 shows some major classes after introducing the SchedulingContext class.
One issue we immediately notice with this design is the tight coupling between the SchedulingContext class and the classes that depend on it. As you can see, quite a number of classes depend on the SchedulingContext class. If we have to change the SchedulingContext class for any reasons (fixing bugs, adding new business features, switching to other application server, etc.), chances are we also have to make changes to the dependent classes.
Following OO best practices, interfaces are good at promoting loose coupling. In fact, it's always a good idea to code to interfaces rather than concrete classes (another simple yet powerful OO principle). The solution is simple enough - we abstract the scheduling context by making SchedulingContext an interface. We also want this interface's client to be unaware of the actual implementation, and the GoF Abstract Factory Pattern does the job.
The design of the Scheduling Context is shown in Figure 3. When it comes time to support clustering, we can do it with little programming. It makes it a lot easier to maintain the software after it goes into production. We also avoid vendor lock-in by shielding the cluster-sensitive implementation, which is likely to be vendor-specific.
Another consequence of moving the states of an individual class into SchedulingContext is that we can now design other stateless classes, which is good because:
Note that, at this point, ScheduleManager and SchedulingContent are acting together as the "container" of the other classes in the sense that:
This is intended and, as you'll see later, we'll use the container-like feature of these two classes more in our design.
Decoupling with IoC Pattern
So far, everything is pretty simple and straightforward. What else can we do to make the design better?
With the current design, unit testing isn't a trivial task. Let's take the FNDTaskPuller class as an example. Listing 1 shows the simplified version of this class. For demonstration purposes, let's assume this class has a business method, someBizMethod().
To test this class, we'd create a test class similar to the one shown in Listing 2.
What's the problem with this code? We're testing SchedulingContextSimpleImpl (which is the concrete class of SchedulingContext) indirectly. But we really mean to test the FNDTaskPuller class. This isn't right. As we all know a unit test should never go outside of its own class boundary.
Furthermore, it's hard to control the states of the SchedulingContextSimpleImpl in order to test the different behaviors of FNDTaskPuller class.
In practice, a common technique to overcome this kind of tight coupling is using mock objects, which can assist in separating unit tests. Mock objects themselves, however, require extra coding efforts. This extra coding effort can be significant, buggy, and cause maintenance problems. What more? Developers have to replace mock objects with real classes at deployment.
The reason for this problem is the way we acquire the reference to the SchedulingContext object in the FNDTaskPuller class. In this case, the FNDTaskPuller class is asking for a reference to a SchedulingContext object. Explicitly.
To get around this, we need to change the way we obtain an object reference. This is where Inversion of Control (IoC) comes into play.
With IoC, objects obtain references to their dependent objects passively. The IoC container literally "injects" the dependency into the classes.
Now, we need an IoC container for our design to wire the objects. We have the choice of using an existing container product or building our own. Normally in-house framework building is considered a bad practice because of its complexity and inefficiency. However, we decided to do it anyway after carefully figuring out what we really needed. Some of the reasons are:
Our solution was to embed the simple dependency wiring functions in the ScheduleManager class, which, as mentioned, was already acting as the SMC "container." It also made sense to embed the IoC functions in this class. Because of the simplicity of construction injection, we refactored all the classes depending on the SchedulingContext so that they were ready for constructor injection. Listing 3 gives the FNDTaskPuller class as an example.
All we have to do in the ScheduleManager class is to instantiate a SchedulingContext object and assign it to classes that need a reference to it. If, some time in the future, a full IoC container is needed, we can just modify the ScheduleManager class.
Taking Care of Scattered Code
While designing the data access layer, we foresaw that there would be a lot of scattered "plumping" code - code that doesn't do anything related to business functions. They are only here to fulfill middleware functions: JNDI lookup, JDBC resource management, exception handling, etc. This code will spread into almost all data access objects (DAOs) and, in most cases, this code is identical method to method. We all hate scattered code because it's a maintenance nightmare, is error-prone, and makes code hard to understand.
List 2 is an example of a how we would have implemented our data access methods without AOP.
In Listing 4, only one line of the code is actually "doing something." The rest is just "plumping" code. When "plumping" code starts leaking into your application, chances are you'll find yourself hunting down all the application code when requirements change.
There's one thing we can do. While OOP makes software design modular, AOP makes code modular. And modularity is good.
Once again, we faced the choice of using an existing AOP framework or building our own. We decided not to use any of these frameworks for the same reason why we didn't use a IoC container. Instead, we separate the concerns programmatically in our code. Although this is not the most elegant, cleanest way, it's the fastest, which, in our case, is a big gain.
To separate the cross-cutting concerns from the DAOs, we came up with the following class design:
Figure 4 shows the class diagram. A client asks DAOFactory for a service provided by a particular DAO by passing in the DAO class name. Upon request, DAOFactory instantiates a proxy instance and a real instance of that class, and hands the proxy back. The client then makes calls on the proxy object, instead of the real instance of the DAO class, to consume the service.
Because all calls are made through the proxy object, the proxy can intercept the calls, wrapping the business methods with cross-cutting concerns. Listings 5, 6, 7, 8, and 9 show a simplified version of these classes. Note that MaterialDAO is just an example of many DAOs.
Note that with this design, the DAOs aren't completely POJOs: Each DAO has to provide an interface and extend the AbstractDAO super-class. The clients are also aware of the concrete implementation class. With this solution, however, we're trying to land somewhere between the pain of living with scattered code and the burden of implementing a complete AOP framework.
Conclusion
In this article, I've just covered a small number of the design issues in our project. It's an even smaller part compared to the real world of software design. The idea is to present our way of thinking of design. We believe this kind of thinking will definitely familiarize the team with IoC and AOP concepts and prepare it for next step forward.
Sidebar
The Open Close Principle
In the OO world, there's this design principle called the Open Close Principle. It says that software should be designed so it's open to extension and closed to modification. Set aside its fancy name, it basically suggests that software should be extensible function-wise (i.e., open to extension) without having to open up existing code and modify it (i.e., closed to modification). Functional extension should be done by creating new modules and plugging them into the existing system.
The consequence is beneficial. After all, you don't want to modify your well-tested code and risk your code to new bugs, which can result in "wreck-a-mole"-style bug fixing (fixing one bug introduces more).
2nd Sidebar
Inversion Of Control
Inversion of Control (IoC), also referred as Dependency Injection (DI), is a powerful pattern that can be applied in software design to reduce coupling between components. It's a key feature in lots of lightweight containers, which help assemble components from different sources into a cohesive application.
IoC comes in three flavors - type 1, type 2, and type 3 IoCs; however, they're more often referred to by their more descriptive names: interface inject, setter injection, and constructor injection, respectively.
Nowadays many IoC container products exist such as Spring and PicoContainer.
3rd Sidebar
The Abstract Factory Pattern
The intent of the Abstract Factory Pattern is to provide an interface for creating families of related or dependent objects without specifying their concrete classes. The benefits of this pattern are that it isolates clients from concrete implementation classes, makes exchanging product families easy, and enforces the use of products from only one family.
4th Sidebar
Aspect-Oriented Programming
Aspect-Oriented Programming (AOP) attempts to aid programmers in the separation of concerns, specifically cross-cutting concerns, as an advance in modularization. The idea is to encapsulate concerns into separate features and minimize their functional overlaps as much as possible.
Compared to procedural programming, OOP methodologies take a significant step towards separation of concerns.But there are concerns when OOP fails to separate, one of which is a concern that cuts across many modules of an application (hence the name cross-cutting concerns).
The following definitions are based on Wikipedia (www.wikipedia.org/).
Advice: describes a certain function, method, or procedure that is to be applied at a given join point of a program.
Join point: a point in the flow of a program.
Pointcut: a set of join points. Whenever the program execution reaches one of the join points described in the pointcut, the advice associated with the pointcut is executed.
Java is full of frameworks, AOP included: : AspectJ, Spring, JBossAOP, etc.