Factory - multiple interfaces | Contact Editor Link Search Menu Expand Document

Factory return type with multiple foreign interfaces - code extensibility

Consider again the UserWSheet.cls class. Suppose this class is a part of some "UserLogin" library, and some applications already use it. Now, we wish to extend the functionality of the UserWSheet, while preserving the backward compatibility of the library. Perhaps, we now have multiple users saved in a table and want to load a specific user. Assuming that our UserModel class is still satisfactory and does not need to be updated, we need to add new methods for loading/saving data to the UserWSheet, LoadDataRow/SaveDataRow:


Public Sub LoadDataRow(ByVal RecordIndex as Long)
  With this.Model
    .FirstName = ThisWorkbook.Worksheets(this.WSheet).Range("A" & CStr(RecordIndex)).Value
    .LastName  = ThisWorkbook.Worksheets(this.WSheet).Range("B" & CStr(RecordIndex)).Value
    .Login     = ThisWorkbook.Worksheets(this.WSheet).Range("C" & CStr(RecordIndex)).Value
  End With
End Sub

Public Sub SaveDataRow(ByVal RecordIndex as Long)
  With this.Model
    ThisWorkbook.Worksheets(this.WSheet).Range("A" & CStr(RecordIndex)).Value = .FirstName
    ThisWorkbook.Worksheets(this.WSheet).Range("B" & CStr(RecordIndex)).Value = .LastName
    ThisWorkbook.Worksheets(this.WSheet).Range("C" & CStr(RecordIndex)).Value = .Login
  End With
End Sub

Changing signatures of existing methods or interfaces would break backward compatibility, so these changes are prohibited. The new functionality still needs to be exposed via an interface, and since we cannot change the existing one, we create a new one:

IUserStorageV2.cls


Public Property Get Model() As UserModel
End Property

Public Property Set Model(ByVal Instance As UserModel)
End Property

Public Sub LoadData()
End Sub

Public Sub SaveData()
End Sub

Public Sub LoadDataRow(ByVal RecordIndex as Long)
End Sub

Public Sub SaveDataRow(ByVal RecordIndex as Long)
End Sub

The new interface IUserStorageV2.cls also incorporates the old interface functionality that will be used on the new interface. Let us extend the UserWSheet class:

UserWSheet.cls


'''' N.B.: This class must be predeclared

Implements IUserStorage
Implements IUserStorageV2

Private Type TUserWSheet
  Model As UserModel
  WSheetName As String
End Type
Private this As TUserWSheet

IUserStorage interface implementation

Private Sub IUserStorage_LoadData()
  With this.Model
    .FirstName = ThisWorkbook.Worksheets(this.WSheet).Range("A1").Value
    .LastName  = ThisWorkbook.Worksheets(this.WSheet).Range("B1").Value
    .Login     = ThisWorkbook.Worksheets(this.WSheet).Range("C1").Value
  End With
End Sub

Private Sub IUserStorage_SaveData()
  With this.Model
    ThisWorkbook.Worksheets(this.WSheet).Range("A1").Value = .FirstName
    ThisWorkbook.Worksheets(this.WSheet).Range("B1").Value = .LastName
    ThisWorkbook.Worksheets(this.WSheet).Range("C1").Value = .Login
  End With
End Sub

Private Property Get IUserStorage_Model() As UserModel
  Set IUserStorage_Model = this.Model
End Property

Private Property Set IUserStorage_Model(ByVal Instance As UserModel)
  Set this.Model = Instance
End Property

IUserStorageV2 interface implementation

Private Sub IUserStorageV2_LoadDataRow(ByVal RecordIndex as Long)
  With this.Model
    .FirstName = ThisWorkbook.Worksheets(this.WSheet).Range("A" & CStr(RecordIndex)).Value
    .LastName  = ThisWorkbook.Worksheets(this.WSheet).Range("B" & CStr(RecordIndex)).Value
    .Login     = ThisWorkbook.Worksheets(this.WSheet).Range("C" & CStr(RecordIndex)).Value
  End With
End Sub

Private Sub IUserStorageV2_SaveDataRow(ByVal RecordIndex as Long)
  With this.Model
    ThisWorkbook.Worksheets(this.WSheet).Range("A" & CStr(RecordIndex)).Value = .FirstName
    ThisWorkbook.Worksheets(this.WSheet).Range("B" & CStr(RecordIndex)).Value = .LastName
    ThisWorkbook.Worksheets(this.WSheet).Range("C" & CStr(RecordIndex)).Value = .Login
  End With
End Sub

Private Sub IUserStorageV2_LoadData()
  With this.Model
    .FirstName = ThisWorkbook.Worksheets(this.WSheet).Range("A1").Value
    .LastName  = ThisWorkbook.Worksheets(this.WSheet).Range("B1").Value
    .Login     = ThisWorkbook.Worksheets(this.WSheet).Range("C1").Value
  End With
End Sub

Private Sub IUserStorageV2_SaveData()
  With this.Model
    ThisWorkbook.Worksheets(this.WSheet).Range("A1").Value = .FirstName
    ThisWorkbook.Worksheets(this.WSheet).Range("B1").Value = .LastName
    ThisWorkbook.Worksheets(this.WSheet).Range("C1").Value = .Login
  End With
End Sub

Private Property Get IUserStorageV2_Model() As UserModel
  Set IUserStorageV2_Model = this.Model
End Property

Private Property Set IUserStorageV2_Model(ByVal Instance As UserModel)
  Set this.Model = Instance
End Property

Factory/Constructor

Public Function Create(ByVal Model As UserModel, _
                       ByVal WSheetName As String) As UserWSheet
  Dim Instance As UserWSheet
  Set Instance = New UserWSheet
  Instance.Init Model, WSheetName
  Set Create = Instance
End Function
  
Public Sub Init(ByVal Model As UserModel, ByVal WSheetName As String)
  Set this.Model = Model
  this.WSheetName = WSheetName
End Sub

This definition is backward compatible with old software that uses the IUserStorage interface. Now, what should we do about the factory if we want to generate instances of both new and old interfaces? There are two possible approaches here. The simplest scenario is if the current factory is compatible with the new IUserStorageV2 instances except for the type. It appears that different foreign interfaces of a class can be switched directly the same way via assignment. For example, if an IUserStorageV2 variable is assigned a reference from an IUserStorage, the interface is switched as before, though the RubberDuck VBA extension flags such an assignment as illegal.

A better approach is to declare the factory's return type as the default interface. Interface switching will occur in the calling code when the factory's returned reference is assigned to a local variable (see SomeStorage assignment in DemoInterfaceSwitching.bas). Similarly to this demo, the new code would declare the StorageEx variable as IUserStorageV2, switching the default interface to IUserStorageV2. Further, the factory's argument list can be extended with optional arguments, and new private fields can be added to the UserWSheet class and initialized without affecting the old code.

If this approach does not provide sufficient flexibility, a new separate factory CreateV2 returning the new or default interface can be defined.

These two approaches can, in principle, accommodate any number of foreign interfaces and code upgrades while preserving full backward compatibility.