Implementing the Duwamish Books Business Logic Layer in Visual C++

Duwamish Books, Phase 3

Michael Zonczyk
Microsoft Developer Network

September 1998

Summary: Describes a Microsoft® Visual C++® implementation of the Duwamish Books, Phase 3 Business Logic Layer. (11 printed pages) Covers:

Introduction

The Visual C++ Business Logic Layer (BLL) implementation is the second Visual C++ installment in our goal to provide parallel implementations of the middle-tier software using several of the Microsoft development languages in Microsoft Visual Studio® version 6.0.

Like the Visual C++ Data Access Layer (DAL) component in Phase 2, the Phase 3 BLL component is a functional port of its Visual Basic counterpart. The primary requirement has been to develop an interchangeable component that fits into the cross-language architecture of the Duwamish Books Sample project. The unifying aspect of our cross–language architecture is the use of Microsoft ActiveX® Data Objects (ADO) technology and its underlying object model for data access abstraction. As well as being cross-language, ADO offers a key scalability feature called disconnected recordsets (see Robert Coleridge's article "Designing a Data Access Layer").

Here are the features of the Visual C++ BLL:

Implementation Goals

Implementation Strategy

Compatibility

While the Visual C++ and Visual Basic BLL components expose the same methods and symbols, they do not share the same COM globally unique identifier (GUID)—they are not binary compatible. The Phase 3 Visual C++ Business Logic Layer DLL (db3vcbll.dll) is ProgId compatible with its counterpart Visual Basic DLL (db3vbbll.dll). ProgId compatibility is sufficient for late binding, but not for early binding clients such as our Duwamish Books client apps. To use the Visual C++ BLL with the Duwamish Books sample, you'll need to recompile the set of Duwamish Books clients after redirecting their BLL references to use "MSDN: Duwamish 3, VC++ Business Logic Layer". (In Visual Basic, click References from the Tools menu.)

Nevertheless, ProgId compatibility implies the Visual Basic and Visual C++ components have Microsoft Windows® registry keys in common. So, registering either BLL.DLL—by compiling its integrated development environment (IDE) project or by invoking regsvr32 on the DLL file—causes registry key values to be overwritten by the last registered version. The Duwamish Books client applications' component references need to agree with the last registered (Visual C++ or Visual Basic) DLL version to avoid a startup error.

Justification for supporting ProgId rather than binary compatibility is presented in the Phase 2 article "Migrating a Visual Basic 5.0 Component to Visual C++ 5.0."

Compatibility Surprises

A ground rule guiding our development of two versions of a component in parallel has been to make the Visual C++ and Visual Basic versions interchangeable at both design time and at run time (see "Migrating a Visual Basic 5.0 Component to Visual C++ 5.0").

Unfortunately, a compatibility problem between the Visual Basic and Visual C++ BLL implementations surfaces in a Visual Basic client wherever it uses a fully qualified public enumeration constant expression.

Surprisingly, although an enumeration type name, such as EXPANSION_TYPE, can be used, and the enumeration constant icPK, for example, resolves correctly, the fully qualified constant expression is not accepted when compiling a Visual Basic client referencing the Visual C++ BLL.

To work around this, use enumeration constants without type name qualification.

Porting the BLL: Core Processing

The core processing of the BLL manages the generation of ADO-compliant Microsoft SQL Server™ command strings in conjunction with automation-type parameter validation and conversion.

As a middle-tier component, the Business Logic Layer is both a server and a client. It is a server to all of the Duwamish Books client applications and, in turn, is itself a client of the Data Access Layer (DAL). We can gain a perspective on the core BLL processing if we notice that the BLL exposes 35 data passing methods in its interface but uses only 2 from the DAL—GetRecordset and ExecQuery. How are the many funneled down to just two? (Note: We're not counting use of the DAL's transaction signaling methods here, because they don't involve data transfer.)

Figure 1 characterizes the parameter types and directions for the methods of the BLL invoked by the clients, as well as the methods the BLL invokes on the DAL. The illustration helps to emphasize the asymmetric use of the _Recordset parameter and the essence of the BLL processing. The [in] _Recordset between the client and the BLL acts as a collection of named VARIANTS, which the BLL composes into the ADO query string, while the [out] _Recordset is a pass-through from the DAL to the client.

Figure 1. BLL external data flows

Implementation

This is an ATL COM AppWizard initiated project. When starting the project we used the following Wizard settings.

AppWizard options:

Object Wizard options for CBusLogic class:

Object Wizard attributes for CBusLogic class:

Background

The ADO _Recordset data type has a starring role throughout the Duwamish Books architecture. While it is a complex COM object, it also exposes behavior in common with a keyed collection—also called a Map. In this sense, a _Recordset is a collection of late-bound, named VARIANTS. We perform extraction of a variant by name in two contexts:

For example:

// Variant containing IDispatch stored in parent under name "Details"
   _Recordset *pRs= pParent;
   _variant_t spVar= pRs->Fields->GetItem(L"Details")->Value;

A fundamental aspect of the Duwamish Book business logic model's implementation is that it's composed of two parts—the BLL and the set of stored procedures on the SQL Server database. There is an essential but wholly implicit name coupling between the stored procedure names and the command names embedded in the query strings passed from the BLL to the DLL. The BLL design manages the name literals for this purpose as constants organized into six domains—the CBllCmd command class and the five parameter classes derived from CBllParam.

The command strings to be generated are effectively late-binding function calls to SQL Server with ADO expressions as intermediaries. The distinction between ADO atomic and composite (Shaped) query expressions is reflected in the two concrete command classes, CBllCmdExec and CBllCmdShape, respectively.

Writing a middle-tier component in C++, a strongly typed language, to work in an entirely late-binding context means that we have less confidence about the data being passed into our code. Our input parameters are dynamic, string-indexed, VARIANT collections, and we generate on-the-fly ADO query strings. With this in mind, the design provides for (some) parameter type checking and reporting during query string generation.

Reusable core processing syntax

With the porting goals and strategies in mind, and with the background just presented, consider the following code sequences, which illustrate the recurring syntax patterns for the core processing throughout all 35 cBusLogic (IBusLogic) methods.

The BLL defines concrete parameter classes corresponding to these business value types: entity key, count, date, money, string literal, and enumeration. (Note that enumerations are further specialized to support bounds checking.) All concrete parameter classes implement two constructor signatures. Table 1 shows some valid instantiation expressions.

Table 1. Core Processing Syntax: Various Parameter Instantiations

Syntax Semantics
// lPKId is an integer input parameter
// pRs is an input _Recordset* parameter
CBllKey authorId( PRM_PKID, lPKId ) // from integer
CBllKey authorId( PRM_PKID, pRs ) // from Recordset
Constructs CBllKey object, observing the business rule for the key value type (that is, ignore if zero).
// bstrAlias is an BSTR input parameter
// pRs is an input _Recordset* parameter
CBllBstr employeeAlias( PRM_ALIAS bstrAlias )
CBllBstr employeeAlias( PRM_ALIAS pRs )
Constructs CBllBstr object, observing the business rule for the string-literal value type (must be single quoted and embedded quotes escaped).
CNExpansion expansionType( icDETAILS )
Constructs a CNExpansion object, observing the rule for the enumeration value type (ignore unless in range 1 ... BLL_EXPANSION_TYPE_MAX).

Table 2 shows how we create and use simple commands with parameters.

Table 2. Core Processing Syntax: Simple Command Instantiation, Parameterization, and Use

Syntax Semantics
// bstrAlias and bstrPassword are input arguments
CBllCmdExec cmdGetEmployees(CMD_GET_EMPLOYEES);
Creates a simple command.
cmdGetEmployees << CBllBstr( PRM_ALIAS bstrAlias)
                << CBllBstr( PRM_PASSWORD bstrPassword);
Adds two parameters.
// cmdGetEmployees converted to string by user-defined // CBllCmdExec::operator _bstr_t()
spDAL->GetRecordset(…, cmdGetEmployees, ppEmployees,…) 
Invokes DAL:: GetRecordset() or ExecQuery().

Table 3 shows how we create and use a shaped command, starting with two simple commands.

Table 3. Core Processing Syntax: Shaped Command Instantiation and Use

Syntax Semantics
// cmdGetOrders & cmdGetSaleHeaders are CBllCmdExec
// commands with parameters, similar to first example
CBllCmdShape cmdGetOrders( &cmdGetSaleHeaders,    
                              &cmdGetSaleDetails, 

   CMD_AS_DETAILS_RELATE_TO_SALE)
Creates a shaped command from two simple commands and the symbol for a particular stored procedure.
// cmdGetOrders converted to string by user-defined
// CBllCmdShape::operator _bstr_t()
spDAL->GetRecordset(…, cmdGetOrders, ppOrders,…) 
Invokes DAL:: GetRecordset() or ExecQuery().

Parameters and commands

Finally, Tables 4-6 present synopses of the command and parameter class. Note that derived class names are indented from their respective base classes.

Table 4. Command Class Hierarchy

Class name Behavior
CBllCmd The pure abstract base class for commands. The key requirement imposed on every concrete command class is to implement a method to render itself into an ADO query expression string. Derived classes must override:

public:

virtual operator _bstr_t() = 0;

This base class implements stored-procedure name lookup for all defined commands via:

protected:

static const WCHAR *Lookup( COMMAND_TYPE nType)

CBllCmdExec The overridden operator _bstr_t() generates an ADO non-shaped query expression.

Uses a standard library vector type to hold parameter sub expressions in a collection until needed by operator _bstr_t()

Defines the "<<" operator to provide a convenient syntax for adding parameters to the command. Its signature is:

public:

CBllCmdExec& operator <<( const CBllParam &param )

CBllCmdShape The overridden operator _bstr_t() generates an ADO Shaped query expression by wrapping two non-shaped expressions with additional syntax.

The only way to construct a CBllCmdShape is from two completed CBllCmdExec objects:

public:

CBllCmdShape( CBllCmdExec *pHeader, CBllCmdExec *pDetails, COMMAND_TYPE nType)


Table 5. Abstract Parameter Class

Class name Abstract behavior
CBllParam The pure abstract base class for all parameter types. The non-virtual function AsValidatedPair() is provided for Command objects to use as they render themselves to strings, that is via the _bstr_t() operator:

public:

pair< bool, pair< _bstr_t, _bstr_t> > AsValidatedPair()

The pure virtual function ValidatedValue() is overridden exclusively by leaf classes in the parameter hierarchy (note exception for the non-leaf CBllEnum):

public:

virtual pair< bool, _bstr_t> ValidatedValue() = 0

The only implemented constructor has protected access permission:

protected:

CBllParam( const WCHAR szName, bool bIsDefined )


Table 6. Parameter Class Hierarchy

Class name     Behavior
Notes for CBllParam derived classes     All CBllParam derived classes have two public constructors with signatures:
  • CBllXxx( <some enum type> nType, _Recordset *pRs)

  • CBllXxx( <some enum type> nType, Xxx  xVal)
  CBllBool   For use with VARIANT_BOOL parameters.

Converts VARIANT_BOOL values to the unquoted string "1" or "0".

  CBllBstr   For use with BSTR parameters.

Converts BSTR values to quoted and bracketed form metastrings. NULL BSTRs and zero length BSTRs resolve to dual single-quote string.

Validation rule: Strings are always valid unless the constructor's second argument is false (the default). If this argument is false, a NULL or zero-length string is treated as undefined so that ValidatedValue(…) returns pair< false, ''>.

  CBllMoney   For use with struct CY (that is, currency) parameters.

Converts _int64 values to fixed-point, currency-amount unquoted strings in canonical format (that is, four decimal places).

Validation rule: Always valid.

  CBllDouble   For usage with logical subtypes of double parameters represented by its derived classes.

Validation rule: Always valid.

    CBllDate Converts a double value to a quoted ANSI date-string format yyyy.mm.dd hh:mm:ss.
  CBllLong   For use with logical subtypes of long parameters represented by its derived classes.

Converts a long value to an unquoted string.

    CBllKey Validation rule: Returns <true, X>, where X is the key value and the key value does not equal zero. When the key value equals zero, returns < false, 0>.
    CBllCount Validation rule: Always true.
    CBllEnum Exception to general pattern. Non-leaf class CBllEnum implements virtual ValidatedValue(...) for its derived classes.
          CNExpansion Validation rule: True only for values in range 1 … BLL_EXPANSION_TYPE_MAX.
          CNInventoryUpdateType Validation rule: True only for values in range 1 … BLL_INVENTORY_UPDATE_TYPE_MAX.
          CNItemTemplate Validation rule: True only for values in range 1 … BLL_ITEM_TEMPLATE_TYPE_MAX.
          CNOrderTemplate Validation rule: True only for values in range 1 … BLL_ORDER_TEMPLATE_TYPE_MAX
          CNOrderUpdateType Validation rule: True only for values in range 1 … BLL_ORDER_UPDATE_TYPE_MAX

Implementing ISupportErrorInfo ( COM Exceptions)

Our COM object exposes the ISupportErrorInfo interface and implements its single method, InterfaceSupportsErrorInfo( REFIID riid), to return S_OK when passed our _cBusLogic (IBusLogic) interface GUID. By this protocol, our object asserts that it sets a COM error in conjunction with any failed _cBusLogic (IBusLogic) HRESULT. COM errors convey exception information in a language-neutral way from server to client.

We implement this commitment by having every _cBusLogic (IBusLogic) method catch all exceptions and convert them to COM errors, before returning. As a method catches an exception, as part of the conversion process, it pastes local context information onto the error description. For effective conversion and context pasting we recognize all possible exceptions as being in one of three categories: due to a constraint check in the BLL object itself (CBllBailout), passed-through from the DAL (_com_error), and all others.

Notice that the C++ implementation raises failed constraint checks to the status of a COM error while our Visual Basic implementation does not. For example, the following are excerpts from the InsertAuthor method from both BLL implementations.

Visual Basic version:

InsertAuthor= False
If Author Is Nothing Then Exit Function

Visual C++ version:

*pbSuccess= VARIANT_FALSE;
if (*ppAuthor==0) throw CBllBailout(E_INVALIDARG, L"Input Recordset cannot be Null");

Our C++ implementation throws COM errors for all of these easily detected interface assumption violation errors.

Miscellaneous Implementation Issues

Here are a couple of hard-to-categorize implementation issues you may also run into. The first, import indirection, describes a solution to the problem of not having an available ADO Interface Definition Language (IDL) file to import into our own component's IDL—this is a recap of my explanation in "Migrating a Visual Basic 5.0 Component to Visual C++ 5.0." The second, calling own-component interface methods, explains why a component should not directly call an interface method on itself and proposes a simple solution when this is necessary.

Import indirection

Near the top of the project IDL file (dbbll.idl) you'll find this statement: import "helper.idl". Helper.idl is used here for import indirection, because it contains a single statement: import "msado15.idl".

As we know, MIDL's automatic behavior inserts a corresponding #include into the project header (dbbll.h) file for every import in the IDL file. By using import indirection, our project header acquires the statement #include "helper.h". This is good because file msado15.h is, for the time being, not distributed as part of the Data Access SDK. All we need in the project header, anyway, is a forward declaration of struct _Recordset, which we put into "helper.h" to complete the workaround.

File name File contents
helper.idl import msado.idl
helper.h struct _Recordset

Calling own-component interface methods

Sometimes within a component's method there is a need to call another method in the same component. There is a complication if the second method is part of the COM interface for the component—unnecessary conversion between C++ exceptions and COM errors, and back again. If properly written as an interface method, the called method converts internally caught exceptions into COM errors. When the caller is an own-component method, conversion back is inelegant and wasteful.

A nice solution is to split the interface method into an interface method that catches and converts exceptions and a private core method that can be called by any own-component method. The BLL uses this approach with respect to the GetOrders() and GetSales() interface methods—_GetOrders() and _GetSales() are private core methods called from several internal locations.

Increase Robustness

In developing Duwamish Books, we are mostly working in rapid application development (RAD) mode. Visual Basic is very well suited to RAD development, however C++ is less so. Why is it worthwhile to (re)write a production version of a middle-tier business logic layer in C++? Some would propose run-time efficiency as the answer, but often efficiency advantages are nonexistent for thin tiers of similar design—after all, ADO and SQL Server are doing the real work.

The big advantage of using Visual C++ to implement a BLL is its comprehensive support for object-oriented programming (OOP) and its superior exception handling, which together enable more robust designs. This is an important advantage if we consider the essential requirements of a production-quality BLL—implementing and enforcing business rules and processing policy for diverse clients, while being extendable and maintainable.

A cohesive and consistent error-handling architecture reflects business policy too! A highly developed middle tier can help protect shared resources from a new buggy client or enforce a security policy when appropriate. A comprehensive error-handling strategy greatly increases reliability, especially as computation is distributed to n tiers. For example, each layer can be designed to preclude and/or filter categories of errors based on information available at that tier.

Once the business logic is stable, it is reasonable that client applications may be modified or completely new applications created to work with an existing BLL. Or, the BLL may need to be modified as the business changes. In either case, C++ with OOP supports more effective strategies for factoring and organizing business rules where a higher level of functionality is required.

Wrap-up

As intended, the Duwamish Books Visual C++ Phase 3 BLL component, db3vcbll.dll, is ProgId-compatible with the Visual Basic BLL, db3vbbll.dll, both at run time and design time. In case you're wondering how the C++ project gets away with using parameter names like bstrPassword or IBusLogic while exposing the Visual Basic design-time friendly names Password and _cBusLogic, see "Symbol Renaming" in my article "Migrating a Visual Basic 5.0 Component to Visual C++ 5.0."

The "commands and parameters" design choice effectively leverages C++ OOP for the core processing by clearly separating parameter conversion and validation from ADO query string formatting. If you're familiar with both Visual Basic and Visual C++ syntax, you will notice how this approach reduces code clutter and makes the main logic more transparent than the inline conditional string pasting done in our Visual Basic implementation.

Finally, our Visual C++ implementation is more rigorous with exception handling, as appropriate for a less RAD implementation using C++. You'll notice that any run-time error messages you see when using the C++ version will provide additional method-name context information.