- Rule 2-1: Think in Terms of Interfaces
- Rule 2-2: Use Custom Interfaces
- Rule 2-3: Define Custom Interfaces Separately, Preferably Using IDL
- Rule 2-4: Avoid the Limitations of Class-Based Events with Custom Callbacks
- Rule 2-5: Be Deliberate About Maintaining Compatibility
- Rule 2-6: Choose the Right COM Activation Technique
- Rule 2-7: Beware of Class_Terminate
- Rule 2-8: Model in Terms of Sessions Instead of Entities
- Rule 2-9: Avoid ActiveX EXEs Except for Simple, Small-Scale Needs
Rule 2-7: Beware of Class_Terminate
Two of the most heavily used classes in VB are probably Connection and Recordset, members of the ADO object model. Like most object-oriented classes, these two classes have destructors similar to Class_Terminate. In other words, they have methods that are automatically triggered when an instance of the class is about to be destroyed. Destructor methods are typically used to clean up before an object's state is lost forever, which would seem like the perfect place to handle things like saving changes to a database. So why is it, then, that Connection and Recordset have explicit Close methods that we have to call ourselves?
The answer is that some resources are too important to leave open until the client (or the run-time environment) gets around to triggering the object's destructor.22 In other words, in VB, the destructor is triggered when an object is no longer referenced by any client:
Dim rs As ADODB.Recordset Set rs = New ADODB.Recordset . . . Set rs = Nothing '** destructor is triggered at this point, assuming '** we didn't pass the reference to anyone else
This occurs when all references have been set to Nothing. For database classes like Connection and Recordset, which may be allocating memory and setting locks in the database, unnecessary delay in performing cleanup may waste precious resources and may hurt performance. Hence the explicit Close method:
Dim rs As ADODB.Recordset Set rs = New ADODB.Recordset rs.Open ... . . . rs.Close '** cleanup performed here Set rs = Nothing '** destructor triggered here
Even though the client is responsible for calling Close (and thus may forget), its use can be documented as necessary for correct behavior. Regardless, it provides a solution to the problem of timely cleanup for those able to use it properly.
What does this mean to you? First of all, as a consumer of objects, you must be careful to use other classes properly. Look for methods entitled Close or Dispose, and be sure to call them as soon as you are done using that object. In particular, be careful to call these in your error handlers as well. For example, here's the proper way to ensure that both a connection and a recordset are closed, even in the presence of errors:
Public Sub SomeTask() On Error Goto errHandler Dim dbConn As ADODB.Connection Dim rs As ADODB.Recordset Set dbConn = New ADODB.Connection Set rs = New ADODB.Recordset . . . rs.Close : dbConn.Close Set rs = Nothing : Set dbConn = Nothing Exit Sub errHandler: If rs Is Nothing Then Else If rs.State <> adStateClosed Then rs.Close Set rs = Nothing End If If dbConn Is Nothing Then Else If dbConn.State <> adStateClosed Then dbConn.Close Set dbConn = Nothing End If Err.Raise ... End Sub
Second, as a producer of classes, you need to decide whether VB's Class_Terminate event is sufficient for your cleanup needs. If not, then you need to incorporate an explicit Close or Dispose method in your class design.
The current convention is to provide a Close method if your design allows an object to be reopened or reused after it has been closed. Otherwise, provide a Dispose method, which implies to your clients that the object is no longer usable once Dispose has been called. Implementing these methods is easy; the hard part is deciding when your classes need them.
The obvious examples are classes that open and hold on to operating system or other resources: files, shared memory, network connections, and ADO Connection and Recordset objects. If you find yourself opening these types of resources in your class's Class_Initialize event (or in an explicit Open method) and accessing them via private class variables, then you most likely need a Close or Dispose method. For example, you may design a data access class that automatically logs every access via a private ADO connection:
'** class module: CDataAccess Option Explicit Private dbLog As ADODB.Connection '** for logging accesses Public Sub Open() Set dbLog = New ADODB.Connection dbLog.Open "<proper connection string>" End Sub . . . Public Sub Close() dbLog.Close Set dbLog = Nothing End Sub Private Sub Class_Terminate() Close '** in case client forgets... End Sub
Of course, keep in mind that although maintaining a dedicated logging connection may be efficient, it is certainly wasteful of an important (and usually limited) resource. In fact, we do not recommend the previous design for objects that may live in the middle tier, and thus need to scale (e.g., see rule 5-3). However, if such a design is appropriate in your case, then remember to think twice before relying solely on Class_Terminate for cleanup.