Проверить циклические зависимости

Вступление

Иногда, особенно в более сложных структурах классов, я в некотором роде чувствую неуверенность в том, могут ли существовать неоткрытые круговые зависимости.

Поэтому я подумал, как мне легко проверить их существование.


Идея, лежащая в основе

Мне пришла в голову идея использовать центральное место, где я автоматически регистрирую все соответствующие объекты, когда они создаются, и удаляю их, когда они уничтожаются.

При желании / необходимости можно также хранить здесь контекстную информацию для каждого объекта, чтобы иметь возможность различать экземпляры объекта во время оценки.


Слабые ссылки

Конечно, вы можете использовать слабые ссылки в целом, но, возможно, вы не сможете / не будете этого делать в зависимости от проекта.


(Де) активировать мониторинг

Для этого используется условная компиляция. Это также гарантирует, что код для продуктивных сред не будет затронут.

Чтобы создать константу условной компиляции для всего проекта, необходимо использовать диалог свойств проекта VBE:

Для Conditional Compilation Arguments переменная MONITOR_CYCLIC_DEPENDENCIES можно указать и установить на 0 или же 1.

Пример:

MONITOR_CYCLIC_DEPENDENCIES = 1

Обработка ошибок

Конкретная обработка ошибок наверняка может / должна быть реализована.


Именование

На днях я ответил на другой вопрос, связанный с соглашениями об именах.

Я тоже так поступил и некоторое время назад решил использовать Pascal Casing (MyProcedureName) для имен объектов и процедур и Camel Casing (myVariableName) для имен параметров и переменных.

Я всегда пишу константы с заглавной буквы и разделяю слова подчеркиванием (MY_CONSTANT_NAME).

Я всегда предваряю свои комментарии '//, что мне визуально приятнее.

Чтобы избежать автоматического повторного регистрирования моих литералов в VBA, я стараюсь найти хорошие имена, чтобы различать их, и поэтому избегаю необходимости написания одних и тех же литералов в разных регистрах.

Иногда это невозможно, хороший пример: value, который используется как Value из некоторых библиотек, поэтому я не могу назвать переменную value.

Для таких особых случаев, как этот, я смирился с использованием фактически бессмысленного префикса: x. Это не имеет отношения к префиксу как таковому.

В x на самом деле только визуально гарантирует, что мои глаза чувствуют себя комфортно с кодом. Это конечно чисто субъективно, но меня это устраивает.

Примеры, в которых я использую этот «префикс», например:

  • xValue
  • xIndex
  • xItem
  • xResult

Это компромисс, на который я пришел.


Реализация

CyclicDependenciesMonitor

Важно: этот класс использует Attribute VB_PredeclaredId = True

Option Explicit

Private Type TState
    MonitoredObjects As Collection
End Type

Private this As TState

Private Sub Class_Initialize()
    Reset
End Sub

'// Reset object monitoring for another run
Public Sub Reset()
    Set this.MonitoredObjects = New Collection
End Sub

'// Add an object for monitoring
Public Sub AddObject(ByVal objectToMonitor As Object)
    this.MonitoredObjects.Add _
        ObjPtr(objectToMonitor) & " (" & TypeName(objectToMonitor) & ")", _
        CStr(ObjPtr(objectToMonitor))
End Sub

'// This procedure allows to store additional
'// context information if desired for debugging
Public Sub AddContextInformationToObject(ByVal monitoredObject As Object, _
                                         ByVal contextInformation As String)
    On Error Resume Next

    Dim xResult As String
    xResult = this.MonitoredObjects(CStr(ObjPtr(monitoredObject)))

    If xResult = vbNullString Then Exit Sub

    xResult = xResult & " " & contextInformation

    RemoveObject monitoredObject

    this.MonitoredObjects.Add xResult, CStr(ObjPtr(monitoredObject))
End Sub

'// Remove an object from monitoring
Public Sub RemoveObject(ByVal objectToMonitor As Object)
    this.MonitoredObjects.Remove CStr(ObjPtr(objectToMonitor))
End Sub

'// Determine information from leftover objects
Public Function LeftoverObjects() As String
    LeftoverObjects = TypeName(Me) & " - " & "Objects left over: " & _
        this.MonitoredObjects.Count & vbNewLine

    If this.MonitoredObjects.Count = 0 Then Exit Function

    LeftoverObjects = LeftoverObjects & vbNewLine & _
        "<Memory Adress> (<TypeName>) <Provided context Information>" & vbNewLine & _
        "-----------------------------------------------------------" & vbNewLine

    Dim xItem As Variant
    For Each xItem In this.MonitoredObjects
        LeftoverObjects = LeftoverObjects & xItem & vbNewLine
    Next xItem
End Function

Встраивание кода в пользовательские классы

Для демонстрации я создал два определяемых пользователем класса, один с возможностью добавления данных контекста для внутренней отладки.

UserClass

Option Explicit

Private Type TState
    SampleStoredObject As Object
End Type

Private this As TState

Private Sub Class_Initialize()
    #If MONITOR_CYCLIC_DEPENDENCIES Then
        CyclicDependenciesMonitor.AddObject Me
    #End If

    '// Place here the code that your class needs in this procedure
End Sub

Private Sub Class_Terminate()
    '// Place here the code that your class needs in this procedure

    #If MONITOR_CYCLIC_DEPENDENCIES Then
        CyclicDependenciesMonitor.RemoveObject Me
    #End If
End Sub

'// This is a sample procedure that receives and stores an object
'// to provoke a cyclic dependency
Public Sub SampleStoreObject(ByVal objectToStore As Object)
    Set this.SampleStoredObject = objectToStore
End Sub

ДругойUserClass

Option Explicit

Private Type TState
    SampleStoredObject As Object
    SampleContextData As String
End Type

Private this As TState

Private Sub Class_Initialize()
    #If MONITOR_CYCLIC_DEPENDENCIES Then
        CyclicDependenciesMonitor.AddObject Me
    #End If

    '// Place here the code that your class needs in this procedure
End Sub

Private Sub Class_Terminate()
    '// Place here the code that your class needs in this procedure

    #If MONITOR_CYCLIC_DEPENDENCIES Then
        CyclicDependenciesMonitor.RemoveObject Me
    #End If
End Sub

'// This is a sample procedure that receives and stores an object
'// to provoke a cyclic dependency
Public Sub SampleStoreObject(ByVal objectToStore As Object)
    Set this.SampleStoredObject = objectToStore
End Sub

'// This procedure could have been intentionally created to store contextual information,
'// but it could also be an existing procedure that is now also used for this purpose
Public Sub SampleStoreContextData(ByVal contextData As String)
    this.SampleContextData = contextData

    #If MONITOR_CYCLIC_DEPENDENCIES Then
        CyclicDependenciesMonitor.AddContextInformationToObject Me, contextData
    #End If
End Sub

Используй это

Как видите, код создает три объекта двух разных типов.

Object1 а также Object3 затем ссылаются друг на друга, вызывая циклическую зависимость.

После попытки уничтожить объекты, существующие циклические зависимости определяются и выводятся.

Комментирование одной из этих строк предотвращает существование циклических зависимостей:

object1.SampleStoreObject object3
object3.SampleStoreObject object1

ClientTestModule

Option Explicit

Public Sub TestObjectMonitoring()
    #If MONITOR_CYCLIC_DEPENDENCIES Then
        CyclicDependenciesMonitor.Reset
    #End If

    Dim object1 As UserClass
    Set object1 = New UserClass

    #If MONITOR_CYCLIC_DEPENDENCIES Then
        '// Add some context data for monitoring
        '// (however, this is not necessary and just for debugging purposes)
        CyclicDependenciesMonitor.AddContextInformationToObject object1, "Foo"
    #End If

    Dim object2 As UserClass
    Set object2 = New UserClass

    Dim object3 As AnotherUserClass
    Set object3 = New AnotherUserClass

    '// The context data for monitoring will be added by a class method of the object
    '// (however, this is also not necessary)
    object3.SampleStoreContextData "Bar"

    '// Provoke a cyclic dependency:
    object1.SampleStoreObject object3
    object3.SampleStoreObject object1

    '// The attempt to destroy the objects by removing the references
    Set object1 = Nothing
    Set object2 = Nothing
    Set object3 = Nothing

    #If MONITOR_CYCLIC_DEPENDENCIES Then
        '// Check whether monitored objects still exist, which have not been destroyed
        Debug.Print CyclicDependenciesMonitor.LeftoverObjects()
    #End If
End Sub

Выход

CyclicDependenciesMonitor - Objects left over: 2

<Memory Adress> (<TypeName>) <Provided context Information>
-----------------------------------------------------------
2357270190368 (UserClass) Foo
2357270190512 (AnotherUserClass) Bar

Или в случае отсутствия циклической зависимости:

CyclicDependenciesMonitor - Objects left over: 0

Вопросов

Что вы думаете об этом?

Подход полезен / работоспособен?

Есть ли у вас предложения по улучшению?

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *