Designing an NAS Application
By Steve Malmskog
Send comments and questions about this article to View Source.
Click here for printer-friendly version
Whenever a development organization is faced with the task of developing
a large-scale application within a new application framework, it can be
a bit daunting to determine how the software should be laid out architecturally.
Should there be a single tier or multiple tiers? If multiple tiers, how
many, and which software entities should reside in which tier? When developing
your first large-scale software project within Netscape Application Server
(NAS), these are just a few of the questions that surface. Along with the
rest of the Internet Shopping Network
(ISN) development team, I faced these and other similar questions when
designing our First Auction Web
site in early 1997. At that time, there were no other large-scale NAS sites
in existence. The uncharted territory, as we were to quickly discover,
was full of potential pitfalls.
In looking back on our development of the First Auction site and reflecting
on the design choices we made, I've come to realize that many aspects of
the day-to-day development we do now -- particularly how quickly we're
able to deploy new features and enhancements -- are a direct result of
the design decisions we made back then. In this article,
I'll share with you the high-level architectural decisions we made
in developing the First Auction site. The specific areas I'll touch on include:
- breaking NAS into three layers (application, service object, and database)
- creating a class hierarchy for AppLogics
- creating a custom AppLogic base class
- writing your own COM-like objects
While my code examples will be in C++ (and some parts of the discussion
will apply only to C++), the same design can just as easily be created
with the Java language. I'm going to assume that you are familiar with
the basic NAS architecture (NSAPI plug-ins, KJS/KXS/KCS processes, AppLogics,
and so on) and have some experience in software development (although minimal)
within the NAS framework.
INTRODUCING THE THREE LAYERS
One of the first decisions -- and probably the most significant one -- that we made in designing the First Auction site was to break the NAS
business logic into two layers. The general NAS architecture, which is
basically a three-tiered design, ends up looking like the diagram in Figure
1.
Figure 1. General three-tiered architecture using NAS
If we exclude the client and focus on the server side, we end up with
three layers to work with: the application layer, the service object layer,
and the database layer. These three layers comprise the complete scope
within which the Web site's software system exists.
Looking at the NAS model in more detail, we can see how our three layers
fit into a large-scale NAS environment. In general, the NAS environment
is made up of several KJS and KCS processes running on one or more machines.
Requests come to the Web server and are dispatched to a KXS via an NSAPI
plug-in. Each KJS or KCS receives requests from the KXS and, based on the
request, binds the appropriate AppLogic to an available thread in its local
thread pool. The AppLogic is then executed in its own thread (possibly
accessing the database, file system, and so on) and streams HTML back to
the user (usually in the form of a merged template via the EvalTemplate()
call).
Figure 2 shows a high-level view of the server-side architecture with
our three layers intact. The role played by each of these layers is as
follows:
-
Application layer -- Objects (that is, AppLogics) fulfill a particular
application requirement (business logic). They very often correspond one-to-one
with the pages on a Web site.
-
Service object layer -- Objects in this layer fall in one of two
categories: Netscape extensions, which provide either a particular service
or a point of access to the database; or stand-alone (COM or non-COM) objects,
which serve some common utilitarian purpose.
-
Database layer -- This layer includes the data model, triggers,
stored procedures, and so on, as required by the Web site.
Figure 2. Three-layer server-side architecture using NAS
From a software development standpoint, the application layer and service
object layer house the bulk of the business logic and services; we'll focus
our discussion on these two layers.
THE APPLICATION LAYER
Within the application layer resides all the core business logic to
run a Web site. The primary intent of this layer is to provide application-specific
logic -- not generic services to be used by other applications.
Thus, the business logic for the Web site is encapsulated into individual
AppLogics.
Tip: Since there is often a one-to-one correspondence of business
logic to a Web page on a site, there's usually a one-to-one correspondence
of a Web page to an AppLogic "powering" that page. If you have an existing
page-based mockup (or storyboard) for a Web site, this can serve as a rough
gauge of how many AppLogics you'll need to develop.
It's very tempting when starting a new project to jump right in and
start cranking out one AppLogic for each page being designed. But, while
this is a good rule of thumb, you should realize (as we did) that there
may be common functionality across many of the AppLogics. Rather than cut
and paste this functionality from AppLogic to AppLogic, it makes better
sense to develop a class hierarchy based on the common functionality.
In general, the vast majority of AppLogics you'll develop will fall
into one of these three categories:
-
Create a page of information merged from the database.
-
Handle data coming in from an HTML form and update or insert the information
into a database or the file system.
-
Report information back to a user based on some query.
With these categories in mind, we can create a class hierarchy that corresponds
to the AppLogic types as shown in Figure 3. The three subclasses of the
CISNAppLogic are CISNPageAppLogic for returning merged
page information, CISNFormAppLogic for handling HTML forms, and
CISNReportAppLogic for reporting back to the user.
Figure 3. Subclassing the GXAppLogic class by AppLogic functionality
An Example Core Base Class Function
From the base class CISNAppLogic, we would expect components that
either provide common convenience functionality or establish some type
of standardization for accessing other parts of our system (like extensions)
or for event handling. These might include the following:
-
an error-handling mechanism for both system- and user-level errors
-
access and loading of specific NAS extensions
-
SSL secure/insecure transitioning
-
simplified query execution
We'll look at simplified query execution as an example. Usually, the technique
for making a simple flat (nonhierarchical) query goes something like this:
-
Get a database connection (CreateDataConn()).
-
Get a query object (CreateQuery()).
-
Load a query into the query object (LoadQuery()).
-
Execute the query (pDataConn->ExecuteQuery()).
-
Get back an IGXResultSet from the Execute() call.
In C++, this translates to code like that shown in Example 1.
Example 1
GenericAppLogic::Execute()
{
// ...Do something useful...
// Get the database connection.
IGXValList* pList = GXCreateValList();
GXSetValListString(pList, "DSN", "datasource");
GXSetValListString(pList, "DB", "database");
GXSetValListString(pList, "USER", "username");
GXSetValListString(pList, "PSWD", "password");
IGXDataConn* pDataConn = NULL;
CreateDataConn(0, 0, pList, m_pContext, &pDataConn);
// Set up our database parameter marker values.
IGXValList* pParmList = GXCreateValList();
GXSetValListString(pParmList, "PARM1", "VALUE1");
// Get the query object.
IGXQuery* pQuery = NULL;
LoadQuery("queryfile.gxq", "myQuery", 0, pParmList, &pQuery);
// Execute the query and populate the result set.
IGXResultSet* pRS = NULL;
pDataConn->ExecuteQuery(0, pQuery, NULL, NULL, &pRS);
// ...Handle result set...
}
We've left out the error handling for ease of readability, but obviously
we should be checking the HRESULT value of each call to ensure
that it's GXE_SUCCESS, and handle the error accordingly if it's
not. At any rate, you can see that a lot of code gets written for each
database access. It would be nice if we could bundle this into a single
function and place it in the CISNAppLogic class. We can do this
by analyzing the function calls made in terms of the input and output parameters:
-
Input parameters:
-
Connection parameters (a ValList consisting of the data source name, database
name, username, and password)
-
Database parameter marker values (a ValList)
-
Query file name
-
Query name
-
Output parameters:
The transient components are the database connection (pDataConn)
and the query object (pQuery). The connection parameters ValList
is something that for the most part will be fairly static; it would make
sense to move this information into a configuration file and access it
once (perhaps during AppLogic creation), storing it locally in member variables.
But for now we have code that looks like Example
2.
Example 2
class CISNAppLogic {
public:
...
protected:
IGXValList* pDBParmList;
private:
...
};
CISNAppLogic::CISNAppLogic()
{
pDBParmList = GXCreateValList();
// Read connection parameters from config file and store values in our connection
// parameters ValList.
LPSTR dbSource;
LPSTR dbName;
LPSTR dbUsername;
LPSTR dbPassword;
readConfigVariable(DB_SOURCE, dbSource);
readConfigVariable(DB_NAME, dbName);
readConfigVariable(DB_USERNAME, dbUsername);
readConfigVariable(DB_PASSWORD, dbPassword);
GXSetValListString(pDBParmList, "DSN", dbSource);
GXSetValListString(pDBParmList, "DB", dbName);
GXSetValListString(pDBParmList, "USER", dbUsername);
GXSetValListString(pDBParmList, "PSWD", dbPassword);
}
Since we have the connection parameters during the AppLogic's construction,
we can refer to them whenever we need to. Now we can create a function
that will execute a flat query (Example 3).
Example 3
class CISNAppLogic {
...
public:
STDMETHOD(ExecuteQuery) (
/* [in] */ LPSTR queryPath,
/* [in] */ LPSTR queryName,
/* [in] */ IGXValList* queryParms,
/* [out] */ IGXResultSet* pRS,
/* [in] */ IGXTrans* pTrans=NULL
);
...
};
CISNAppLogic::ExecuteQuery(LPSTR queryPath, LPSTR queryName, IGXValList* queryParms,
IGXResultSet* pRS, IGXTrans* pTrans)
{
// Get the database connection (use our previously constructed pDBParmList).
IGXDataConn* pDataConn = NULL;
CreateDataConn(0, 0, pDBParmList, m_pContext, &pDataConn);
// Get the query object.
IGXQuery* pQuery = NULL;
LoadQuery(queryPath, queryName, 0, queryParms, &pQuery);
// Execute the query and populate the result set.
pDataConn->ExecuteQuery(0, pQuery, pTrans, NULL, &pRS);
pDataConn->Release();
pQuery->Release();
}
Here we've added an IGXTrans pointer to the function's signature
so that we correctly support database transactions. You'll notice that
the code is similar to the segment we originally wrote in GenericAppLogic's
Execute() method (Example 1), but we're
now using our convenient connection parameters list (pDBParmList),
a single point for retrieving information about the database. Revisiting
the AppLogic's Execute() method, we get code that looks like Example
4. Obviously, this gives us a much simpler and cleaner interface for performing
query execution.
Example 4
GenericAppLogic::Execute()
{
// ...Do something useful...
// Set up our database parameter marker values.
IGXValList* pParmList = GXCreateValList();
GXSetValListString(pParmList, "PARM1", "VALUE1");
// Execute the query (no transaction used).
IGXResultSet* pRS = NULL;
ExecuteQuery("queryfile.gxq", "myQuery", pParmList, pRS);
// ...Handle result set...
}
An Example Subclass
Similar to the example above, we can create custom functionality for each
of our three subclasses. As an example, let's look at CISNFormAppLogic.
Any AppLogic that takes information from an HTML page may need to perform
some kind of validation on the information being retrieved. While much
of this can be done with client-side JavaScript, there may be cases where
server-side validation is the only viable option (any validation that requires
a database access, for example). Let's assume, though, that we want CISNFormAppLogic
to be able to perform rudimentary data type validation. This would make
it unnecessary for us to have every AppLogic that pulls information from
an HTML form do its own version of validation and possibly generate inconsistencies
in our validation logic. In this case, we might find ourselves with an
API like the one shown in Example 5.
Example 5
class CISNFormAppLogic : public CISNAppLogic {
public:
...
STDMETHOD(validateFormItem) (
/* [in] */ LPSTR formValue,
/* [in] */ LONG dataType,
/* [out] */ BOOL& isValid
);
...
};
The various data types can be defined as constants, such as FORM_DATATYPE_DATETIME.
We would pass in a value from the form along with its data type and then
determine whether it's valid. If the value turns out to be invalid, we
might return the error to the user via a standardized user-level error
handler (which could be localized to CISNAppLogic so that all
applications can make use of it). The code might look something like Example
6.
Example 6
class CISNMyFormAppLogic : public CISNFormAppLogic {
...
};
CISNMyFormAppLogic::Execute()
{
// ...Do something useful...
BOOL isValid = TRUE; // Be an optimist.
validateFormItem(GXGetValListString("DATE_OF_BIRTH"), FORM_DATATYPE_DATE, isValid);
if (!isValid) {
// Use some standardized error handler for user-level errors.
addErrorMsg(ERROR_MSG_INVALID_DATE_OF_BIRTH);
}
validateFormItem(GXGetValListString("AGE"),FORM_DATATYPE_INTEGER, (isValid = TRUE));
if (!isValid) {
addErrorMsg(ERROR_MSG_INVALID_AGE);
}
...
if (hasErrors()) {
// We have errors, so show them to the user and resend the same template back.
return handleErrors(HTML_FORM_TEMPLATE_PATH);
}
...
}
From Example 6 you can see how we can easily take advantage of localized
data type validation for any AppLogic that gathers data from an HTML form.
Additionally, using a common error-handling mechanism simplifies how the
errors are returned to a user. In this case, we've come up with a function,
addErrorMsg(), that takes a error code parameter. This parameter
may be associated with an actual error string (in a flat file or in the
database) that's appended to an error list. Once we've added the error
message to our list, we can set a flag indicating that an error condition
exists. A call to hasErrors() by an AppLogic will test for the
existence of an error in the message list. If one exists, we can call handleErrors(),
which would reevaluate the same template the user previously had (the HTML
form), but this time with the errors listed at the top. We can use the
m_pValIn list to repopulate the form with the originally entered
data so that the user doesn't have to retype everything.
Be aware that while most AppLogics will fall neatly under one of the
categories mentioned earlier, you'll always run across the need for AppLogics
that aren't necessarily based on user events. Electronic commerce sites
are full of batch jobs that must be run at periodic times (settling credit
card transactions, for example). In cases like these, your AppLogics
will follow a model that closely resembles a Unix-based cron job.
Fortunately, NAS supports such a model through the
use of the IGXAppEvent interface. Events are registered with
NAS and will perform AppLogic execution, e-mail transmission, or both.
The time at which the events will occur is determined when the event is
registered.
Note that because the registration and modification of events with the
AppEvent manager is API-based, it's wise to write an AppLogic-based tool
with an HTML front end that can act as an administrative tool for this
API. Site maintainers can use this tool to register and modify events.
Realize that as you develop a site, you'll come across more and more instances
of "infrastructure" AppLogics that are over and above the one-page-to-one-AppLogic
estimate you may make when developing a Web site. Additionally, you'll
have these "cron job"-type AppLogics that will also raise your
AppLogic development estimate.
THE SERVICE OBJECT LAYER
Now that you have a feel for what belongs in the application layer and
how to structure the applications within that layer, we'll take a look
at the far more daunting task of writing classes for the service object
layer. Software for this layer should attempt to accomplish one (or more)
of the following goals:
-
Perform some common utility for applications or other services.
-
Provide some "service" for applications, with or without accessing the
database.
-
Provide an access point for applications (that is, AppLogics) to select,
update, or delete information from the database.
If your software component isn't targeted to accomplish one of the above
goals, it should most likely be written in the application layer.
For those software pieces that accomplish the aforementioned goals,
there are specific techniques for developing each type of component. We'll
explore the first goal (utility objects) as an example to give you a feel
for what it takes to develop service objects.
COM Objects in NAS
NAS provides an open-ended mechanism for
implementing objects that are used in a "service" capacity by the application
layer. This is accomplished
by creating a class with a COM (Component
Object Model) interface and then using a concrete implementation class to
fulfill the requirements of the interface. A class is considered
"concrete" if it does not contain any pure virtual functions. An interface
in C++ is really just an abstract class; that is, it contains one or more
pure virtual functions, and as such cannot be instantiated. The combination
of using an abstract class (interface) with a concrete one (implementation)
is commonly referred to as the "interface/implementation" idiom.
All NAS COM objects inherit from an object provided by Netscape: IGXObject.
IGXObject is really just an empty class that has been inherited
from the abstract base class, IUnknown. IUnknown contains
the following pure virtual functions, which provide the core functionality
common to all COM objects:
-
QueryInterface() tests an object to see whether it supports a
certain interface, and if so, dynamically casts the object to use that
interface. In C++ parlance, it tests to see whether an object has inherited
from a particular abstract class, and if so, binds an abstract class pointer
to that object so that the interface can be used.
-
AddRef() and Release() support a reference-counting model
for memory management. AddRef() increments the reference count
and Release() decrements it. When an object's reference count
goes to 0, it's deleted off the heap. With this type of model in place,
objects are not directly deleted with the C++ delete keyword.
For example:
IGXValList* pValList = GXCreateValList();
pValList->Release(); // Correct
delete pValList; // Error
Because all our COM objects inherit from IGXObject, they must
implement, as a minimum, these three functions.
Additionally, every COM object's interface must have a unique identifier
that we can use when we're testing an object to see whether it supports
a particular interface. This unique identifier is known as a GUID (globally
unique identifier), or alternately an IID (interface identifier). (Note
that Netscape has used the C++ typedef mechanism to make both
a GUID and an IID identical in the gxdefs.h file.) Thus,
whenever we create a new COM object within our service object layer we
need to create a new IID. Fortunately, NAS provides us with a command-line
utility, kuuidgen, to do just that.
Once we have an IID, we need an IDL file for our COM object. This file
specifies the interface that's associated with our IID. Example 7 is a
segment of the abstract class header file (IGXTemplateMap.h, generated
from an IDL file via the KIDL utility) for
the IGXTemplateMap interface.
Example 7
#include "IGXObject.h"
struct IGXObject;
struct IGXBuffer;
EXTERN_C const IID IID_IGXTemplateMap;
// [local, object, uuid(16FB0740-9A3C-11cf-970E-0020AFED9A65)]
struct IGXTemplateMap : public IGXObject
{
// IGXTemplateMap is used during template processing to do field mapping.
public:
virtual HRESULT STDMETHODCALLTYPE Get(
/* [in] */ LPSTR szExpr,
/* [in] */ IGXObject __RPC_FAR* pData,
/* [in] */ IGXObject __RPC_FAR* pMark,
/* [out] */ IGXBuffer __RPC_FAR* __RPC_FAR* pBuff) = 0;
};
You'll notice the interesting line
EXTERN_C const IID IID_IGXTemplateMap;
What's going on here? Well, we're declaring that externally defined
(somewhere!), there is a constant, IID_IGXTemplateMap, that corresponds
to this interface. So, if we ever wanted to test an object to see whether
it's using this interface, we'd use IID_IGXTemplateMap.
Note: That "somewhere" where the IID_IGXTemplateMap
IID is defined is actually the NAS library libgxidl.so. This library
contains all the IIDs for the various NAS-included COM objects. Thus, whenever
you compile a C++ AppLogic you'd link in this library.
Looking at the IGXTemplateMap interface in Example 7, we see
that it inherits from IGXObject and makes a single API call, to
Get(), which takes four arguments. The interface is a pure virtual
function (notice the "= 0"). If we wanted to inherit from this
class, we'd have to implement not only QueryInterface(), AddRef(),
and Release() (to fulfill our obligation to IGXObject),
but also Get() (from IGXTemplateMap).
An Example Utility Class
Suppose we want to create a utility class, CISNTemplateMap, that
we might use all over our Web site -- in anything from applications to
other service objects. This is an excellent candidate for inclusion in
the service object layer, as it would fulfill the first goal of performing
some common utility for applications or other services. Let's say CISNTemplateMap
is going to extend the IGXTemplateMap functionality so that we
can put items into the map with Put() in addition to just getting
them with Get(). Thus, we'd want to create an interface for CISNTemplateMap -- say, IISNTemplateMap. (I follow a convention of preceding interface
names with "I" and names of concrete classes with "C".) This would give
us a file structure like the one shown in Figure 4.
Figure 4. File entities and their class counterparts for NAS
COM object implementation
From Figure 4 you can see that a COM object requires four files:
-
IDL file -- an IDL definition of the interface we want to specify
-
IDL header file -- the language-specific header file, which results
from running the IDL file through an IDL compiler (KIDL, for NAS)
-
Implementation header file -- The implementation classes' header
file, which contains the non-pure virtual function declarations for the
class
-
Implementation code file -- The *.cpp file, which contains
the actual implementation code
For IISNTemplateMap, a segment of the IDL file might look like
Example 8.
Example 8
interface IISNTemplateMap;
[
local,
object,
uuid(A2D9167A-D1B6-1446-B6F0-08002080A910)
]
interface IISNTemplateMap : IGXTemplateMap
{
STDMETHOD(Put) (
[in] LPSTR theKey,
[in] LPSTR theValue
);
}
Note that we have an IID already specified for this interface along with
the interface's API (in this case, just the single call, Put()).
When this is compiled with KIDL, we get something like Example 9 as output.
Example 9
class IISNTemplateMap;
/* [
local,
object,
uuid(A2D9167A-D1B6-1446-B6F0-08002080A910)
] */
extern "C" const IID IID_IISNTemplateMap;
class IISNTemplateMap : public IGXTemplateMap {
public:
STDMETHOD(Put) (
/* [in] */ LPSTR theKey,
/* [in] */ LPSTR theValue
);
};
By running the file through the IDL compiler, we've generated a C++
abstract class with Put() as a pure virtual function. We can now
#include this file in our implementation header file (Example
10).
Example 10
#include "IISNTemplateMap.h" // Our interface
class CISNTemplateMap : public IISNTemplateMap {
public:
CISNTemplateMap();
virtual ~CISNTemplateMap();
// IUnknown Interface
STDMETHOD(QueryInterface) (REFIID riid, LPVOID *ppvObject);
STDMETHOD_(ULONG, AddRef) ()
STDMETHOD_(ULONG, Release) ();
// IGXTemplateMap Interface
STDMETHOD(Get) (
/* [in] */ LPSTR szExpr,
/* [in] */ IGXObject* pData,
/* [in] */ IGXObject* pMark,
/* [out] */ IGXBuffer** ppBuff
);
// IISNTemplateMap Interface
STDMETHOD(Put) (
/* [in] */ LPSTR theKey,
/* [in] */ LPSTR theValue
);
private:
// Data structure for storing our data
ISNMapDictionary* theDictionary;
// Required for reference-counting macros
GXBasic m_GXBasic;
// Used to make sure object is thread-safe (Get() and Put() functions are atomic)
GXCRIT_SECTION m_Crit;
};
Our implementation class, CISNTemplateMap, inherits from our
interface class, IISNTemplateMap. It also includes declarations
for all of the pure virtual functions we must implement -- from IUnknown
down to IGXTemplateMap to our own IISNTemplateMap.
The QueryInterface() Code
In the implementation code, things get a bit more interesting. The QueryInterface()
code looks like Example 11.
Example 11
STDMETHODIMP
CISNTemplateMap::QueryInterface(REFIID riid, LPVOID *ppvObject)
{
if (!ppvObject) {
return E_INVALIDARG;
}
*ppvObject = NULL;
if (GXGUID_EQUAL(riid, IID_IUnknown) ||
GXGUID_EQUAL(riid, IID_IGXObject) ||
GXGUID_EQUAL(riid, IID_IGXTemplateMap) ||
GXGUID_EQUAL(riid, IID_IISNTemplateMap))
{
*ppvObject = (LPVOID) this;
}
if (*ppvObject) {
((IUnknown *) *ppvObject)->AddRef();
return NOERROR;
}
return E_NOINTERFACE;
}
The caller supplies a REFIID (which is really just a constant
reference to an IID -- that is, const IID&) and a void pointer
to a pointer (a handle), ppvObject. QueryInterface()
does some simple error checking to make sure that the pointer isn't pointing
to a NULL pointer. The first if statement tests to see
whether this object can support the interface being queried. This is done
by comparing the constant IIDs for the various interfaces supported by
this object with the IID being supplied in the function call. For our class,
CISNTemplateMap, we support IUnknown, IGXObject,
IGXTemplateMap, and IISNTemplateMap. If any one of the
GXGUID_EQUAL(...) macros returns a successful match for the comparison,
we assign our object (via the this pointer) to the handle by doing
a hard cast to make it a void*.
Our final check is to see whether our assignment worked. If it did,
we got an interface match and we can call AddRef() on the object
we're returning through the handle. Otherwise, we return an error, E_NOINTERFACE,
so that the caller knows the object does not support the interface we queried
(as demonstrated in Example 12).
Example 12
// Get an ISNTemplateMap.
IISNTemplateMap* pISNMap = ISNCreateTemplateMap();
// Test to see whether the object being pointed to by our IISNTemplateMap pointer
// (pISNMap) supports the IGXTemplateMap interface.
IGXTemplateMap* pGXMap = NULL;
HRESULT hr = GXE_SUCCESS;
if (((hr = pISNMap->QueryInterface(IID_IGXTemplateMap, (LPVOID*) &pGXMap))
!= E_NOINTERFACE) && (pGXMap != NULL))
{
cout << "Interface supported." << endl;
pGXMap->Release();
} else {
cout << "Interface NOT supported." << endl;
}
Here we use QueryInterface() to see whether the object being
pointed to by the IISNTemplateMap pointer supports the IGXTemplateMap
interface. But notice how this same call to QueryInterface() also
binds the object to a new interface pointer -- in this case, IGXTemplateMap.
Thus we can use QueryInterface() in a multiple-inheritance scheme
to dynamically cast our object to use two different interfaces, depending
on the need. In fact, this is exactly what NAS does with the object being
returned from an Execute() call through an IGXHierQuery
interface. The caller gets back some object bound to an IGXHierResultSet
interface pointer, but can turn around and call QueryInterface()
on that object and "magically" transform the IGXResultSet into
an IGXTemplateData! Example 13 illustrates this.
Example 13
IGXDataConn* pDataConn = NULL;
// ...Get the database connection...
// Get a hierarchical query object.
IGXHierQuery* pHq = NULL;
CreateHierQuery(&pHq); // Bind the interface to its implementation.
// Load our flat file query into our query object.
IGXQuery* pQuery = NULL;
LoadQuery("myQueryFile.gxq", "myQuery", 0, NULL, &pQuery);
// Add the query to our hierarchical one.
pHq->AddQuery(pQuery, pDataConn, "myHierQuery", NULL, NULL);
// Execute the query.
IGXHierResultSet* pHierRS = NULL;
pHq->Execute(0, 0, NULL, &pHierRS);
// Magically transform our HierResultSet into a TemplateData.
IGXTemplateData* pTData = NULL;
pHierRS->QueryInterface(IID_IGXTemplateData, (LPVOID*) &pTData);
// Voila! A TemplateData. Now we can call EvalTemplate().
EvalTemplate("template.html", pTData, NULL, NULL, NULL);
// Release our resources.
pHq->Release();
pQuery->Release();
pHierRS->Release();
pTData->Release();
Note that at the end of this code, we still need to release both IGXTemplateData*
and IGXHierRS* because we have two references to the same object.
The QueryInterface() call, if successful, will return the object
passed to AddRef().
The AddRef() and Release() Code
Now that you've seen how the more complicated QueryInterface()
works, let's look briefly at the other two functions from IUnknown,
AddRef() and Release(). These functions perform simple
operations -- one increments a counter and the other decrements it. When
the counter reaches 0, the object is destroyed by deleting the this
pointer; this in turn causes the object's destructor to be called. The
implementation of these functions is shown in Example 14.
Example 14
CISNTemplateMap::AddRef()
{
GXUTIL_ADDREF();
}
CISNTemplateMap::Release()
{
GXUTIL_RELEASE();
}
This is rather simple from a development standpoint because Netscape
has provided us with macros that perform the reference-counting details.
If we dig beneath the surface, we see that these macros are defined as
shown in Example 15.
Example 15
#define GXUTIL_ADDREF() \
if (1) \
return m_GXBasic.IncrRefCount(); \
else 0
#define GXUTIL_RELEASE() \
if (1) \
{ \
ULONG count; \
GXASSERT((m_GXBasic.GetRefCount())> 0, GXASSERT_ERROR, \
"TOO MANY RELEASES"); \
count = m_GXBasic.DecrRefCount(); \
if (!count) \
delete this; \
return (count); \
} else 0
Both macros use the same basic object, m_GXBasic. This is an object
of the class GXBasic, which actually contains the reference count
for the object (along with a GXSYNCVAR to keep AddRef()
and Release() atomic). Thus, in order for the macros to work,
all your COM objects must contain the GXBasic class, and the object
being reference-counted must be named m_GXBasic.
AddRef() is straightforward; it's a light wrapper around the
GXBasic member function IncrRefCount(), which returns
the current reference count after the increment. Release()
is a bit more complicated, as it includes an assertion macro to make sure
the current reference count is greater than 0 before we reduce it. (Actually,
in the production release, GXASSERT(...) is a no-op, so this check
is made only during a debug build of NAS.) After the assertion macro, we
decrement the reference count and get the new value after the decrement.
If the count variable has dropped to 0, we call delete this
and return the count.
As you can see from our CISNTemplateMap class, there are several
steps we've had to complete in order to develop
a utility class within NAS that supports the COM model. But the results
are worth it. With the example above, we've written our own equivalent
to the NAS GXTemplateMapBasic class and can now add whatever functionality
we want to the template map. We can overload Put() and Get()
with our IISNTemplateMap interface to include other data types,
such as long and int. This isn't possible with GXTemplateMapBasic,
but only with our CISNTemplateMap, snugly hidden behind an interface.
And since we've subclassed our interface from the NAS IGXTemplateMap
interface, we can use a CISNTemplateMap wherever we would have
used GXTemplateMapBasic (as in EvalTemplate() calls,
for example).
TO SUMMARIZE
Netscape Application Server provides the framework for developing large-scale -- and, if you're not careful, potentially complicated -- software entities.
Breaking down the business logic into two layers, an application layer
and a service object layer, is the first step toward creating a manageable,
extensible architecture.
The AppLogics, which reside in the application layer, should be derived
from specialized base classes from the GXAppLogic class. These
specialized base classes are created according to their functionality.
A single derived base class (like our CISNAppLogic
class) from which all specialized classes derive is handy for localizing
functionality that's likely to be needed by all AppLogics.
The software entities in the service object layer should serve some
simple utilitarian purpose (as do template maps, for example), or provide
either a "service" of some kind (such as supplying run-time configurable
information) or access to the database (for example, via objects that wrap
database tables).
As we saw in our example of a template map COM object, we need a specific
IID to associate with our interface (written in an IDL file), as well as
accompanying implementation header and code files. Since all COM objects
in NAS derive from IGXObject (and ultimately from IUnknown),
we must support the QueryInterface(), AddRef(), and Release()
functions. Having done this, we end up with a flexible and robust utility
class that can be used by other service objects or applications.
FURTHER RESOURCES
View Source wants your feedback!
Write to us and let
us know
what you think of this article.
Many thanks to Robert Husted and Mike Lee for their technical review
of this article, and, of course, to View Source editor Paul Dreyfus.
Steve Malmskog
is Director of Product Development for the Internet
Shopping Network. The Internet Shopping Network
is home to First Auction -- the
world's only real-time online auction, featuring consumer electronics,
computers, home and leisure merchandise, and jewelry. When he's not busy
designing applications or writing code, Steve enjoys playing tennis and
basketball and lending a helping hand at his church.
(8:98)
Related Readings:
Any sample code included above is provided for your use on an "AS IS" basis, under the Netscape License Agreement - Terms of Use