Search code examples
vbaobjectcollectionsrelationbidirectional

VBA Linking two Objects Bidirectionally


I have two objects, clsGroup and clsUser. Users and groups are separate objects.

  • Each user can be in any number of groups.
  • Each group can contain any number of users.

Rather than store a list of groups that belong to each user, and a list of users that belong in each group in each object, does VBA offer any way to link them so that from the user object we can get a list of groups associated with that user, and from the group object we can get a list of users associated with that group?

I'm trying to avoid creating collections within users and groups as syncing will be problematic.


Solution

  • This is the general problem of circular references. There is a lot of stuff about it out there, and the general conclusion is that they are something to avoid as much as possible.

    Therefore you are correct when you refuse to store in each class a collection of references to the other class. In VBA, this will result in memory leaks, because the garbage collector, which is based on reference counting, will never destroy objects with circular references, even though the group they form together is no longer needed.

    But what if you indeed need such a bidirectional association?

    There are different solutions to deal with circular references, using different techniques. In VBA, what you need is a mechanism to release the cross-references, at some place when you know they are no longer needed. In other words, you need to somehow manage yourself the lifetime of the references.

    A possible implementation would be to maintain a global Dictionary of the groups that are alive. When you want to remove a group, say in some procedure existing somewhere in the code, you first free (empty) its collection of students. This will break the circular reference, allowing the garbage collector to work properly. The following is the skeleton of such a possible implementation.

    'In class clsUser:
      Public id as String '<~~ could be any type of identifier, such as Integer
      Public groups as New Dictionary
      Public Sub subscribeToGroup(g as clsGroup)
          Me.groups.add g.id, g
      End Sub
    
      Public Sub unsubscribeFromGroup(g as clsGroup)
          Me.groups.Remove g.id
      End Sub
    ''''''''''''''''''''''''''''''''''''''''''''''''
    
    'In class clsGroup:
      Public id as String '<~~ could be any type of identifier, such as Integer
      Public users as New Dictionary
    
      Public Sub registerUser(u as clsUser)
          Me.users.add u.id, u
      End Sub
    
      Public Sub unregisterUser(u as clsUser)
          Me.users.Remove u.id
      End Sub
      Public Sub removeAllUsers() 'empty the users collection, break the circular reference
        For Each u In users: u.unsubscribeFromGroup(Me): Next
        users.RemoveAll
    End Sub
    ''''''''''''''''''''''''''''''''''''''''''''''''
    

    You have somewhere (i.e. a global variable, or in some class called GroupManager, etc) a Dictionary of groups, and methods to create or remove a group.

    Public AllGroups as Dictionary
    Function addGroup(id as String) as clsGroup
       Set addGroup = new clsGroup
       addGroup.id = id
       AllGroups.Add id, addGroup
    End Function 
    
    Sub removeGroup(g as clsGroup)
        ' Explicitly Tell the group to free its users before going away.
        ' This would not be needed if we didn't have circular references!
        g.removeAllUsers
        AllGroups.Remove g.id
    End Sub
    

    Now, the circular references are gone, and the group will be destroyed (no memory leak) because nothing holds any reference to it.