Component-based Design

What is the best approach when designing a new application, be it a small tool or an end-user application?

Build a Prototype?

Many developers jump straight into a prototype, in order to get a feel for how the application will work. While prototypes are good for demonstrations, they are dangerous: in projects with tight time or budget constraints, the temptation to simply "build out" the prototype becomes irresistible. This leads to applications with nice user interfaces (hereafter called UI), but inflexible and difficult-to-follow implementations.

Build Components

A better first step is to list the requirements and assign them to possible components. This doesn't have to be a long or complete evaluation of the requirements; a few minutes is enough to come up with enough ideas to get started coding. These non-UI components are a natural fit for testing environments and are more likely to define a clean, sensible API (Application Programming Interface). Once the core logic has been built and tested, a prototype can easily be built on top of it.

To summarize, the component-based approach is important for the following reasons:

  • It maintains business logic in clearly-defined units
  • It improves testability of business logic
  • It improves portability and reuse
  • It avoids spreading core logic throughout event handlers, which is a common practice in RAD (Rapid Application Development) environments

RAD considered harmful

A good UI library is a wonderful thing, allowing clean-looking, well-integrated applications to be built in a very short time. However, the allure of this style of programming is dangerous, as it quickly leads to applications without a clearly defined API, which leads to extensibility and maintenance issues.

These systems entice programmers into working "backwards", building their application logic around events generated by the UI. The first generation of RAD environments were notorious for mixing UI and business code. The latest generations make use of libraries with "code-behind" built right in, automatically supporting core/UI separation in both web or classic UI application.

This separation of core logic and UI events makes is commonly called the MVC or Model-View-Controller pattern.

What is MVC?

MVC is the official name for the technique described above, in which functionality is contained in a model (M), which communicates state changes to a view (V) through some form of update mechanism. The controller (C) represents user input and applies changes to the model.

In many UI libraries, the view and controller layers are merged, making it much easier to apply the pattern to smaller projects. View components are typically bound to model components using the Observer pattern: the view "listens" for changes in the model and reacts accordingly.

Designing with Components

Consider a tool which processes text files and generates output of some kind (perhaps PDF or CSV). The actual task doesn't matter - this is the kind of tool that is often written in a seat-of-the-pants fashion, with the excuse that it is "faster" to get it done this way. Let's take a component-based approach and see what we get.

What are the components of the system?

  • Transformer - Takes an input, applies one or more actions and generates an output
  • Actions - Performs an operation on data
  • Importers - Readers for various input formats; convert to a format the transformer understands
  • Exporters - Writers for various output formats; converts from the transformer format
  • Plugin Registry - Registration for recognized input and output formats
  • Options/Preferences -Global options for the system

Analyzing the Component-based Design

This list took only a few minutes to write and could have been written by anyone familiar with the project. The list contains only domain knowledge — there is no implementation-specific data. Having written down the requirements, we see that there is a need for an internal data representation, which will be used by the importers, exporters and actions. This is a facet of the design that might have gone unnoticed during prototyping, but would have been expressed implicitly nonetheless.

Is it overdesigned?

The list of features above is not an "over-design", but rather an explicit expression of the specifications. While an implementation can avoid using importer, exporter and action components, these concepts are part of the design nonetheless: an implementation without tehm is simply more difficult to describe, understand and extend.

With a little bit of thought, we have designed a system that will scale to multiple import and export formats and even support multiple transformations. Writing the application in this way may involve marginally more initial work, but will result in a far more testable, extensible and reusable framework, decreasing maintenance and support time.

Does it slow development?

Another popular argument is the perceived reduction in programming efficiency. Applications or tools of the "throwaway" kind will take longer to develop when using a clean programming model. Whereas that may be true in the very short term, the majority of an application's life span is spent in support and maintenance, which takes more time and energy if the application is poorly designed.

Though a throwaway prototype may be available marginally quicker, it will be of poorer quality. In addition, subsequent applications cannot benefit from its code. The biggest loss comes in the form of functionality, improvements or bug fixes which are never even attempted because the code is not in a maintainable or testable state.

How does the UI work?

Realization of this design at the core level is not so difficult. Even though the application initially only has one importer and one exporter, it doesn't take much more to define an API that supports multiple plugins. Writing the tests for these components is likewise trivial. The opposite is true in the UI: building an interface to manage and configure all of the functionality that was easily written into the model is prohibitive.

There is no reason, however, that the UI has to express all of the details of the underlying model; the application, as specified, need only expose enough functionality in the UI to be able to import and export. The UI stays remarkably simple, but can be easily and quickly extended to offer more features, if desired. Since the model has automatic tests, it can be assumed to be stable and it is easier to accurately estimate the time required to build the new GUI elements.

Analyzing the Prototype-based Design

The standard, quick-prototyping approach would have started coding a main form with some input fields, building the transformation code directly into the form itself. Options and preferences would have likely been encapsulated with a few controls on the main form, which, in turn, would have been responsible for loading and storing them.

The design sketched above would be expressed implicitly and partially, at best. An application written without these concepts in mind will not be worth refactoring. If the code is re-used at all, it is typically copied to a new project and modified there, resulting in multiple copies of nearly the same code. Fixes and enhancements to one will not necessarily appear in the other.

A prototype that is considered "throwaway", but grows into an application, does not benefit from any of the following:

  • It is easier to document the clearly defined API of a model; good documentation allows multiple developers to support or upgrade the application
  • Reuse across multiple applications
  • It is far easier to refactor and repurpose model code that lends itself to test-driven development

Extending the Application

It's obvious from the design above that it can be extended to support multiple importers, exporters and actions. The initial application was assumed to be a GUI which did not expose all of the functionality available in the model. The GUI can be made more powerful, exposing more of the underlying functionality. The extensibility of the design is clear. What about reuse?

A command line version

The examples below are in Delphi Pascal.

In a traditional prototype, command-line support is bolted on to the same application,because the required code is buried in UI structures. Such a command line application will involve something like:

Listing 1 - Hacking the GUI Application

if command = 'C' then begin
  { Create the main form first, so it is 
    treated as the main form by the system, then 
    hide both forms so they don't appear in front 
    of the command line. 
  }
  form:= MainForm.Create;
  form.Visible:= False;
  prefsForm:= PrefsForm.Create;
  prefsForm.Visible:= False;
  prefsForm.LoadOptions;
  form.EdtFileToUse.Text:= parameterFromCommandLine;
  form.BtnConvertClick( nil );
  form.Close; // Close main form to quit application
end;

Using the elements of the model from the component-based design, we could build a separate application, whose main loop is logical and readable:

Listing 2 - Logical and readable

if command = 'C' then begin
  options:= ToolOptions.Create;
  options.Load;
  try
    try
      converter:= FileConverter.Create( options );
      converter.Convert( parameterFromCommandLine );
    finally
      FreeAndNil( converter );
    end;
  finally
    FreeAndNil( options );
  end;
end;

The second version addresses the requirements in a much clearer, more maintainable fashion. On top of that, the implementation in the GUI application would have a similar pattern. The code above could go into an event handler, passing text from an input control instead of an argument from the command line. The following code assumes that the converter and options from the command line example above are globally available:

Listing 3 - A clean GUI implementation

procedure MainForm.BtnConvertClick( Sender: Object );
begin
  Converter.Convert( EdtFileToUse.Text );
end;

Conclusions

With a small amount of time invested at the beginning, one can define any application in terms of UI-independent components. An application that was designed in this way lends itself to ready reuse. Applications that use these components need only be concerned with delivering input to a clearly defined API. Fixes and updates to the core components will be reflected in all applications.

updated on 12/18/2017