Implementing Factory Methods
A subject indirectly related to Singleton objects is the use of factory methods. From the last chapter, you know that the Singleton idiom is implemented by using a shared method to ensure the creation of one object, and defining a private constructor to ensure external consumers can't create objects without the help method. These helper methods are often referred to as factory methods.
A factory method is a shared method whose job it is to create instances of objects. Factory methods are used to instantiate Singleton objects, and are used to orchestrate object construction to facilitate the proper initialization of objects. A perfect example of a factory method is the CreateGraphics method. You can't instantiate a GDI+ Graphics object; you must request one from the CreateGraphics method (see Listing 11.5). (In the case of the Graphics object, a factory method was probably used because graphics are a limited and expensive resource.)
Factory methods don't have to be shared members of a class. In fact, in VB6 they couldn't be because we had no equivalent of the Shared modifier. In VB6, factory methods had to be defined as regular functions in a module. Visual Basic .NET supports shared members, allowing us to implement the factory method idiom as a member of a global function.
Martin Fowler's book Refactoring: Improving the Design of Existing Code provides a general motivation for the refactoring "Replace Constructor with Factory Method." However, in general, you can use a factory method whenever you find yourself repeatedly writing the same couple of lines that create and initialize an object.
A factory method is a function that takes the necessary arguments to initialize an object. The factory method creates the designated object and uses passed parameters to initialize the object, returning the instance of the properly constructed object in the Return statement.
To compel a consumer to use a factory method, you can define a constructor as protected or private and make the factory method shared and public. As a consequence, the only way to create an object would be to call the factory method and pass the correct arguments. Sometimes you can accomplish the same thing by introducing a parameterized constructor; however, if the class in question can't be subclassed, you can use a factory method to ensure proper initialization.
Listing 11.8 defines two factory methods that hide the details of initializing an ADODB.Recordset and an ADODB.Connection. The example demonstrates an alternative solution for storing application configuration options in a text file by using an Access database.
Listing 11.8 Two factory methods for creating and initializing a Connection and Recordset object
1: Private Function ConnectionString() As String 2: Return "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & _ 3: Application.StartupPath & "\ Options.mdb" 4: End Function 5: 6: Private Function Connection() As ADODB.Connection 7: 8: Dim C As New ADODB.Connection() 9: C.ConnectionString = ConnectionString() 10: C.Open() 11: Return C 12: 13: End Function 14: 15: Private Function Recordset(ByVal AConnection As _ 16: ADODB.Connection) As ADODB.Recordset 17: 18: Dim R As New ADODB.Recordset() 19: R.Open("Options", AConnection, _ 20: ADODB.CursorTypeEnum.adOpenDynamic, _ 21: ADODB.LockTypeEnum.adLockOptimistic) 22: 23: Return R 24: End Function 25: 26: Private Sub DoAddOption(ByVal R As ADODB.Recordset, _ 27: ByVal C As ADODB.Connection, _ 28: ByVal Key As String, ByVal Value As String) 29: 30: R.AddNew() 31: R.Fields("Key").Value = Key 32: R.Fields("Value").Value = Value 33: R.Update() 34: R.Close() 35: C.Close() 36: 37: End Sub 38: 39: Private Sub AddOption(ByVal C As ADODB.Connection, _ 40: ByVal Key As String, ByVal Value As String) 41: 42: DoAddOption(Recordset, C, Key, Value) 43: 44: End Sub 45: 46: Private Sub Button1_Click(ByVal sender As System.Object, _ 47: ByVal e As System.EventArgs) Handles Button1.Click 48: 49: AddOption(Connection(), "LastFileName", "file.txt") 50: 51: End Sub
The factory methods are the Connection and Recordset functions started on lines 6 and 15, respectively. Each factory method declares an initialized instance of its respective objects. If you define a factory method as a member of a class, and that method returns an instance of the containing class, the method will be more useful if it's a shared method. Suppose, for example, that we defined the Options class from Listing 11.6 to be used by instances, and then we could define a factory class method to return an instance of the Options class as follows :
Public Class Options Public Shared Function CreateOptions() As Options Return New Options End Function ... End Class
The preceding example could be called as an initializer: Dim MyOptions As Options = Options.CreateOptions. This statement demonstrates using the factory method, and roughly approximates the behavior of a constructor call. The benefit derived from a factory method is attained when you include necessary, additional initialization before returning the object, as demonstrated in Listing 11.8.