A Practical Alternative to Business Objects
An Experience Report
February, 1997
Daniel S. Halbach
Pareto Associates, Inc.
www.pareto.com
dsh_author@pareto.com
and
Eric C. Newton
MetaSlash Inc.
www.metaslash.com
ecn@metaslash.com
Abstract
Like everyone who writes Information Systems for a living, we look for ways
to improve productivity, to increase reuse, and to reduce the costly effects
of changes in customer requirements. For years we have applied main stream
approaches, such as commercial Graphical User Interface (GUI) frameworks,
Model-View-Controller architectures, and Business Objects, to achieve these
goals. Much to our disappointment, we have found these approaches fall short
of their promise. This report describes the drawbacks we have encountered
and presents an alternative architecture more appropriate for these systems.
|
Context
We write Information Systems: GUI clients, relational data base servers,
paper report generation, and the occasional complex computation (e.g. activity
scheduling or loan calculation). Our customers are rarely entire enterprises;
they are typically single departments within the overall enterprises. As such,
they are subject to external constraints on time and funding and to mandated
business practices which change in response to changes in the economy, government
policy, etc. This environment produces two distinct differences between our
typical projects and textbook Information Systems development.
-
In the textbook case, the requirements are fully defined and remain
constant throughout development, whereas in our experience, we rarely have
that luxury. We shoot at moving targets.
-
In the textbook problem, choosing the appropriate operating system,
language, and development tools is the province of the problem solver. In
practice, these are often dictated by the customer, who must follow
enterprise-wide policy, coexist with legacy database servers, and make use
of their investment in existing client hardware.
|
Problem
After developing dozens of Information System (IS) applications and despite
the use of well-received Object-Oriented Design and Development techniques
(see Existing Approaches), we still find the following things difficult
to obtain or achieve:
-
Prototype code that matures into the final application. Typically, the
requirements change significantly while prototyping or soon after delivery,
making it easier to start over than to salvage the prototype code.
-
A development process that works reasonably well with different languages,
GUI tools, databases, etc.
-
Application code that can easily accommodate changes requested late in
development. We find it costly to make changes that the users believe to
be simple or straight forward, including the addition of a field to a form,
a column to a database table, or permissions to control access to features
of the application. A small change has a ripple effect throughout the
application, requiring changes to several project modules.
-
Large scale code reuse within and across domains. Even though each IS application
we write is in many ways similar to at least one other application we wrote
before, reusing existing code is difficult at best because every form, callback,
and database access function is parameterized by attributes specific to the
individual application.
As an example, consider the following scenario encountered in almost all
information systems. It is an abstract process for editing an entry stored
in a database table.
-
Get the list of available items from the database. (Typically, you only retrieve
key fields.)
-
Format each item for display.
-
Display the list and allow the user to choose the desired entry.
-
Use the key information for the chosen entry to retrieve the entire data
item.
-
Decompose the record to populate the fields on a form that is displayed to
the user.
-
Validate changes made by the user.
-
Display an error message if changes are invalid.
-
Update the database with new values.
Granted, this has been oversimplified for the sake of example, but anyone
who writes IS applications has coded this or something like it dozens of
times. The reason this description is so universal is that it does not say
anything specific about the actual table layouts, data attributes, or controls
on the forms. In other words, it is abstracted away from the details of a
specific domain. We ought to be able to reuse the code we write to perform
the process above, but this is hard precisely because a real system cannot
ignore the detailed attributes of the domain data.
In summary, we continue to look for better ways:
-
to capture the parts of the system that are stable or reusable
-
to avoid being derailed by every change in requirements
-
to be productive regardless of the GUI, database, and development tools dictated
by the customer
|
Existing Approaches
We did not start out by trying to invent our own solution to these problems.
We faithfully followed the state of the art through a progression of three
approaches. While each has its strengths and promise, each fell short of
our hopes and expectations for reasons described below.
1. Vendor Frameworks
These include Microsoft Foundation Classes (MFC), Visual Basic, PowerBuilder,
and Delphi.
The problem with these frameworks was that everything becomes GUI-centric.
Design is driven by screen layout. Activity flow definition is broken up
and distributed across numerous callbacks. Database management code consists
of function calls littered throughout GUI event handlers. Accessing
application-specific data inside callbacks typically requires type casting,
making the code very difficult to reuse.
However, these tools do automate screen layout and callback declaration nicely.
We continued to look for a solution that could take advantage of these
capabilities.
2. Model-View-Controller (MVC) Approaches
These approaches include the original Smalltalk Model-View-Controller framework,
Microsoft's Document-View and other implementation of the Observer pattern
[Gamma 95].
Although this scheme works well in small, focused cases, it has not scaled
well for us. Using this approach across the entirety of a non-trivial application
results either in a large, monolithic model or a hierarchy of smaller models.
With the single, large model we tended to lose the benefits of the model-view
interaction, since there was no direct support for handling internal interactions
within the model. The hierarchy of smaller models handled the interaction
better, but it required us to maintain a parallel hierarchy of views to match.
With each model potentially having multiple views and views potentially merging
information from multiple model components, the number of connections and
interactions grew geometrically. Simple conceptual changes to the hierarchies
became prohibitively difficult to implement, due to the difficulty in tracing
possible side effects throughout the numerous interconnections of model and
view components. In one application, for example, adding one attribute to
a model object required changes to eight separate C++ source files and the
recompilation of dozens of related files, due to the ripple effect between
interrelated model objects and their associated views.
In a recent article [Vlissides 96], John
Vlissides describes an approach to this problem that employs the Visitor
pattern to produce the view hierarchy dynamically while traversing the subject
hierarchy. Although our solution to the problem does not use the Visitor
pattern per se, there are important similarities that are discussed later
in this report.
3. Business Objects.
Business Objects are usually defined as domain-specific objects that handle
some aspect of a business process or category of business information
[Casanave 96, Johnson 96]. They are intended
to be smart agents with guarded state and guaranteed behavior. Often they
are promoted as the components of a middle tier between the data repository
and the end-user applications. In many ways they are seen as the evolution
of the MVC approach, where the Business Objects are stable, reusable models
and user applications are the evolved views.
Our problem with Business Objects stems from an inability to re-engineer
our customers' businesses. At a minimum, creating useful Business Objects
requires user consensus. The lack of a stable consensus has been the heart
of our problem in the first place. Even obvious candidates (e.g. a Person
object) are difficult to share. Factions within the customer organization
must decide which faction maintains the data definition, who controls the
data, and how to share the development and maintenance costs. In one project
where we developed logistics applications for six groups within one organization,
four groups had the notion of person, three defined a contract,
three dealt with funds data, and four had aircraft entities.
In theory, we could have reduced these fourteen objects to only four reused
business objects, but the six groups were unable or unwilling to coordinate
their requirements or share development and maintenance costs. Thus, each
project followed its own course and no reuse was achieved.
A second problem with Business Objects stems from the fact that interesting
behavior in most of our applications is supplied by the users themselves in the
form of decisions about how to modify the data. The application basically
provides controlled access to concurrent users and checks the consistency
of the users' edits. Thus, behavior is reduced to input validation, relational
integrity checking, record locking, and permission enforcement. These functions
are well supported -- and usually must be supported -- by GUI and database
constructs, leaving little else for the Business Objects to implement:
-
Users expect to see GUI controls tailored to the type of data they edit (e.g.
date-specific editors instead of generic text boxes, and option menus or
combo boxes instead of text boxes when the list of allowable values is predefined
and finite). The user also expects invalid entries to produce immediate visual
responses, which is also the province of the GUI. Thus, validation logic
cannot be fully decoupled from the GUI.
-
Relational integrity and permissions must be enforced at the database level
since it is the common point of access. Putting these responsibilities on
newly defined Business Objects requires existing applications to be re-engineered
and disallows ad hoc access to the database. These options typically do not
exist for our customers.
Thus, the Business Objects are reduced to object wrappers around
business-specific data structures. In fact, referring to them as "objects"
is questionable since it violates one of the prime directives of Object-Oriented
Development: all objects have identity. The data structures really have no
identity of their own but are merely values being passed between the database
and user interface. We have found that artificially assuming object-like
identity for the data is a significant part of the problem since it
unsuccessfully tries to give an atomic appearance to what is inherently
distributed logic. There has been no escaping the fact that data attributes
are redundantly defined by the layouts of database tables and GUI forms or
that changing access rights in the database affects form-to-form interactions
in the GUI. Managing the ripple effect caused by a seemingly small change
in requirements is made even more difficult by artificially object-izing
this distributed logic.
To summarized, whether it is from practical necessity or specific direction
from the customer, the database server must house the business logic for
maintaining the access rights and integrity of the shared data, while the
GUI forms and controls handle much of the validation logic and interactive
responses that result from violating that logic. We are given little opportunity
to re-engineer organizations at the level where introducing Business Objects
as a middle tier is value added. As a result, most of the non-GUI and
non-database code exists: 1) to glue the database to the GUI, 2) to pass
application specific data structures, and 3) to implement application flow
by hooking screens together inside callback code. This is not a situation
that we have been pleased with or proud of. Opportunities for reuse have
not been what we had hoped and we have spent considerable time writing the
same sort of brittle, error-prone code over and over again. This inspired
our efforts to define an alternative approach.
|
An Alternative Approach
In developing an alternative approach to writing Information Systems, we
have attempted to borrow the best and most appropriate pieces of existing
architectural styles. The key aspects of our design are:
-
It is based on a pipes and filters architecture
[Shaw 95], also referred to as activity
flow [Lea 97]. Systems are composed
of a graph of interconnected nodes, where each node transforms the data it
receives on its input connections and passes the resulting data along its
output connections.
-
All active objects in the architecture are nodes conforming to a common protocol.
This includes database proxy nodes for storing and retrieving data and GUI
proxy nodes that transform screen control states into data transportable
within the network.
-
The role of each node in the system is defined independently of any
application-specific data. This is made possible via the existence of
ports and profiles. Each node has one or more input and output
ports. Any output port can be connected to any input port in exactly the
same way, regardless of the data that flows between the ports. Profiles (provided
by each output port) describe the structure of the data that flows through
each port.
-
Conceptually, the one and only event type defined in our system is the notification
of new data available from an output port. Nodes are activated (i.e. they perform their
transformations) in response to receiving these notifications. This response may not
be immediate, however. For example, a proxy node for a GUI form will display
the form when it receives data to be edited. Output from the proxy node will
not occur until the user hits the OK button. To down stream nodes, this is
simply seen as a delay in the response of that node. GUI events are entirely
encapsulated within the node.
-
The data that flows between ports is treated like the raw copy that it truly
is. It has no behavior of its own but is acted upon by the real "business
objects" in the system, i.e. the transforming nodes. The data lives just
long enough to be consumed by the receiving node.
-
The meta-data provided by profiles allows many general purpose nodes to be
coded once and become completely reusable. The flattener node is a
good example of this. It is frequently necessary for a record (i.e. a stack
of attributes) to be flattened into a single string for presentation
to the user. The flattener node is configured by parameters that correspond
to the order and format of the fields desired in the flattened string. The
profile provides the information necessary about the incoming data for the
flattener to perform its task as defined by its parameters. The combination
of the configuration parameters and the profile are all that is needed to
make a single flattener type completely reusable.
|
Benefits
The benefits of this approach are:
-
Because all nodes conform to the same uniform mechanisms for composition
and activation, it is easy to disassemble and reassemble the nodes at the
source code level in response to a change in requirements. In other words,
the act of coding is less cumbersome, time consuming, and error prone. The
resulting code is also easier to comprehend since the code focuses on node
composition which in turn directly defines activity flow.
-
It works in conjunction with existing GUI and database tools. We use these
vendor tools to do what they do best and wrap that code into nodes for use
in our applications.
-
The approach is language independent. Most popular languages are powerful
enough to define the required node types. This is even true of languages,
such as Visual Basic, that do not support inheritance (although this feature
is sorely missed).
-
Activity flow is supported by first class objects. It is no longer
necessary to trace through callback code to see how one screen in connected
to the next or to see what contextual data is required to show a form or
validate edited data. These are overtly defined by a node's connections on
its input and output ports and the associated profiles.
-
It is more feasible to abstract (and therefore reuse) use cases across domains.
The example given previously of editing a database record provides a good
example of this. Each of the eight steps corresponds to a node as shown in
Figure 1. The composition of nodes provides a generic, reusable network for
a simple get/edit/save operation.
As with the description of this example problem, the graph in Figure 1 is
somewhat oversimplified for the sake of example. Nonetheless, in practice,
composing nodes to create the solution is quite simple - simpler, in fact,
than drawing the diagram. Figure 2 shows C++ source for implementing Figure
1 for simple Person information. This approach provides a number of
factors that increase reuse and maintainability:
-
The role of each node in the system is independent of its internal implementation.
For example, the list items can be reformatted simply by changing the
configuration parameters of the flattener. In fact, any node that can perform
the flattening function can be used in its place.
-
Each node is completely independent in its role. For example, if additional
validation checks become necessary, additional Validator nodes can be inserted
between the original Validator and the Record Injector. These two original
nodes will continue to function as before with no side effects from the inclusion
of new Validator nodes.
-
Any activity for which there is a finite set of generally accepted
implementations is a candidate for reuse. Validators are, again, a good example.
There are common checks made on user input, including: range checking, date
formatting, uniqueness within a set, etc. Each of these can be coded once
into a properly parameterized node that is reusable whenever that type of
validation is required.
-
Many other nodes, such as the Finder and the Record Injector, can be defined
generically so that they work for any data profile, although these are more
likely to be specific to the underlying GUI or database tools. Though more
complicated, the Edit Form node can be made generic through the judicious
use of naming conventions that allow data record elements to be associated
with controls on the underlying GUI form. Obviously, achieving this reuse
depends on the scope of services provided by the underlying GUI.
|
(Click here for a text-only version of Figure 1)
|
#include <Nodes.h>
#include <OraWrap.h>
#include <XWrap.h>
int main()
{
//Declare a database connection...
OracleDatabase PersonDatabase("scott/tiger");
//Declare a GUI application...
XApplication PersonApp("Person");
//..."Person" is a GUI resource ID.
//Declare nodes...
ListSource PersonListSrc(&PersonDatabase);
ListFlattener PersonFlattener;
Finder PersonFinder(&PersonApp);
RecSource PersonRecSrc(&PersonDatabase);
EditForm PersonForm(&PersonApp);
UniqueValidator PersonNameValidator(&PersonDatabase);
MsgWindow PersonMessage(&PersonApp);
RecInjector PersonInjector(&PersonDatabase);
//Connect to a live database...
PersonDatabase.Connect();
//Parameterize nodes...
PersonListSrc.From("PERSON");
PersonListSrc.Sort(SORT_ASCEND);
PersonListSrc << "LAST_NAME"
<< "FIRST_NAME"
<< "SSN";
PersonFlattener << Flattribute("LAST_NAME",20)
<< Flattribute("FIRST_NAME",15)
<< Flattribute("SSN",0);
PersonFinder.Title("Person Records");
PersonRecSrc.From("PERSON");
PersonRecSrc << "SSN";
PersonNameValidator.From("PERSON");
PersonNameValidator << "LAST_NAME"
<< "FIRST_NAME";
PersonMessage.Message("You must enter a unique name...");
PersonInjector.Into("PERSON");
//Attach outputs to inputs...
PersonListSrc >> PersonFlattener;
PersonFlattener >> PersonFinder;
PersonFinder >> PersonRecSrc;
PersonRecSrc >> PersonForm;
PersonForm >> PersonNameValidator;
PersonNameValidator.Error >> PersonMessage;
PersonNameValidator.OK >> PersonInjector;
PersonMessage >> PersonForm.Error;
PersonInjector >> PersonListSrc;
//...Feedback to repopulate finder
//Start the flow...
PersonListSrc.Pump();
//Start the GUI Event Loop...
PersonApp.Start();
//Shutdown the database after returning from event loop...
PersonDatabase.Disconnect();
}
Figure 2. C++ Implementation of the graph in Figure 1.
|
Comparisons and Contrasts
This approach has similarities to the other existing approaches; however,
there are important distinctions:
-
This may appear to be a throw back to the traditional notion of data-flow
decomposition, reminiscent of Yourdon or DeMarco. It is true that systems
are defined in terms of data flowing between processing nodes, but the similarity
ends there. In the abstract, our nodes are application-neutral transformers,
not application-specific function calls. Inheritance is used to produce multiple,
concrete implementations conforming to abstract interfaces. The data-flow
emphasis results from the nature of the systems we write, where moving valid
data between the GUI and the database is our main task. The approach is better
described as a means of directly implementing use cases, where the
parameterization of generic nodes produces a specific application scenario
from the generic use case.
-
Although there is automatic notification of changes between upstream and
downstream nodes, this is not an instance of the Observer pattern. In Observer,
the model and view are much more intimately coupled. Our nodes know nothing
of each other but only that data is available on an input port. This decoupling
avoids the "unscalable observer" problem described by Vlissides
[Vlissides 96].
-
Our approach is in direct contrast to the prevailing notion of Business Objects
in that our data is basically dumb and passive. However, as with most Business
Object frameworks, we do rely on the availability of meta-data to facilitate
reuse and composability of components. Our components perform data manipulations
common to business applications but are at a lower implementation level than
business objects are usually meant to dwell. This does not preclude us from
composing basic nodes into higher level composite nodes when opportunities
for reuse at that higher level present themselves within an application or
domain. The code in Figure 2, for example, can be wrapped into a single node
that is reusable across multiple data sources and user interface tools.
-
Although we do not employ the Visitor pattern per se, we intentionally designed
the network of nodes to be visitable. This capability is important
in that it provides a means of combating the ripple effect discussed throughout
this report. By tracing outputs to inputs, nodes can be visited and their
profiles inspected to determine which nodes depend on the existence, type
or format of any data attribute.
|
|
[Casanave 96]
Casanave, Cory B., "What are Business Objects and Why are they Important?",
Data Access Technologies Presentation, OMG Business Object Domain Task Force,
1996.
[Gamma 95]
Gamma, E., R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements
of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995.
[Johnson 96]
Johnson, Ralph E. and Yoshida, Kazuki, "Models of Business Objects: Accounts"
Business Object Workshop II, OOPSLA 1996.
[Lea 97]
Lea, Doug, Concurrent Programming in Java(tm) Design Principles and
Patterns, Corporate and Professional Publishing Group, The Java Series,
ISBN 0-201-69581-2, 1997.
[Shaw 95]
Shaw, Mary, "Patterns for Software Architectures", Appeared in Conf. The
Pattern Languages of Programming, August 1994, In Pattern Languages of
Program Design, ed. J.Coplien, D. Schmidt, Addison-Wesley, pp 453-462,
1995.
[Vlissides 96]
Vlissides, John, "Pattern Hatching: The Trouble with Observer" C++ Report,
September 1996.
|
|
Return to Pareto's main site...
Return to MetaSlash web site...
|