http://www.developer.com/

Back to article

Dissection of an Application Frameworks, Part 2


May 24, 2005

[Editor's Note: This piece picks up where the article Dissection of an Application Frameworks left off.]

The Inheritance Approach

The inheritance approach is driven by two important object-oriented concepts:

  • Hook methods
  • Template methods

Hook Methods

A hook method is a placeholder that will be filled by the application-specific logic. It is the manifestation of a hot spot concept in an actual class. The terms "hook method" and "hot spot" are interchangeable in many publications. However, a hot spot doesn't necessarily take the form of a method; it can also be in the form of a class or application, although the method form is the most common manifestation of a hot spot. Although a hook method can also appear in a concrete class (as we will see later in this section), it often appears in the form of an abstract method inside an abstract class.

An abstract class, by definition, is a class that contains abstract methods. An abstract method defined in the abstract class doesn't contain its method implementation. In order to use an abstract class, a class must inherit from the abstract class and implement the abstract methods in the parent class. Because of this feature, the derived class has the opportunity to inject some customized logic to make it behave according to some specific business requirement. The following example shows how abstract methods work:

public abstract class BasicBusiness{   protected float income;   //the template method   public void ReportTax()   {      float sTax = CalculateStateTax();      float fTax = CalculateFedTax();      bool ok = CheckBankBalance(sTax + fTax);      if (!ok)      {         FileBankruptcy();      }      else      {         SendMoneyToGov(sTax + fTax);      }   }   protected abstract float CalculateStateTax();   protected abstract float CalculateFedTax();}

The BasicBusiness class is an abstract class containing three methods: ReportTax, CalculateStateTax, and CalculateFedTax; the latter two are abstract methods. BasicBusiness is used to report income tax to state and federal government, and it contains the fictitious business-domain knowledge about the action taken at the time of tax filing. ReportTax determines whether the bank balance can cover the total tax. If the balance can cover it, a check will be sent out to the government; otherwise, the account holder will file for bankruptcy.

The ReportTax method depends on two pieces of information in order to determine whether to pay the tax or file for bankruptcy: the tax amount to be paid to the state government and the tax amount to be paid to the federal government. The creators of the BasicBusiness component have no knowledge of the tax law that applies to each situation. For example, depending on the location and type of business, different tax brackets may apply. Since BasicBusiness doesn't know how to calculate the tax amount, it will leave such tasks to someone who knows. The two abstract methods will act as placeholders to be filled later with code that generates the final tax amounts. Although CalculateStateTax and CalculateFedTax are abstract methods, they can be called just like regular methods. As you can see in the previous sample, the CheckBankBalance method takes the return values of two abstract methods as its parameters even though the two abstract methods have not yet been implemented.

Of course, someone has to implement the CalculateStateTax and CalculateFedTax abstract methods before the ReportTax method can provide any meaningful functionality. In fact, because BasicBusiness contains the abstract methods, it can't be used by instantiating it. Instead, you will need to create a concrete class that derives from the BasicBusiness class and implements its two abstract methods. The following example shows a class that extends BasicBusiness and implements its two abstract methods:

public class NewYorkBusiness : BasicBusiness{   //implementation of abstract method   protected override float CalculateStateTax()   {      return income * 0.1F;   }   //implementation of abstract method   protected override float CalculateFedTax()   {      return income * 0.2F;   }}

NewYorkBusiness is a concrete class that provides the tax calculation methods for New York State. With the custom implementation in the NewYorkBusiness class, ReportTax defined in BasicBusiness can perform some meaningful actions, as shown in the next example:

BasicBusiness nyBusiness = new NewYorkBusiness();//ReportTax now will use the tax calculation algorithm defined for//New York.nyBusiness.ReportTax();BasicBusiness caBusiness = new CaliforniaBusiness();//ReportTax now will use the tax calculation algorithm defined for//California.caBusiness.ReportTax();

Abstract methods are a very powerful concept in both application development and framework development. Imagine that the BasicBusiness class is a framework component and the NewYorkBusiness is an application component. BasicBusiness leaves the hot spots (CalculateStateTax and CalculateFedTax) for the application to fill. Each application will fill those spots according to its specific requirements.

Although the application provides the implementation for the abstract method, it often doesn't invoke the abstract methods directly. Abstract methods are often invoked through the template method.

Template Methods

A template method, one of the GOF design patterns, describes a skeleton or process flow for certain operations rather than prescribing how each operation is carried out. In the example of our BasicBusiness component, ReportTax is a template method. The ReportTax method describes what steps are involved in reporting tax, but it does not describe how every step is performed, since some of the methods it references haven't been implemented. The template method emphasizes how the coordination between different objects/methods is carried out. In framework development, template methods contain the business-domain knowledge on how different methods should work together, whereas abstract methods provide a means of custom method implementation that is referenced in the template method. It is important to realize that template methods and hook methods embody two different concepts; Figure 5 illustrates the relationship between an abstract method (or hook method) and a template method in a framework.



Click here for a larger image.

Figure 5. Template method and abstract method

The left side of the figure represents a framework component, which contains the concrete template method that contains the business expertise and knowledge for a specific business domain. The framework component also has a number of hot spots, represented by the abstract methods. The arrows that point from the template method to the abstract methods indicate that the template method calls several abstract methods for their application-specific behaviors.

On the right side of the figure, an application component extends the framework component and implements the abstract methods in the framework component with the application-specific business logic and knowledge. Because the application framework inherits the functionalities of its framework component parent, its overridden methods (or hook methods) and the template method in its parent class together should be able to provide a combination of business-domain expertise/knowledge and application-level business logics.

Hot spots can be enabled not only through the hook and template methods as we just saw, but also through the concept of pluggable objects in a composition approach.

Note: A term has been used in several technical papers to describe this framework approach as the "Hollywood principle" or the "don't call me, I'll call you principle." Such terms describe the characteristic that a template method in the framework calls the application code instead of the application code calling the framework. However, this terminology is somewhat misleading. The reason such terms are used to describe a framework is that they stress that it is the template method in the framework that calls the implemented abstract methods and controls the process flow in the application. In most cases, the application always calls the framework. Although it may appear that a template method is calling the application code, it is the application code that first invokes the template method.

The Composition Approach

The inheritance approach is a simple approach to enabling hot spots inside a framework, but because developers must know what data and methods are available inside the parent class as well as their interrelations in order to implement the abstract method, they often are required to have a very detailed knowledge of a framework in order to use it.

For example, in the NewYorkBusiness class, as easy as the implementation of CalculateStateTax and CalculateFedTax may seem, in order to implement these two methods, the developer must know that there is a protected float variable called income and that its value must have been set somewhere else prior to invoking either of the CalculateXxxTax methods. Also, exposing the internal details of the child class diminishes the encapsulation of the parent class, which can lead to the uncontrolled access and modification of the class's internal state by developers that are beyond the intent of parent class's designer. The following code segment shows the filling of the "hot spot" in an inheritance approach:

public class NewYorkBusiness : BasicBusiness{   protected override float CalculateStateTax()   {      return income * 0.1F;   }   protected override float CalculateFedTax()   {      return income * 0.2F;   }}

As you can imagine, as the method grows more complicated, developers may need to reference more data and methods inside the parent class and understand what the consequences are of setting the values of such data and calling such methods to change the overall state of the object.

Requiring developers to know such detailed information about the parent class stretches their learning curve for the framework and is burdensome to developers using the framework. One way to keep developers from having to learn the internal details of framework components is to define the hot spots as a set of interfaces that are well connected with the framework. Developers can create components by implementing such interfaces, which can then be plugged into the framework to customize its behaviors.

Pluggable Components

To enable hot spots through pluggable components, the application framework must first define the interface for the hot spot. An interface describes a set of methods a class must implement to be considered compatible with the interface. The interface describes what the methods inside the class look like, such as the method names, the number of parameters, and the parameter types, but not how they should be implemented. The following is an example of an interface definition in C#:

public interface ICalculateTax{   float CalculateStateTax();   float CalculateFedTax();   float Income   {      get;set;   }}

You can then create application components that support ICalculateTax by creating a concrete class and implementing every method/property defined in the interface, such as the NewYorkBusiness class shown as follows:

public class NewYorkBusiness : ICalculateTax{   private float income;   public float Income   {      get { return income; }      set { income = value; }   }   public float CalculateStateTax()   {      return income * 0.1F;   }   public float CalculateFedTax()   {      return income * 0.2F;   }}

Because NewYorkBusiness implements the ICalculateTax interface, it now becomes compatible with or pluggable to any hot spot in the framework that can work with the ICalculateTax interface. With the help of interfaces, application developers can compose the custom behaviors into the underlying framework by loading the pluggable application components in the hot spots of the framework. Figure 6 illustrates how the composition approach enables the hot spots in the application framework.



Click here for a larger image.

Figure 6. A pluggable application component

In using the composition approach to enable the hot spot, developers will need to create pluggable application components that have matching interfaces with the hot spot in the framework. The developer can then plug the component into the hot spot by binding the application component and the framework component together.

This composition approach for enabling hot spots is based on yet another GOF design pattern called "strategy." You will learn more about the strategy pattern in Chapter 5.

Now let's convert the tax example so that the hot spot is enabled through the composition approach. First, we need to modify the BasicBusiness component. Instead of making it an abstract class, this time we will make it a concrete class. The following code snippet shows the new BasicBusiness component:

public class BasicBusiness{   public void ReportTax (ICalculateTax calculator)   {      float sTax = calculator.CalculateStateTax();      float fTax = calculator.CalculateFedTax();      bool ok = CheckBankBalance(sTax +fTax);      if (!ok)      {         FileBankruptcy();      }      else      {         SendMoneyToGov(sTax + fTax);      }   }}

The ReportTax method now takes an input parameter of ICalculateTax type. This input parameter will provide the custom tax calculation mechanism, which is also a hot spot in the BasicBusiness framework component. As you can see, by plugging in the custom application component, we effectively "fill up" the hot spot with application-specific business logic/knowledge.

The following example shows how we can plug the application component into the framework from the application code:

ICalculateTax nyBusiness = new NewYorkBusiness();ICalculateTax caBusiness = new CaliforniaBusiness();BasicBusiness basic= new BasicBusiness();basic.ReportTax(nyBusiness);basic.ReportTax(caBusiness);

In the previous example, each ReportTax call will result in a different outcome, since the framework component is bound to a different ICalculateTax component on each call.

In order for the framework component to load the pluggable object, you can either use the approach described in the previous example or store the application component's type information inside a configuration file, then load the appropriate component through reflection and plug it right into the framework component dynamically.

Identifying and enabling common spots and hot spots are the central themes of application framework development. Depending on how you enable such spots, you create either a white-box framework, a black-box framework, or a gray-box framework.

White-Box Frameworks

A white-box framework is a framework that consists of abstract classes. Adapting a white-box framework requires developers to create concrete classes that inherit the abstract classes in the framework. White-box frameworks take on the inheritance approach to enable their hot spots. Figure 7 shows a white-box framework.

Figure 7. A white-box framework

A white-box framework is relatively easy to develop. You can start developing the abstract class by looking at some of the similar applications you have developed before, identifying their hot spots, and making them the abstract methods. When developing white-box frameworks, you are making an assumption about the pattern of process flow involved in each framework component through the template method. You often base these assumptions on business-domain expertise and prior business-application development experience. As developers start adapting the white-box framework, they need to program only a small number of "override" methods in the derived class and don't have to worry about the overall process flow or how the abstract method is used inside the framework. This allows developers to focus solely on the abstract method without worrying about how the methods they are overriding relate to the rest of the framework.

Of course, there is a tradeoff in almost everything. White-box frameworks are very easy to design and develop, but they have their drawbacks. The first drawback is their inflexibility. In a white-box framework, when you determine how the process flow occurs inside a component through the template method you effectively hard-code the process flow and coordination logic in the component. Although developers who adapt the component may change the logic of the component's hot spot, the overall process flow is nevertheless fixed. This "hard-coded" process flow is reflected as inflexibility when a change in business rules triggers a change in the process flow in the component. Because the process flow and coordination logic are fixed, you would have to update the existing component or write a new one that carries the process flow and coordination logic.

Another drawback of a white-box framework is that it often requires the developer to know many implementation details of the framework component. As the developer is implementing the abstract method in the framework component, he or she often needs to reference the abstract class's methods and variables in the implementation code. This makes the understanding of internal details of the framework component an important prerequisite for correctly adapting the component.

A black-box framework often takes a composition-style approach to solving some of the challenges of the white box, but it has its own share of drawbacks.

Black-Box Frameworks

Black-box frameworks consist of concrete and ready-to-use classes and services. Although developers can extend the existing framework components to achieve customization in a black-box framework, they more often adapt the framework by combining a number of components to create the desired result. A black-box framework may contain many common spots, and it employs the composition approach to enable its hot spots. Figure 8 illustrates a black-box framework.

Figure 8. A black-box framework

Because of the composition approach in a black-box framework, it provides a greater range of flexibility than that of a white-box framework. Developers can pick and choose different components to achieve specific application requirements with infinite possibilities. Unlike white-box frameworks, where a developer often needs to know the detailed implementation of the framework component for adaptation, black-box frameworks consist of components that hide their internal implementation. Adaptation of such components is done through well-defined interfaces, such as certain public methods and properties. Developers need to be familiar with only these public members in order to use the framework.

Compared with white-box frameworks, black-box frameworks are harder to develop. Encapsulating business-domain expertise into components that are generic enough to be used in many application scenarios is not an easy task. Encapsulating too much will lead the domain expertise inside the component becoming less fit in many application scenarios. Encapsulating too little will lead to developers having to work with a large number of components and more complex coordination logic in order to build the application.

The extra flexibility of black-box frameworks doesn't come for free. When using a black-box framework, developers must implement their own process flow and coordination logic needed to link multiple components together. Because developers now control how and what components need to work together, they are responsible for the extra workload on "wiring" the components together along with the extra flexibility provided by the black-box framework. In contrast, white-box frameworks automatically handle the "wiring" for you in the template methods of their components.

Although developers don't have to deal with learning the internal implementation of the abstract class as they do with a white-box framework, they do have to be familiar with a greater number of components and their use when using a black-box framework, since the developer now has more "moving" parts to deal with in order to combine them into something they need.

When developing an application framework, there is no requirement that the framework contain either all abstract classes or all concrete classes. In fact, neither pure white-box nor black-box frameworks are practical in the real world. Having a mix of both the inheritance approach and composition approach gives you the freedom to use whatever approach is best for the design of a particular component. By mixing white-box frameworks and black-box frameworks, you effectivelycreate a gray-box framework.

Gray-Box Frameworks

Gray-box frameworks take both inheritance and composition approach, is usually made up with combination of abstract classes and concrete classes. The approach of enabling its common spots and hot spots is determined on a component-by-component basis. Figure 9 shows a gray-box framework.

Note: Because a concrete class that contains virtual methods can either be used directly or inherited by another class that overrides the virtual methods to alter its behavior, it is possible for a gray-box framework to consist of only concrete classes, as long as the approach taken by the classes is a combination of composition and inheritance.

In the real world, the application frameworks you will be developing will most likely be gray-box frameworks. The decision whether a given components will follow the inheritance approach or composition approach is decided by which approach is best suited for what the component is trying to accomplish and how developers will likely use the component in their business applications.

As we are choosing the inheritance approach, the composition approach, or a mix of the two, we should consistently keep in mind the tradeoffs and implications for performance, maintenance, and usability with each approach.

Figure 9. A gray-box framework

From the performance point of view, the composition approach tends to beslower than the inheritance approach, primarily because of the extra components it has to load and access at run time to produce the desired results. As you are gathering the features from multiple components, you may also add a number of extra calls to bridge different components. In the inheritance approach, however, an inherited class often contains most of the required features within itself, hence reducing the number of objects the program needs to create and access to produce the same results and eliminating as well much of the extra code that would be needed to bridge different components if the composition approach were used.

Maintenance is another area in which we see the tradeoffs in both approaches. In the composition approach, developers work with a set of highly decoupled framework components, which makes their application more adaptive to changes, and hence more flexible and extensible. However, after the application is deployed, those who provide postproduction support will have to deal with many more "moving parts," which leads to extra maintenance effort. For example, a change in a business requirement may result in the modification of framework components. With a composition approach, such requirement changes may potentially affect a series of framework components that participate in a certain business feature, since the business requirement is supported by the collaboration of a number of components. Such changes in multiple components may also multiply the effort in testing and deployment of the application framework. On the other hand, the inheritance approach may be less flexible than the composition approach, but in compensation, it introduces fewer moving parts. When business features are served by a hierarchy of classes, a change in business requirements can often be resolved with changes to far fewer classes on the hierarchical tree. Of course, the real maintenance cost of your application framework depends on the design of the framework as well as the type of business-requirement changes involved. But generally speaking, you have less overhead on maintenance if you have fewer moving parts to deal with.

Usability is yet another area you need to consider in designing the application framework. The framework component that takes on the inheritance approach usually hides the complex coordination logic and process flow inside the parent class or abstract class, so the developer often needs only to implement or override a few methods to achieve the desired result. Hence, inheritance provides very good usability as long as developers aren't required to learn overwhelming details of the parent class or abstract class they inherit. Usability for the composition approach, on the other hand, depends considerably on how much composition developers have to support to achieve certain results. Having a set of highly decoupled components often requires developers to learn and code their own coordination logic and process flow to wire such components together to produce the desired results. However, if you are willing to sacrifice flexibility and create a few coarse-grained components so that developers need to work with only a small number of components to get what they want, then the framework will become easier to use and developers will be much more productive, since the composition approach significantly reduces the coordination logic developers have to learn and write.

As you approach many framework design decisions, you must keep in mind that there is no silver bullet. There is often a tradeoff between different approaches. It is your job to decide how to balance them and create an application framework that fits your objectives.

Design Patterns

As you are architecting and developing the application framework, you will often run into design challenges on recurring scenarios, such as how to improve handling of changes to the process flow and how to improve application-specific customization. Design patterns, which describe the solution to common software development problems, can assist you in solving some of these common problems in developing an application framework. Many commonly used design patterns are documented in the classic book Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, the "gang of four" (GOF). Some design patterns are especially useful in application framework development. The following list describes some of these patterns and the problems they can solve:

Strategy: a design that handles the variation of algorithms in the application. It allows the developer to customize the framework by "plug and play"-ing different application-specific algorithms.

Bridge: a design that decouples the abstraction and implementation in the application. It allows developers to provide different implementations for part of the application without affecting other parts of the application.

Decorator: a design that provides a layer approach in processing data. It allows developers to easily assemble multiple components to process data.

Observer: a design that provides a publishsubscribe communication model. It allows developers to disperse information easily to multiple objects.

Mediator: a design that keeps objects from referring to each other explicitly. It allows developers to create loosely coupled communication between different objects.

Template method: a design that provides the skeleton of the algorithm it oper-ates. It allows developers to define process flow and coordination logic without having to define how the algorithm is implemented.

Visitor: a design that lets you define a new operation without changing the existing ones. It allows developers to decouple an operation from the coordination logic that is constantly changing.

Singleton: a design that ensures that only one instance of the class is created. It allows developers to have better control of the creation of the object.

Abstract factory: a design that provides an interface for creating families of objects without specifying their concrete classes. It allows developers to reduce the reference to concrete classes throughout the application, and hence reduce the amount of code changed when the concrete classes change.

For the rest of the book, we will look at how these design patterns can help us develop our application framework and how these patterns are implemented in .NET.

Summary

In this chapter, you have learned about processes and techniques of application framework development. We first looked at the different layers that make up the application framework and how each layer is related to the others. Then we looked at the framework development process, which involves analysis, design, development, and stabilization stages in an iterative fashion and specific tasks involved in each of these stages. Following that, you learned about the several approaches in framework development, such as white-box, black-box, and gray-box frameworks. We also looked at some key framework development techniques through discussion of common-spot, hot-spot, and design patterns.

About the Author

Xin Chen is the founder of Xtremework, Inc. Since the inception of .NET, Xin Chen has helped customers in a wide range of industries turn their business ideas into software products using .NET technology. Leveraging his expertise in .NET and EAI, Xin Chen has also worked with several technology leaders, such as Microsoft and Accenture, to bring winning solutions to their customers. When not working overtime, Xin enjoys reading books, writing books, and resting. He is the author of BizTalk 2002 Design and Implementation (Apress, 2003). Xin Chen earned a master's degree in statistics from Columbia University and is currently living in New Jersey.

About the Book

Developing Application Frameworks in .NET by Xin Chen

Published April 2004, Softbound, 392 pages
Published by Apress
ISBN 1-59059-288-3
Retail price: $49.99
This material is from Chapter 2 of the book.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date