Using an Abstract Base Class

for RuBoard

As we've discussed elsewhere in this book, the fact that the common language runtime and thus managed languages can use implementation inheritance enables you to take advantage of polymorphism and write code that is both reusable and generic. It comes as no surprise that you can utilize this feature when you build your data access classes.

In the following sections, you'll learn how to create a base class for both non-serviced and serviced components . These base classes will incorporate some commonly implemented features and will be reusable across projects.

Creating a Base Class

When designing a reusable base class, you first need to understand its benefits and think clearly about what kinds of services it should and should not provide.

Of course, the fundamental reason to create a base class is so that developers deriving from the class can increase their productivity by not having to reinvent the wheel in order to handle issues such as logging. At the same time, the base class can be used to ensure that core services such as connection management, logging, and exception handling are handled in a standard fashion across your organization. For that reason, a data access base class should implement only features that are common to all your data access classes. In other words, it should not assume that its descendants will always work with DataSet objects as opposed to data readers, or that the descendants will always be running from within a particular environment such as ASP.NET. Your goal should be to design the base class to be as generic as possible and yet to provide useful services.

To illustrate these concepts, consider the base class shown in Listing 17.1.

Listing 17.1 A data access base class. This base class implements connection management, logging, and exception handling.
 Option Strict On Imports System.Data.SqlClient Namespace ComputeBooks.Data   Public MustInherit Class ComputeBooksDABase : Implements IDisposable     Private _connect As String     Private _daSwitch As BooleanSwitch     Private _sqlCon As SqlConnection     Private _disposed As Boolean     Protected ReadOnly Property Disposed() As Boolean       Get         Return _disposed       End Get     End Property     Protected ReadOnly Property SqlCon() As SqlConnection       Get         Return _sqlCon       End Get     End Property     Protected ReadOnly Property SwitchEnabled() As Boolean       Get         Return _daSwitch.Enabled       End Get     End Property     Public Property ConnectString() As String       Get         Return _connect       End Get       Set(ByVal Value As String)         _connect = Value         Me.SqlCon.ConnectionString = _connect         Me.WriteTrace("ConnectString set to" & _connect)       End Set     End Property     Public Sub New(ByVal connect As String)       _initClass()       Me.ConnectString = connect       Try         Me.SqlCon.ConnectionString = Me.ConnectString       Catch e As Exception         Me.ThrowComputeBookException("Could not set the connection string", e)       End Try     End Sub     Public Sub New()       _initClass()     End Sub     Private Sub _initClass()      _sqlCon = New SqlConnection()      _daSwitch = New BooleanSwitch("daSwitch", "Data Access")       Me.WriteTrace("Instance created")     End Sub     Protected Sub WriteTrace(ByVal message As String)       ' Write a message to the trace if the switch is enabled       Trace.WriteLineIf(Me.SwitchEnabled, Now & ":" & message)     End Sub     Public Sub Dispose() Implements IDisposable.Dispose       Dispose(True)     End Sub     Protected Sub Dispose(ByVal disposing As Boolean)       If disposing Then         ' Clean up SQL Connection         If Me.SqlCon.State = ConnectionState.Open Then           Me.SqlCon.Close()         End If         _sqlCon = Nothing       End If       Me.WriteTrace("Disposed")       Trace.Flush()      _disposed = True       ' Make sure Finalize does not run       GC.SuppressFinalize(Me)     End Sub     Protected Overrides Sub Finalize()       Dispose(False)     End Sub     Protected Sub ThrowComputeBookException(ByVal message As String, _       ByVal e As Exception)       Dim newMine As New ComputeBookDAException(message, e)       ' Trace the error       Me.WriteTrace(message & "{" & e.Message & "} ")       ' Throw       Throw newMine     End Sub   End Class End Namespace 
graphics/analysis.gif

As you can see in Listing 17.1, the ComputeBooksDABase class is placed in the ComputeBooks.Data namespace. The class follows the naming conventions suggested in the online documentation, where organization name is at the highest level followed by the functionality. In this case, this class would also likely be compiled into its own assembly so that other developers (perhaps even working in different languages) could reference it in their projects. The classes that developers would derive from the base class might then be placed in namespaces directly under the ComputeBooks.Data namespace, such as ComputeBooks.Data.OrderProcessing . Once again, this illustrates that the base class should be generic so that it can be reused across projects.

Note

Alternatively, the base class could be compiled into a .NET module (using the command-line compiler) and then used in multi-file assemblies built with the Assembly Linker (AL.exe) command-line utility. However, it would be far easier to compile it into an assembly along with any other interfaces or classes that might be used by data access developers in your organization. With an assembly, you also have the option of placing it in the Global Assembly Cache (GAC) so that multiple applications on the same machine access the same version.


You'll notice that the class is also marked as MustInherit ( abstract in C#) so that instances of it can't be created directly. This makes sense because the class provides only auxiliary services and therefore can't be used in a standalone mode. Because the class is abstract, the name has been appended with "Base" as is recommended in the design guidelines in the online documentation.

The class itself contains the Disposed , SqlCon , SwitchEnabled , and ConnectString properties, along with the Dispose , WriteTrace , and ThrowComputeBookException methods . The first three properties are read-only because they are created by the base class. They are also protected so that they can be seen only by a descendant of this class and are not available publicly . The ConnectString is specified either through the overloaded constructor or using the property. In total, the base class provides the following services:

  • Connection Management . The private _initClass method creates a new SqlConnection object when an instance of the class is created and returns it through the protected SqlCon property. In addition, the connection string to use is exposed through the ConnectString property and can be set in the constructor. In this way, derived classes needn't worry about creating a connection object or associating it with the connection string. In addition, the class implements the IDisposable interface so that clients have the option (although they don't have to use it) of disposing of the SqlConnection object. The inclusion of both the private and public Dispose methods follows the pattern shown in the online documentation.

  • Tracing . You'll notice that the class includes a private variable called _daSwitch that is instantiated as a BooleanSwitch object in the _initClass method. This object is used in conjunction with the SwitchEnabled property and protected WriteTrace method to provide a way for the base class and its descendants to conditionally write tracing information to a log. (We'll discuss this feature in more depth in the following section.)

  • Exception Handling . In conjunction with a custom class called ComputeBookDAException , the base class handles wrapping exceptions that are caught into instances of the custom exception class and throws them back to the caller in the protected ThrowComputeBookException method. (We'll discuss ComputeBookDAException in more detail shortly.)

After a class inherits from this base class, it has access to all of the public and protected members .

Conditional Tracing

As you learned in the previous section, one of the base class's services is the use of conditional tracing. This is accomplished by using the Trace and BooleanSwitch classes from the System.Diagnostics namespace.

The Trace class is used to instrument your application to output diagnostic information so that developers and administrators can pinpoint performance problems or validate the input and output of methods. In addition, tracing is helpful for understanding what is happening in nonvisual code segments like those in the data services tier . The Trace class exposes Write , WriteIf , WriteLine , and WriteLineIf methods that can be used to write data to one or more listeners. You can see that the WriteTrace method of the ComputeBooksDABase class uses the WriteLineIf method.

graphics/newterm.gif

Listeners are objects derived from TraceListener and are exposed through the Listeners property of both the Trace and Debug objects (these objects share the underlying TraceListenerCollection ). By default, the only object in the collection is an instance of DefaultTraceListener that writes both the Trace and Debug output to the Output window within VS .NET. However, applications can add new listeners to the collection to redirect the output. In fact, TraceListener serves as the base class for the EventLogListener and TextWriterTraceListener classes, which write output to the event log and to a file or stream, respectively.

For example, an application can allow the Trace output from the ComputeBooksDABase class and its descendants to be logged to a file by adding a TextWriterTraceListener object to the collection as follows:

 Trace.Listeners.Add(New TextWriterTraceListener("DataAccess.log")) 

The constructor of TextWriterTraceListener also accepts Stream or TextWriter objects.

As you might expect, the extensibility of the .NET Framework also enables you to create your own listener by deriving from TraceListener and overriding its Write and WriteLine methods. This is especially useful if you want to standardize application tracing throughout your organization.

Note

Developing a custom listener can also be a powerful technique because the overloaded Write and WriteLine methods of the Debug and Trace classes also support passing objects directly to the listener. In this way, you can write a listener that abstracts the trace processing of an object so that developers using your listener don't need to be concerned with passing specific data to the listener.


The second part of the conditional tracing technique shown in the base class is the use of the BooleanSwitch object. Basically, switch objects like _daSwitch in the ComputeBooksDABase class enable applications to control whether trace or debug output is displayed. In this case, you'll notice that the switch is instantiated in the _initClass method and is passed two values in the constructor. The first is the DisplayName ; the second is the Description . The DisplayName property is the name associated with the switch throughout the application and used in the application's configuration file to turn the switch on and off. The Description is simply human-readable text.

To use the switch, the client application can include a system.diagnostics element with the application configuration file. For example, a client application that uses a descendant of ComputeBooksDABase might have the following section in its configuration file (Web.config for ASP.NET applications):

 <?xml version="1.0"?> <configuration>  <system.diagnostics>     <switches>         <add name="daSwitch" value="1" />     </switches>     <trace autoflush="true" indentsize="4">     </trace>  </system.diagnostics> </configuration> 

Here, daSwitch is set to 1 , which is on or enabled. By default, if the switch isn't found in the configuration file, its Enabled property will be set to False . In addition, the file here indicates that the AutoFlush property of the Trace object should be set to True so that data will automatically be flushed from the buffer to the underlying stream with each write.

Now you can see how the WriteTrace method works. It simply tests to the Enabled property of the switch object using the protected SwitchEnabled property and then writes the message passed to it to the trace listeners.

Custom Exceptions

The other service that base class encapsulates is the throwing of exceptions. The technique used here is for the base class to use a custom exception class particular to the ComputeBooks data services tier called ComputeBooksDAException , as shown in Listing 17.2.

Listing 17.2 A custom exception. This class is used by the base class and its descendants to wrap exceptions.
 Namespace ComputeBooks.Data   <Serializable()> _   Public Class ComputeBookDAException : Inherits ApplicationException     Public Sub New(ByVal message As String)       MyBase.New(message)     End Sub     Public Sub New(ByVal message As String, _      ByVal originalException As Exception)       MyBase.New(message, originalException)     End Sub     Protected Sub New(ByVal info As SerializationInfo, _      ByVal context As StreamingContext)       MyBase.New(info, context)     End Sub     ' Add custom members here   End Class End Namespace 
graphics/analysis.gif

Like the base class, the ComputeBooksDAException class exists in the ComputeBooks.Data namespace, although it derives from ApplicationException . Because it inherits most of its functionality from ApplicationException , it simply needs to define constructors that, in this case, accept either a message or a message in conjunction with the original exception that was thrown.

Note

The class is marked with the Serializable attribute so that it can be serialized across application domains by the common language runtime in the event that this exception is raised in code running remotely. You'll note that the class also includes a protected constructor that assists in the serialization process.


The protected ThrowComputeBookException method then uses this class by creating a new instance of it and throwing it back to the caller. An example of how this technique is used can be seen in the constructor of ComputeBooksDABase that accepts the connect string. There, the code uses a Try Catch block to catch exceptions raised by setting the ConnectionString property of the SqlConnection object. If one is caught, it is encapsulated in a ComputeBookDAException and is thrown. In this way, clients can differentiate exceptions coming from the base class and its descendants from other classes using their own Try Catch logic.

Handling Serviced Components

The base class shown in Listing 17.1 works wonderfully when your data access classes don't rely on Component Services for distributed transactions, object pooling, and other services. For a class to use these services, it must inherit directly or indirectly from the ServicedComponent class in the System.EnterpriseServices namespace. Because the .NET Framework supports only single inheritance, classes derived from ComputeBooksDABase , as shown in Listing 17.1, can't use Component Services. As a result, if you know that your classes will use these services, you can create a second base class that mimics the first but that derives from ServicedComponent , as shown in Listing 17.3. Both classes can then be made available in the same assembly so that developers can choose which to derive from, depending on their requirements.

Listing 17.3 Serviced component base class. This class can be used as the base class for serviced components.
 Namespace ComputeBooks.Data   <ClassInterface(ClassInterfaceType.AutoDual), _     Transaction(TransactionOption.Supported), _     EventTrackingEnabled(True), _     ConstructionEnabled(Enabled:=True)> _   Public MustInherit Class ComputeBooksDAServicedBase     Inherits ServicedComponent     Private _connect As String     Private _daSwitch As BooleanSwitch     Private _sqlCon As SqlConnection     Public Sub New()       _sqlCon = New SqlConnection()       _daSwitch = New BooleanSwitch("daSwitch", "Data Access")       ' Add a listener for the Event Log       Trace.Listeners.Add(New EventLogTraceListener("ComputeBooksDA"))     End Sub     Protected ReadOnly Property SqlCon() As SqlConnection       Get         Return _sqlCon       End Get     End Property     Public Property ConstructString() As String       Set(ByVal Value As String)         _connect = Value       End Set       Get         Return _connect       End Get     End Property     Protected ReadOnly Property SwitchEnabled() As Boolean       Get         Return _daSwitch.Enabled       End Get     End Property     Protected NotOverridable Overrides Sub Construct(ByVal s As String)       ' Implements object construction       _connect = s       Try         _sqlCon = New SqlConnection(_connect)       Catch e As Exception         Me.ThrowComputeBookException("Connect string cannot be set", e)       End Try     End Sub     Protected Overrides Function CanBePooled() As Boolean       ' Default is that the objects will not be pooled       Return False     End Function     Protected Overrides Sub Activate()       WriteTrace("Activate")     End Sub     Protected Overrides Sub Deactivate()       WriteTrace("Deactivate")     End Sub     Protected Overridable Sub WriteTrace(ByVal eventMessage As String)       ' Writes the event to the Application log       Dim objLog As EventLog       Try         Trace.WriteLineIf(Me.SwitchEnabled, _           Now & ": " & eventMessage & vbCrLf & _createTraceString())       Catch e As Exception         ' Not important so skip it       End Try     End Sub     Private Function _createTraceString() As String       ' Used to write events to the application event log       Dim traceMess As New StringBuilder()       With traceMess         .Append("Class: " & Me.GetType.ToString & vbCrLf)         .Append("Construct String: " & _connect & vbCrLf)         .Append("Activity:" & ContextUtil.ActivityId.ToString & vbCrLf)         .Append("Context:" & ContextUtil.ContextId.ToString & vbCrLf)         .Append("Transaction?" & ContextUtil.IsInTransaction.ToString & vbCrLf)         .Append("Security?" & ContextUtil.IsSecurityEnabled.ToString & vbCrLf)         If ContextUtil.IsInTransaction Then           .Append("TransactionId:" & _             ContextUtil.TransactionId.ToString & vbCrLf)           .Append("Direct Caller:" & _             SecurityCallContext.CurrentCall.DirectCaller.AccountName & vbCrLf)           .Append("Original Caller:" & _             SecurityCallContext.CurrentCall.OriginalCaller.AccountName)         End If       End With       Return traceMess.ToString()     End Function     Protected Sub ThrowComputeBookException(ByVal message As String, _       ByVal e As Exception)       Dim newMine As New ComputeBookDAException(message, e)       ' Trace the error       WriteTrace(message & "{" & e.Message & "} ")       ' Throw       Throw newMine     End Sub   End Class End Namespace 
graphics/analysis.gif

You can see that the ComputeBooksDAServicedBase class is substantially the same as the class shown in Listing 17.1, with the following changes:

  • It inherits from ServicedComponent to give it access to the Component Services infrastructure.

  • It includes attributes from the System.EnterpriseServices namespace to indicate that a dual COM interface is created when the class is placed in Component Services, that distributed transactions are supported, that statistical tracking is enabled, and that the object construction is enabled.

  • It overrides the Construct method of ServicedComponent in order to catch the connection string that is specified in the Component Services Manager.

  • Its constructor automatically creates an EventLogTraceListener so that trace output will also be redirected to the Application event log under the source ComputeBooksDA.

  • The private _createTraceString method collects information about the serviced component through the ContextUtil and CallContext objects so that the WriteTrace method can output this information to the log.

In addition to these differences, you'll notice that the ComputeBooksDAServicedBase doesn't expose a parameterized constructor. It doesn't need to because the connection string will be fed to the object from the Construct method automatically. In any event, serviced components shouldn't expose parameterized constructors because Component Services doesn't know how to handle them.

It's also important to note that even though the base class specifies attributes such as Transaction as defaults, the derived class can and should override them by specifying the attribute as well. For example, the declaration of the following class overrides the transactional behavior because the component requires transactions rather than simply supporting them:

 <ClassInterface(ClassInterfaceType.AutoDual), _ Transaction(TransactionOption.Required)> _ Public Class ComputeBooksStores : Inherits ComputeBooksDAServicedBase   <AutoComplete()> _   Public Sub DoSomething()     ' Do my work     MyBase.WriteTrace("Done doing my stuff")   End Sub End Class 

Note

For more information about using Component Services, see Chapter 9 of my Sams book Building Distributed Applications with Visual Basic .NET .


for RuBoard


Sams Teach Yourself Ado. Net in 21 Days
Sams Teach Yourself ADO.NET in 21 Days
ISBN: 0672323869
EAN: 2147483647
Year: 2002
Pages: 158
Authors: Dan Fox

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net