evelopers using binary reusablecomponents essentially becomes users, and immediately they treat these”black boxes” the same way users treat other programs. Amazingcapabilities are overshadowed by ease of use and other trivial issues. While I have not discovered the total recipe for “making reuse fun” yet,guys reusing my components agree that the current level of “ease ofreuse” certainly provides some happy faces.
This article really is part two of my previous article. In fact, the previous article is not complete without this one, but due to the size of the first article, it was decided not to include this information. This has two implications on this article. Firstly, I would advise anyone not familiar with the content of the previous article to follow the above link and read the first article prior to reading this one. Secondly, I am going to use the same samples as in the previous article, plus I am going to repeat some of the information as wewill be looking at some of these things in more detail. So, let’s startoff by revisitthe naming conventions and samples used.
Naming Conventions and Architecture
A component consists of the Business Object (BO) layer on the COM+ machine, and a Client Object (CO) layer that runs on the client machine. The CO functions as a smart proxy between the application and the BO layer. A component on the CO layer does the following:
- It gathers configuration information in Plug & Play style (the focus of this article)
- It functions as a wrapper around business logic in the BO component
- If provides a stateful COM component to the application, shielding the application from any interaction with BO components
I use the following naming conventions to easily distinguish between classes on different levels.
- On CO level
- Project name gets the suffix CO
- On BO level
- Project name gets suffix BO
- Class name gets suffix SVR (Server)
- Classes get the prefix cls
- Collection classes gets the prefix col
Figure 1 shows this architecture and naming conventions applied for our sample project, Client.
Figure 1: Architecture and naming conventions applied to Client project
While the two sample projects are the same as in the previous article, the code for the samples are not the same. In the previous code samples, I removed the functionality andcomplexity associated with the System class (the class discussed in thisarticle), while it is obviously added to the samples for this article.
System 1: Client
This component maintains basic client information. The component consists of the following Visual Basic projects and classes:
- Project: ClientCO
- Project: ClientBO
The client table has the following fields:
Figure 2: Client Table
Project 2: Contract
This component maintains basic information regarding insurance contracts. It also maintains the link between a contract and all the clients associated with that contract. It will reuse our client component for this task. The component consists of the following Visual Basic projects and classes:
- Project: ContractCO
Figure 3: Contract Tables
Reuse Grading System
Reuse means different things to different people. This is most probably the reason why the word “reuse” is frowned upon by many IT people, especially project managers and the guys higher up in the project tree. These guys normally see reuse as pure “black-box” reuse. Every developer however has his/her own view about reuse, ranging from the “black-box” view to cut-and-paste of code or even just using the same flowcharts to iron out the logic to be used. The result is that, too often a piece of code is labeled “reusable”, when in practice it takes more man-hours to reuse than to develop from scratch.
At SDT we use a grading system to indicate what level of reuse can be expected from a certain component. This system serves two important functions. Firstly it sets a standard when developing a new component. Secondly it informs the user (or re-user) what can be expected from this component, and how much work will go into getting the component/codeintegrated into his/her system.
Here is a summary of the six levels of reuse we use to grade components:
- Concept Reuse: The conceptual design of two systems correspond but there is no code reuse, or if code is reused, it does not compile without changes.
- Table Reuse: The conceptual design of two systems correspond and exactly the same table layouts will be used to store data. There is no code reuse, or if code is reused it does not compile without changes.
- Limited Code Reuse: The reuse of a concept to the extent where the conceptual design of two systems correspond and exactly the same table layouts will be used. The code that is reused comes in encapsulated modules or classes and compiles without any changes. This code does not address business issues but rather data manipulation and persistence.
- Business Logic Code Reuse: The reuse of business logic to the extent where the conceptual design of two systems correspond and exactly the same table layouts will be used. The code that is reused comes in encapsulated modules or classes and compiles without any changes. The code that is reused addresses business issues.
- Binary Reuse: The reuse of business or data manipulation logic to the extent where the conceptual design of two systems correspond and exactly the same table layouts will be used. Code reuse is on a pure binary level and is language independent. Objects are exposed via a well defined COM interface. Any object must be able to persist its state to or load it from an XML structure. All 3 Tier objects must adhere to the design as described in the above mentioned article. Configuration information must be persisted to and from a repository and collected automatically when needed.
- Binary Reuse with User Interface reuse: The same as level five, with an extra DLL added providing access to a reusable user interface. In the case of our client database sample, this would be a very simple screen allowing you to manipulate client data, plus maybe a search screen etc. A user interface must be supplied to enable input/editing of configuration information. Note that all user interfaces has to conform to some standards to ensure that the look and feel fits into that of external systems using this interface.
|Note: The grading system was developed by SDT over time, so from a binary reuse perspective some of the lower layers may not make sense, but it suites the needs we have at SDT. I included all the levels here for the sake of completeness, plus to give you an idea of what we see as reusable, and what not.|
Binary reuse occurs only at levels five and six. All components developed by SDT has to be at least on level five. As you can see, we do not really view “code reuse” as reuse. The reasons for this because reused code has to be tested and maintained in multiple systems. We aim for binary reuse, and ultimately we want to reuse the user interface as well (level six).
A component on level five or six must behave like a typical “Plug &Play” card you slot into your computer. The first time it is used ina specific context, the component must collect all the configuration information needed,either from available repositories or by popping up a user interface andprompting the user to supply the information. Thisinformation must then be persisted to a repository where it can be accessed thenext time the component needs it, so that in future no user interaction is necessaryto get the component configured and up and running.
The functionality for making a component as easy to use as a “Plug &Play card” is implemented in clsSystem. Every project (DLL orOCX) has one system class. The system class encapsulates all configuration information needed by any class in the project. My sample projects are very basic, so in the project “Client”, this configuration information is limited to a database connection string. We implement this as the propertyclsSystem.ConnectionString. This property initializes itself bycalling a function FetchConnectionInfo whenever it detects that noconnection string is present. The value of this property is stored in a global named gstrConnectionString. The value of this variable is globally accessible in the ClientCO project. This means that all the instances of clsSystem inside ClientCO shares this variable to maintain the connection string
The property procedures for ConnectionString will look something like this:
|Public Property Get ConnectionString() as String
If Len(gstrConnectionString) = 0 Then Call FetchConnectionInfo
ConnectionString = gstrConnectionString
Public Property Let ConnectionString(pstrInfo as String)
The SystemName property
OK, so when ConnectionString is requested, it will be fetched fromsomewhere. Nice, but what happens when I have multiple systems using thiscomponent and they all use different connection strings?
A standard property called SystemName is introduced into clsSystem. SystemName basically functions as the key for accessing/storingconfiguration information. The default value of this property is normallyhardcoded to the project name, so in the case of sample 1 it will be”Client” The value of this property is also stored in a globalvariable. The code for this property follows:
|Public Property Get SystemName() as String
If Len(gstrSystemName) = 0 Then gstrSystemName = “Client”
SystemName = gstrSystemName
Public Property Let SystemName(pstrName as String)
Note that changing the SystemName property clears the value of ConnectionString,causing ConnectionString to fetch a new value the next time it isused. When using more parameters dependant on the system name, they shouldbe cleared here as well.
The above setup means that we can have Application A and Application B using our Client project, both running on the same client PC and using the same COM+ box. Application A and B can both run simultaneously on the same client PC, as it will run two different instances of the Client project, each with it’s own instance of global data. Application A sets ClientCO.clsSystem.SystemNameto ‘A’ and configuration information is fetched using key ‘A’. ApplicationB sets ClientCO.clsSystem.SystemName to ‘B’ and configuration informationis fetched using key ‘B’.
We can also start up another instance of Application A, change the SystemName property to ‘AnotherA’ to load other connection information, and thus have more than one instance of Application A running onone client PC, but using different sets of connection information.
Our Contract project also has a clsSystem using the same internal logic as the Client project. This means that somewhere in the repository there is connection information stored for the Client and the Contract project. When clsClient is used inside the Contract project, the connection information for Client can be initialized in the following ways:
- Entries in the repository exists for both Client and Contract. The Client system loads it’s own connection information from the repository using the key “Client”. This approach can be used when the two systems is not tightly integrated and normally does not use the same database to store it’s data.
- Entries in the repository exists for Contract. The Contract system sets the SystemName of the Client system to “Contract”. Both the Contract and Client systems fetches the connection information from the repository on first use of this property.
- Entries in the repository exists for Contract. The Contract system sets the SystemName of the Client system to “Contract”. The Contract system fetches the connection information and updates Client’s connection information property.
Which of above you use depends on what you want to accomplish and how your systems fits together. I use the last option in most cases as my systems tend to share the same databases. ()
|Note on multiple databases. Ever tried convincing a client that he needs to restore synchronized versions of 4 different databases? The moment they understand why, they normally tell you that their backup frequencies differ, and that it is all your problem now. Rather use a unique table naming scheme. That way you can put all the tables of all the reusable components making up one system into one database. This way a backup of a system that links contracts to a client will always be in synch with the tables of the system containing the clients. Your apps are developed to each use its own database, but when deployed all tables reside in the same database. The naming convention we adopted is as follows. Each object in the database of a specific system is prefaced with a three character string. This applies to tables, stored procedures, views etc. The client system’s prefix of our sample can be ‘CLN’, while the contract system’s prefix can be ‘CON’. That way both can have a table named ‘Whatever’, but by using the prefix you will never have duplicate names, plus you will know at a glance to which system the table belongs. The ‘Whatever’ tables will be named ‘CLN_Whatever’ and ‘CON_Whatever’.|
A system class normally has much more properties than the very basic examplewe have here. Not all of theseproperties needs to be persisted though, as these properties can normally begrouped as either:
- Configuration information that is dependant on the system name and needs to be persisted/fetched from a repository such as the connection information.
- Information properties that is unique per application instance and has to be created or set every time. A user id is an example of information that will be unique per application instance. This information is normally set only once per application. It does not need to be persisted though, as the next login session will determine the id of the logged in user.
Maintaining State in the System Class on CO level
In a typical Application using the Client system you would want to set the connection information and system name only once, and then have the systemclass maintain it for the duration of the application. All you have to do to accomplish this is to instantiate one instance of ClientCO.clsSystem and to maintain a reference to this instance for the duration of the application (in other words, declare a global variable of type “ClientCO.clsSystem”, instantiate it when your application starts up and only destroy it once your application shuts down). The system class is a relatively lightweight class and this should have no significant impact on your memory usage.
Easy Access to the System Class
When using Client from within another project in Visual Basic, Intellisense helps to create and set the properties in your system class. It is also easy to maintain the state for the duration of the application as explained above. When using Client from an ASP application, this becomes slightly more problematic. Firstly, depending on the amount of hits you expect, it may or may not be a good idea to maintain an instance of ClientCO.clsSystem for the duration of the session. Let’s assume you want to keep your connection information in an Application Variable on IIS. This works quite well, as you only need to fetch the connection information once at application startup. Using the functionality provided by clsSystem, it is relatively easy as well, as you only need to instantiate an instance of clsSystem and copy the content of the ConnectionString property into an application variable. (Note that there are some limitations on the kind of repository you can use, but more about that later)
When writing your ASP page, you need to instantiate an instance of ClientCO.clsSystem in order to set the connection string property, then you need to instantiate ClientCO.clsClient to load and work with your Client object. While this may sound pretty straightforward, using “CreateObject” and having to create two classes in order to work with only one does getsilly after a while, especially if you want to use four different components andnow have to instantiate four system classes as well. This prompted a change to classes such as clsClient that proved to be very useful for much more than just ASP development. Every class in my CO project (except for clsSystem) gets a modular instance ofclsSystem which is exposed as the property System, and loads itself on first reference. This means that I add the following code to ClientCO.clsClient
|Private mobjSystem as ClientCO.clsSystem
Public Property Get System() as ClientCO.clsSystem
In my ASP application I can now instantiate only an instance of clsClient, set the connectionstring and start using the functionality of clsClient. VBScript code for this would typically look as follows:
| Set lobjClient = Server.CreateObject(“ClientCO.clsSystem”)
lobjClient.System.ConnectionString = Application(“ConnectionString”)
set lobjClient = Nothing
CO classes that needs access to information variables now also has easy OOaccess to these variables.
The System Class on BO Level
True to the nature of our 3 Tier design, clsSystem has it’s BOcounterpart called clsSystemSVR. On BO level privatemodular variables is used to store configuration information. As is the case on COlevel, every object on BO level has a System property, which returns aninstance of clsSystemSVR. So, in a typical call from ClientCO toClientBO, the state of clsSystem is packed into XML and unpacked into clsSystemSVR. ClientBO uses the configuration information to connect to the database etc. Atypical call from CO to BO uses the system class as follows:
|/// CO level
Function Load (plngID as Long)
lstrSystemXML = Me.System.Pack
lobjBO.f_Load(pstrSystemXML as String, ….)
Note that the state of the system class is passed ByVal, as the BO level doesnot change information in the system class. In some instances it is necessaryto update information in the system class from BO to CO level. Limit thisas far as possible, as ByRef parameters adds an additional network roundtrip every timeit is used.
On BO level no global variables are used, plus no functionality exists toautomatically fetch values as they are needed. When you use (or reuse) acomponent on BO level, you must supply it with the necessary configurationinformation. You have seen how the CO level supplies the BO level withthis info. An example of the BO supplying another BO level component is when you expose a class B as aproperty of class A. In the following code snippet, clsContractClientSVRexposes clsClientSVR as a property that will be loaded on firstreference:
|Public Property Get Client () as ClientBO.clsClientSVR
if mobjClient is Nothing Then
After creating an instance of mobjClient, the configuration information ispassed on to it using the XML pack and unpack functions. When the twoobjects are in the same project however, it is a much better option to simplyexecute
|Set mobjClient.System = Me.System|
|A note on why I choose to pass all the configuration information between BO and CO level with every call (and taking up bandwidth), as opposed to maintaining it centrally on the BO side:
The aim of the components is to be reusable, and I have no guarantee as to which system is using it, or to which database they it will be connecting. Most of our clients run these components from multiple apps to multiple databases while using one COM+ box, making a central store difficult to maintain. Fetching this information on a call by call basis on BO level also imposes an overhead on the BO classes, while the CO classes can maintain state and thus only has to fetch this information once. Passing this information into the SPM and managing it using some kind of key per CO instance can be used as an alternative to the way I use it.
|Objects developed in VB lives inside a Single Threaded Apartment (STA). Global variables are scoped at STA level rather than process level, so they are available to all objects sharing an STA. In a normal VB client application using DLLs, only one STA normally exists, so synchronization of global etc. is easy to manage and debug. On a COM+ box each server package process maintains a pool of STA threads. COM+ matches objects with STA threads in an arbitrary way, so you have no guarantee as to which clients will be sharing threads and thus sharing globals. This means that you should approach global variables on BO level with caution. As far as Read/Write properties go, you cannot rely on a global variable to share data. For Read/Write data that needs to be shared, use the Shared Property Manager (SPM).|
So, where do you store data that needs to be:
- Centrally administered
- Accessible from all workstations on the network
Active Directory seems to be the ideal answer to all the above questions. The problem with using Active Directory however is that:
- Our clients are still migrating from an NT4 domain to Active Directory, and we have to wait for the slowest.
- Some of our reusable components are used on standalone systems running on Windows Professional, which means no domain is present.
This means we are currently using INI files and the registry asrepositories. As basic as INI files may be, they are still the firstchoice of our customers. When we need to fetch information, we first attempt to fetch it from the INI file. The INI file needs to be in the same directory as the DLL. Remember that we use SystemName as the key for fetching information, sothe file must be named “SystemName”.INI. This means that, for the Contract sample we used, in the same directory as “ContractCO.DLL” there needs to be an INI file called “Contract.INI”. In the same directory we can have amultitude of INI files, and by changing the SystemName property,configuration information will be fetched from different INI files.
One of the entries in the INI file is the address of a central INI file. If this entry points to a valid central file, the entries in the local INI file is ignored and the entries in the central INI file is used.
Should the local INI file not exist, we attempt to fetch the information from the registry using the normal Visual Basic functions “GetSetting” with the key (AppName parameter) as “SystemName”.
|With the move towards XML we will be upgrading from INI files to XML files in the near future. While XML will allow us to store more levels of hierarchical data, the basic functionality still stays the same.|
When using a level five reusable object, when changing the SystemName, theprogrammer has to test whether valid configuration information was found. When using the reusable front end from a level six reusable object however, it pops up a screen prompting the user to supply the relevant configuration information. This information is then persisted to the INI file or the registry depending on user preference. AnOCX goes even further and must pop up configuration screens whenever an instanceis dragged onto a form (in the development environment) and no configurationinformation can be found.
To show you how this all fits together, let’s expand our sample project a little bit and add an OCX with self-configuration abilities. Let’s say our OCX will display client data from our Client sample and lets call the project ClientOCX and the control ctlClient. Firstly, the OCX will have a reference to an instance of ClientCO.clsSystem layer throughout it’s lifetime, so add a property System to ctlClient. Declare a private modular object of type ClientCO.clsSystem and expose it as the propertyctlClient.System. (Provide both a Set and Get property procedure, as you may want to set the object to an existing instance of ClientCO.clsSystem from some applications.)
|Private mobjSystem as ClientCO.clsSystem
Public Property Get System() as ClientCO.clsSystem
Public Property Set System(pobjSystem as ClientCO.clsSystem)
When you place a user control on a form, the UserControl_InitProperties event is fired, so this seems like a good place to check for valid configuration information. If the existing VB project and form is opened up on another PC however, the UserControl_InitProperties does not fire again, so this guy will miss out on all this logic we want to implement, so we rather put the logic in the UserControl_Initialize event, which occurs every time a control is created or re-created. The logic is pretty simple. If the connection string property is empty, we want to pop up a form to prompt the user to supply the information. We add a form to the ClientOCX project with the capability to display/capture configuration information. I use the following screen as a basis for most of my configuration screens.
You want the configuration form to pop up only when you are inside VB in design mode. To determine if you are inside VB in design mode, use the UserMode property of the Ambient object that is available inside the user control.
The logic in the UserControl_Initialize event follows:
|Private Sub UserControl_Initialize()
Dim frm as frmSetup
If Len(Me.System.ConnectionString) = 0 and Ambient.UserMode Then
Now another issue pops up. The above works fine when you are using the default system name, but when you want the control to start up using the configuration of its parent, which is the Contract system, you want to fetch the connection string only after you have set the system name. As the system name is the key to which repository to use, you have to persist the system name to some other central repository. You can use different repositories for this but I suggest using the property bag. The property bag travels with the form on which the control is located, which places low admin on you as a developer, which is always good. In a user control the ReadProperties event is used to initialize properties that needs to be persisted, so this is the right place to get the data for the system name. The snag however is that the ReadProperties event fires only when you open up/load a form with an existing control on it, which means that if you move the above code to the ReadProperties event, you can collect the correct system name before fetching the correct configuration information from the property bag, but this will not happen until you once close down and re-open the form. If you do not like this snag, you can implement another repository which is accessable in the Initialize event. The code for using the property bag follows:
|Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
Me.System,SystemName = PropBag.ReadProperty(“SystemName”, “ClientCO”)
If Len(Me.System.ConnectionString) = 0 and Ambient.UserMode Then
Remember the “happy faces” I mentioned in the firstparagraph? We have a workflow component in the typical 3-Tier environmentwith an OCX provided to display/configure route and item informationgraphically. A developer had to integrate this into an existing productand asked me to assist to get the component configured and functioning. Sowe added the reference to the CO and OCX and then dragged the OCX onto aform. Immediately (OK, after closing and opening the form) a wizard popped up and guided the guy through the necessarysetup steps. When he pressed OK at the end, my work was done. Nowthere was a guy with a happy face.
|A note on object reuse
Depending on the type of applications you develop, object reuse on a binary level is not always feasible . I develop mostly in a vertical market where the applications tend to share a lot of logic. The applications reminds me of an octopus, with the tentacles being reusable subsystems and the body custom developed using core business logic linked with these subsystems in order to accomplish what is required of it. So, in my environment reuse can be applied and makes a lot of sense. What about an environment where your one system is always different to the next? Well, I’m not an OOP guru, and I surely don’t always follow all the OOP rules listed by the OOP gurus, but I do strive towards “strong cohesion” and “loose coupling” in my routines, objects and subsystems.
If you strive towards creating routines/object with internal integrity (strong cohesion) and flexible relations to other routines/objects (loose coupling), you will find that very soon your system will consist of “virtual subsystems”. This is the starting point of reuse although it will most probably be “code reuse”. To move onwards to “binary reuse” the coupling has to be loose enough so that the “core system ” has knowledge about the subsystem it is using, with the subsystem being totally independent of the core system. (This introduces a level of complexity we have not touched on yet, namely what if the subsystem needs to inform the core system of changes etc. This will be the focus of another article however)
While from a business perspective it may not make sense to reuse parts of the system you are developing, I would advise you to apply design patterns that will allow reuse, whether business require it or not. Business requirements change without your assistance, while code unfortunately does not mutate over night to fit into new business requirements. As I said in the previous article, one of the biggest lessons I have learned since I started programming is: Find a design pattern (or a set of them) that suits the environment you are in, understand it fully, and apply it!