Я создал этот простой пример, чтобы показать проблему, обнаруженную в более сложном классе
Чего не хватает, так это кода, который потребляет класс, и кода это фактически воспроизводит проблему, но я написал много статей на эту тему, так что давайте все равно копаем.
Private Sub Class_Initialize()
Debug.Print "Enter Initialize"
If Me Is CTest Then
m_birthday = DateValue("1/1/1800")
Else
m_birthday = Now()
End If
Debug.Print "Exit Initialize", m_birthday
End Sub
Полезная часть информации, которую вы не выводите, является ли инициализирующий экземпляр экземпляром по умолчанию. Рассмотрим:
Debug.Print "Initializing " & TypeName(Me) & IIf(Me Is CTest, " (default instance)", vbNullString)
Одна проблема заключается в следующем:
If Me Is CTest Then
m_birthday = DateValue("1/1/1800") '<~
Else
m_birthday = Now()
End If
Если текущий экземпляр является экземпляром класса по умолчанию, внутреннее состояние бесполезно. Сохранение экземпляра по умолчанию без сохранения состояния является ключевым, на самом деле: m_birthday
является подробностью реализации для интерфейса класса по умолчанию (CTest
). Это было бы лучшим защитным предложением:
If Me Is CTest Then Exit Sub
m_birthday = Now()
Нет больше вложенности, m_birthday
назначается только для экземпляра не по умолчанию, и намерение сохранить экземпляр по умолчанию без сохранения состояния гораздо более явно выражено.
Теперь, если вы введете это в ближайшей панели :
Set a = New CTest
Вы получите такой вывод:
Initializing CTest (default instance)
Initializing CTest
Вы пропустив этот след:
Private Sub Class_Terminate()
Debug.Print "Terminating " & TypeName(Me) & IIf(Me Is CTest, " (default instance)", vbNullString)
End Sub
При заводском методе Make
вы на самом деле хотите еще более сильное спасение:
Public Function Make(varparam As Variant) As CTest
If Me Is CTest Then
'...
Рассмотрим:
Public Function Make(varparam As Variant) As CTest
If Not Me Is CTest Then Err.Raise 5, TypeName(Me), "Member call is only valid from default/predeclared instance."
И это удаляет ветку в условном пути. Это также заставляет меня задуматься об этом:
ElseIf varparam Is Nothing Then
Это условие оценивается, когда Me Is CTest
равно False
, то есть когда фабричный метод вызывается из пользовательского экземпляра ... и это не должно допускаться.
Это еще одна проблема:
Select Case VarType(varparam)
Case vbDate:
.Birthday = varparam
Case vbObject:
.Birthday = varparam.Birthday
vbObject
означает, что varparam
является ссылкой Object
, а не CTest
object: поскольку мы работаем с Variant
, вызов члена имеет позднюю привязку, поэтому, если у объекта нет члена Birthday
, здесь возникает ошибка 438 времени выполнения. Мы можем сохранить вызов члена с поздней привязкой, но по-прежнему проверять тип:
Case vbObject:
If TypeOf varparam Is CTest Then .Birthday = varparam.Birthday
Или вы можете получить проверку во время компиляции, введя переменную:
Case vbObject:
Dim typedParam As CTest
If TypeOf varparam Is CTest Then
Set typedParam = varparam
.Birthday = typedParam.Birthday '<~ early-bound member call now
End If
Это не только помогает компилятор подхватывает опечатки (даже Option Explicit
не может спасти вас от опечатки при вызове с поздним связыванием), он также помогает использовать инструменты анализа кода, такие как Rubberduck , которые теперь «видят» вызов участника: если член переименован, инструменты рефакторинга теперь могут обновить этот сайт вызовов - это невозможно сделать с кодом с поздним связыванием.
Public Property Get Self() As CTest
Set Self = Me
End Property
Это синтаксический сахар, который хорошо работает когда задействован явный интерфейс, для чистого отделения экземпляра CTest
по умолчанию без состояния от явного ICTest
клиентского интерфейса (который может включать Property Get
на день рождения, но без Let
метода доступа).
Улучшенный синтаксический сахар, который не влияет на интерфейсы publi c ваших классов и значительно очищает окно инструментов localals в модулях классов, переводя состояние экземпляра в * 1 090 *:
Private Type TState
Birthday As Date
OtherData As Variant '<~ note: this breaks strong-typing and gets you back into late-bound land.
End Type
Private this As TState
Эта Private this
переменная экземпляра (на уровне модуля) заменяет все переменные с префиксом m_
, и теперь свойство Birthday
выглядит так:
Public Property Get Birthday() As Date
Birthday = this.Birthday
End Property
Public Property Let Birthday(ByVal val As Date)
this.Birthday = val
End Property
...
Таким образом, единственным извилистым фрагментом кода, который выглядит подозрительным, является функция Make
, которая отвечает за слишком много вещей.
Напишите отдельную частную функцию, которая работает с Date
, другим, который работает с CTest
объектом и условно вызывает соответствующий объект из Make
.
С функциями, которые делают меньше вещей, меньшее количество вещей может go ошибаться.
Защитите свои методы - если метод включает состояние экземпляра, запретите вызывать его из экземпляра по умолчанию / предварительно объявленного. Если предполагается, что метод вызывается из экземпляра по умолчанию, запретите вызывать его из других экземпляров.
См. эту статью для обновления шаблона, а этот чтобы увидеть это в действии с реальным кодом.