# Background #

I am writing a VBA module which requires the functionality of a custom object.  Due to reasons beyond the scope of this question, standard solutions are impossible: I cannot...

  1. ...define a custom `Type` in my module, which has the required properties.  My module is an **["optional dependency"][1]**, so dependents check for its presence during execution; but `Type` operations fail to even _compile_ in its absence.
  1. ...convert my module into a class for late binding, which should solve the compilation issue.  Its functions must be **usable in Excel** formulae; but this is impossible (I think) with methods from a class.
  1. ...use a native (`Dictionary`) object, which has the required functionality.  My module must be **platform-agnostic**; but `Dictionary` is unavailable on Mac.
  1. ...import a custom object into my module.  My module must be a **standalone** application; but this custom class adds another dependency.


# Solution #

It struck me that a keyed `Collection` resembles an object with named fields (ie. properties).  Indeed, we can mimic both fields and _methods_ in the following way!  Here `obj` is an object and `clx` is a `Collection` that mimics it.

| Object            | Collection              |
| :---------------- | :---------------------- |
| `obj.Field = 1`   | `clx.Add 1, "Field"`    |
| `obj.Field`       | `clx.Item("Field")`     |
| `obj.Method(...)` | `Method(clx, ...)`      |

Now the native `Collection` solves all 4 roadblocks:
  1. its declaration will always compile; and
  1. it is viable in regular modules, whose functions are usable in Excel; and
  1. it is available on Mac; and
  1. it requires no further dependencies.

So all we need is a comprehensive API of procedures, to maintain the illusion of an object: the `New_Obj()` function mimics the constructor; and the `Obj_*()` family mimics the accessors and methods.

| Functionality | Object              | Mimicry API        | Mimicry Description |
| :------------ | :------------------ | :----------------- | :------------------ |
| Construction  | `Set x = New Obj` | `Set x = New_Obj()`  | Return a `Collection` populated by uninitalized values under keys. |
| Getting       | `x.Field`         | `Obj_Field(x)`       | Return the value under the `"Field"` key. |
| Setting       | `x.Field = 1`     | `Obj_Field x, 1`     | Update the value under the `"Field"` key. |
| Invocation    | `x.Method(...)`   | `Obj_Method(x, ...)` | A function that takes a `Collection` and acts like `.Method()`. |


# Question #

What is the best approach for the API?  I have laid out several variations below.


# Code #

Each of the approaches below relies on three helpers, which "extend" the `Collection`.  See the **Helpers** section for their source code.

  - **`Clx_Exists()`**: Test if an item exists under a given key (or at a given position).
  - **`Clx_Set()`**: Update the value under a given key.
  - **`Assign()`**: Assign any value (scalar or objective) to a variable.


## Prototype ##

This illustration involves an "object" with a set of "fields"...
  - `Field1` which is an `Integer`
  - `Field2` which is a `String`
  - `Field3` which is a `Collection`
  - ... 

...and a set of "methods":
  - `Method1()` which simply multiplies `x` by `Field1`
  - ...

We implement the `New_Obj()` function to mimic a constructor; and the `Obj_*()` family of functions to mimic its accessors and methods.

```vba
' ############
' ## Fields ##
' ############

Private Const OBJ_KEY_FIELD1 As String = "Field1"
Private Const OBJ_KEY_FIELD2 As String = "Field2"
Private Const OBJ_KEY_FIELD3 As String = "Field3"
' ...



' #########
' ## API ##
' #########

' Constructor.
Public Function New_Obj( _
    Optional ByVal Field1 As Integer, _
    Optional ByVal Field2 As String, _
    Optional ByVal Field3 As Collection, _
    ' ...
) As Collection
    Dim obj As Collection: Set obj = New Collection
    
    ' Set the values for each field.
    Obj_Field1 obj, val := Field1
    Obj_Field2 obj, val := Field2
    Obj_Field3 obj, val := Field3
    ' ...
    
    ' Ensure the result is fully initialized.
    Obj_Initialize obj
    
    ' Return the result.
    Set New_Obj = obj
End Function


' Field accessors.
Public Function Obj_Field1(ByRef obj As Collection, _
    Optional ByVal val As Variant _
) As Integer
    ' Get the field value...
    Obj_Field1 = obj.Item(OBJ_KEY_FIELD1)
    
    ' ...and optionally set it.
    If Not IsMissing(val) Then
        Dim f1 As Integer: f1 = val
        Clx_Set obj, OBJ_KEY_FIELD1, f1
    End If
End Function


Public Function Obj_Field2(ByRef obj As Collection, _
    Optional ByVal val As Variant _
) As String
    ' Get the field value...
    Obj_Field2 = obj.Item(OBJ_KEY_FIELD2)
    
    ' ...and optionally set it.
    If Not IsMissing(val) Then
        Dim f2 As String: f2 = val
        Clx_Set obj, OBJ_KEY_FIELD2, f2
    End If
End Function


Public Function Obj_Field3(ByRef obj As Collection, _
    Optional ByVal val As Variant _
) As Collection
    ' Get the field value...
    Set Obj_Field3 = obj.Item(OBJ_KEY_FIELD3)
    
    ' ...and optionally set it.
    If Not IsMissing(val) Then
        Dim f3 As Collection: Set f3 = val
        Clx_Set obj, OBJ_KEY_FIELD3, f3
    End If
End Function


' Methods.
Public Function Obj_Method1(ByRef obj As Collection, _
    ByVal x As Integer _
) As Integer
    Dim f1 As Integer: f1 = Obj_Field1(obj)
    
    Obj_Method1 = x * f1
End Function

' ...



' #############
' ## Helpers ##
' #############

' Initializer.
Private Sub Obj_Initialize(ByRef obj As Collection)
    ' Ensure the "obj" collection is ready.
    If obj Is Nothing Then Set obj = New Collection
    
    
    ' Initialize fields that do not exist already.
    Dim f1 As Integer
    If Not Clx_Exists(obj, OBJ_KEY_FIELD1) Then Obj_Field1 obj, val := f1
    
    Dim f2 As String
    If Not Clx_Exists(obj, OBJ_KEY_FIELD2) Then Obj_Field2 obj, val := f2
    
    ' I want the "Field3" collection to exist AND be ready for action.
    Dim f3 As Collection
    If Clx_Exists(obj, OBJ_KEY_FIELD3) Then
        Set f3 = Obj_Field3(obj)
        If f3 Is Nothing Then Obj_Field3 obj, val := New Collection
    Else
        Obj_Field3 obj, val := New Collection
    End If
    
    ' ...
Sub
```


## Keys ##

We have keyed the "fields" with `String` constants.  Thus we maintain them centrally, rather than hard-coding `String`s everywhere.

```vba
' ############
' ## Fields ##
' ############

Private Const OBJ_KEY_FIELD1 As String = "Field1"
Private Const OBJ_KEY_FIELD2 As String = "Field2"
Private Const OBJ_KEY_FIELD3 As String = "Field3"
' ...
```

However, we could also use an **enumeration**, which would require translation into a key `String`.  In this case, `Obj_Key(*)` would replace every `OBJ_KEY_*`.

```vba
Private Enum ObjField
    Field1
    Field2
    Field3
    ' ...
End Enum

' ...

Private Function Obj_Key(ByVal fld As ObjField) As String
    Select Case fld
        Case Field1: Obj_Key = "Field1"
        Case Field2: Obj_Key = "Field2"
        Case Field3: Obj_Key = "Field3"
        ' ...
    End Select
End Function
```


## Optional Fields ##

Unlike actual objects or custom `Type`s, which always have their full set of fields, a `Collection` may lack some elements.  As such, our "object" can treat certain "fields" as optional.

When this "object" has _all_ its "fields", it technically mimics **"subclass"** of a similar "object" with _missing_ "fields".  The advantage is that the _absence_ of a field communicates different information than its _uninitialized presence_, so we gain flexibility of meaning.  The disadvantage is a slight divergence from the behavior of an actual object.

Here is an illustration of making `Field2` an optional "field":

```vba
' Constructor.
Public Function New_Obj( _
    ' ...
    Optional ByVal Field2 As String = vbNullString, _
    ' ...
    ' ...
) As Collection
    ' ...
    
    ' Optionally include "Field2".
    If Field2 <> vbNullString Then Obj_Field2 obj, Field2
    
    ' ...
End Function

' ...

' Fields.
Public Function Obj_Field2(ByRef obj As Collection, _
    Optional ByVal val As Variant _
) As String
    ' Get any field value that MIGHT exist...
    If Clx_Exists(obj, OBJ_FLD_FIELD2) Then Obj_Field2 = obj(OBJ_FLD_FIELD2)
    
    ' ...
End Function

' ...

' Initializer.
Private Sub Obj_Initialize(ByRef obj As Collection)
    ' ...
    
    ' ' Skip initialization for "Field2".
    ' Dim f2 As String
    ' If Not Clx_Exists(obj, OBJ_FLD_FIELD2) Then Obj_Field2 obj, val := f2
    
    ' ...
End Sub
```


## Extent of Helpers ##

For `Private` helpers, there are many tradeoffs between functionality, brevity, elegance, consistency, and ease of maintenance.

  1. To protect certain "fields" from mutation, we might omit the `val` argument from their accessors.  But we can no longer use (say) `Obj_Field1 obj, val := ...` to update the value within `New_Obj()` and `Obj_Initialize()`.
  1. To independently maintain `Obj_Field1()`, without fear of disrupting functionality, we could replace its calls with `Clx_Set obj, OBJ_FLD_FIELD1, ...`.  But this is inconsistent with using `Obj_Field1(obj)` to simply retrieve the value.
  1. To preserve consistency, we could replace all calls like `Obj_Field1(obj)` with `obj.Item OBJ_FLD_FIELD1`.  But this makes the whole thing visually messy.
  1. To restore visual elegance, we could implement these operations as `Private` procedures: `Obj_HasField1()` and `Obj_GetField1()` and `Obj_SetField1()`.  But this requires 3 separate functions for _every_ field, so the code expands dramatically with many implementations.
  1. To centrally maintain the mechanisms for `Obj_Has*()` and `Obj_Get*()` and `Obj_Set*()`, we could have them wrap underlying helpers: `Obj_Has()` and `Obj_Get()` and `Obj_Set()`.  But this adds 3 _more_ functions to an expanding module.
     
     ```vba
     Private Function Obj_Has(ByRef obj As Collection, ByVal fld As String) As Boolean
         Obj_Has = Clx_Exists(obj, fld)
     End Function
     
     Private Function Obj_Get(ByRef obj As Collection, ByVal fld As String) As Variant
         Assign Obj_Get, obj.Item(fld)
     End Function
     
     Private Function Obj_Set(ByRef obj As Collection, ByVal fld As String, ByRef val As Variant)
         Clx_Set obj, fld, val
     End Function
     ```
  1. For the sake of brevity, we could replace all instances of
     - `Obj_Has*(obj)` with `Obj_Has(obj, OBJ_FLD_*)`
     - `Obj_Get*(obj)` with `Obj_Get(obj, OBJ_FLD_*)`
     - `Obj_Set* obj, ...` with `Obj_Set obj, OBJ_FLD_*, ...`
     
     This leaves us with only 3 helpers in _total_, rather than 3 for _each_ "field": just `Obj_Has()` and `Obj_Get()` and `Obj_Set()`.  But while `Obj_Field1(obj)` embeds the "field" name within the syntax, the new `Obj_Get(obj, ...)` imposes no structure on the "field" name, and we lose stability.
  1. For the sake of stability, we could restrict the "field" to the `ObjField` enumeration.  But this requires `Obj_Key()`, which must be maintained _in sync_ with the enumeration, so maintenance becomes decentralized.
     
     ```vba
     Private Function Obj_Has(ByRef obj As Collection, ByVal fld As ObjField) As Boolean
         Obj_Has = Clx_Exists(obj, Obj_Key(fld))
     End Function
     
     Private Function Obj_Get(ByRef obj As Collection, ByVal fld As ObjField) As Variant
         Assign Obj_Get, obj.Item(Obj_Key(fld))
     End Function
     
     Private Function Obj_Set(ByRef obj As Collection, ByVal fld As ObjField, ByRef val As Variant)
         Clx_Set obj, Obj_Key(fld), val
     End Function
     ```


## Properties ##

Could we rewrite the "fields" as modular properties, to allow easy access?  We could obviously "protect" certain "fields" by making their `Let`/`Set` properties `Private`.

```vba
' Fields
Public Property Get Obj_Field1(ByRef obj As Collection) As Integer
    Obj_Field1 = obj.Item(OBJ_FLD_FIELD1)
End Property

Public Property Let Obj_Field1(ByRef obj As Collection, ByVal val As Integer)
    Clx_Set obj, OBJ_FLD_FIELD1, val
End Property

' ...

Public Property Get Obj_Field3(ByRef obj As Collection) As Collection
    Set Obj_Field3 = obj.Item(OBJ_FLD_FIELD3)
End Property

Public Property Set Obj_Field3(ByRef obj As Collection, ByRef val As Collection)
    Clx_Set obj, OBJ_FLD_FIELD3, val
End Property
```

My hope is that the API user can access the fields like so:

```vba
Debug.Print Obj_Field1(obj)

Obj_Field1(obj) = 2

' ...

Obj_Field3(obj) = New Collection
```


-----


# Helpers #

Here is the code for the three helpers.

```vba
Private Function Clx_Exists(ByRef clx As Collection, _
    ByVal index As Variant _
) As Boolean
    On Error GoTo Fail
    clx.Item index
    
    Clx_Exists = True
    Exit Function
    
Fail:
    Clx_Exists = False
End Function

Private Function Clx_Set(ByRef clx As Collection, _
    ByVal key As String, _
    ByRef val As Variant _
)
    If Clx_Exists(clx, key) Then
        clx.Remove key
    End If
    
    clx.Add val, key := key
End Function

Private Sub Assign( _
    ByRef var As Variant, _
    ByVal val As Variant _
)
    If IsObject(val) Then
        Set var = val
    Else
        var = val
    End If
End Sub
```


  [1]: https://stackoverflow.com/q/77073837