Передать аргументы Конструктору в VBA

47

Как вы можете конструировать объекты, передающие аргументы непосредственно вашим собственным классам?

Что-то вроде этого:

Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)

Невозможность сделать это очень раздражает, и вы оказываетесь в грязных решениях для работы над этим.

Теги:
class
oop
constructor
factory

4 ответа

76
Лучший ответ

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

1.- Внедрить общедоступную подпрограмму инициации в каждом из ваших пользовательских классов. Я называю его InitiateProperties во всех моих классах. Этот метод должен принимать аргументы, которые вы хотите отправить конструктору.

2.- Создайте модуль под названием factory и создайте публичную функцию со словом "Создать" с тем же именем, что и класс, и теми же входящими аргументами, что и конструктор. Эта функция должна создать экземпляр вашего класса и вызвать подпрограмму инициации, описанную в пункте (1), передав полученные аргументы. Наконец, вернул метод, инициированный и инициированный.

Пример:

Скажем, у нас есть пользовательский класс Employee. Как предыдущий пример, должен быть создан экземпляр с именем и возрастом.

Это метод InitiateProperties. m_name и m_age - это наши частные свойства, которые нужно установить.

Public Sub InitiateProperties(name as String, age as Integer)

    m_name = name
    m_age = age

End Sub

И теперь в модуле factory:

Public Function CreateEmployee(name as String, age as Integer) as Employee

    Dim employee_obj As Employee
    Set employee_obj = new Employee

    employee_obj.InitiateProperties name:=name, age:=age
    set CreateEmployee = employee_obj

End Function

И наконец, когда вы хотите создать экземпляр сотрудника

Dim this_employee as Employee
Set this_employee = factory.CreateEmployee(name:="Johnny", age:=89)

Особенно полезно, когда у вас есть несколько классов. Просто поместите функцию для каждого из модулей factory и создайте экземпляр, просто позвонив factory.CreateClassA(аргументы), factory.CreateClassB(other_arguments) и т.д..

ИЗМЕНИТЬ

Как указал Stenci, вы можете сделать то же самое с синтаксисом терминов, избегая создания локальной переменной в конструкторских функциях. Например, функция CreateEmployee может быть написана следующим образом:

Public Function CreateEmployee(name as String, age as Integer) as Employee

    Set CreateEmployee = new Employee
    CreateEmployee.InitiateProperties name:=name, age:=age

End Function

Что лучше.

  • 1
    Отличное решение! Хотя я бы, вероятно, переименовал его в factory.CreateEmployee чтобы уменьшить двусмысленность ...
  • 0
    Да, вы правы, это было бы менее двусмысленно. Хотя я привык читать "фабрика". как будто это было "новым". Я изменю это.
Показать ещё 7 комментариев
22

Я использую один модуль Factory, который содержит один (или более) конструктор для каждого класса, который вызывает член Init каждого класса.

Например, класс Point:

Class Point
Private X, Y
Sub Init(X, Y)
  Me.X = X
  Me.Y = Y
End Sub

A Line класс

Class Line
Private P1, P2
Sub Init(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
  If P1 Is Nothing Then
    Set Me.P1 = NewPoint(X1, Y1)
    Set Me.P2 = NewPoint(X2, Y2)
  Else
    Set Me.P1 = P1
    Set Me.P2 = P2
  End If
End Sub

И модуль Factory:

Module Factory
Function NewPoint(X, Y)
  Set NewPoint = New Point
  NewPoint.Init X, Y
End Function

Function NewLine(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
  Set NewLine = New Line
  NewLine.Init P1, P2, X1, Y1, X2, Y2
End Function

Function NewLinePt(P1, P2)
  Set NewLinePt = New Line
  NewLinePt.Init P1:=P1, P2:=P2
End Function

Function NewLineXY(X1, Y1, X2, Y2)
  Set NewLineXY = New Line
  NewLineXY.Init X1:=X1, Y1:=Y1, X2:=X2, Y2:=Y2
End Function

Одним из приятных аспектов этого подхода является упрощение использования функций factory внутри выражений. Например, можно сделать что-то вроде:

D = Distance(NewPoint(10, 10), NewPoint(20, 20)

или

D = NewPoint(10, 10).Distance(NewPoint(20, 20))

Он чист: factory делает очень мало, и он последовательно выполняет все объекты, только создание и один вызов Init для каждого создателя.

И он довольно объектно-ориентированный: функции Init определены внутри объектов.

ИЗМЕНИТЬ

Я забыл добавить, что это позволяет мне создавать статические методы. Например, я могу сделать что-то вроде (после внесения параметров необязательно):

NewLine.DeleteAllLinesShorterThan 10

К сожалению, каждый экземпляр объекта создается каждый раз, поэтому любая статическая переменная будет потеряна после выполнения. Набор строк и любая статическая переменная, используемая в этом псевдостатическом методе, должны быть определены в модуле.

  • 2
    Это чище, чем выбранный ответ.
  • 0
    Прошло много времени с тех пор, как я в последний раз играл с VBA, но ... 1 : как вы получаете построенные объекты из подпрограмм Factory ? определение Sub влечет за собой возвращаемого значения. 2 : даже с учетом того, что мне не хватает, ваша Factory делает почти то же самое, что и моя: создаю объект (я делаю это в два шага, ваш синтаксис явно короче), вызывает метод Init / InitiateProperties и в мой случай, явно вернуть.
Показать ещё 2 комментария
3

Когда вы экспортируете модуль класса и открываете файл в "Блокноте", вы заметите в верхней части кучу скрытых атрибутов (VBE не отображает их и не раскрывает функциональность, чтобы настроить большинство из них или). Один из них - VB_PredeclaredId:

Attribute VB_PredeclaredId = False

Установите его в True, сохраните и повторно импортируйте модуль в проект VBA.

Альтернативно, если вы используете Rubberduck, вы можете просто указать специальную аннотацию в верхней части модуля:

'@PredeclaredId
Option Explicit

Rubberduck выдает результат проверки, в котором говорится, что атрибуты модуля не синхронизированы с аннотациями модуля, и одним щелчком вы можете установить атрибут VB_PredeclaredId, не выходя из VBE (примечание: на момент написания это все еще экспериментальное функция).

У классов с PredeclaredId есть "глобальный экземпляр", который вы получаете бесплатно - точно так же, как UserForm модули (экспортируйте пользовательскую форму, вы увидите, что для атрибута predeclaredId установлено значение true).

Многие люди просто счастливо используют предопределенный экземпляр для хранения состояния. Это неправильно - это как сохранение состояния экземпляра в статическом классе!

Вместо этого вы используете этот экземпляр по умолчанию для реализации вашего метода factory:

[Employee класс]

'@PredeclaredId
Option Explicit

Private Type TEmployee
    Name As String
    Age As Integer
End Type

Private this As TEmployee

Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As Employee
    With New Employee
        .Name = emplName
        .Age = emplAge
        Set .Create = .Self
    End With
End Function

Public Property Get Self() As Employee
    Set Self = Me
End Property

Public Property Get Name() As String
    Name = this.Name
End Property

Public Property Let Name(ByVal value As String)
    this.Name = value
End Property

Public Property Get Age() As String
    Age = this.Age
End Property

Public Property Let Age(ByVal value As String)
    this.Age = value
End Property

С этим вы можете сделать это:

Dim empl As Employee
Set empl = Employee.Create("Johnny", 69)

Employee.Create работает с экземпляром по умолчанию, то есть считается членом этого типа и вызывается только из экземпляра по умолчанию.

Проблема в том, что это также совершенно законно:

Dim emplFactory As New Employee
Dim empl As Employee
Set empl = emplFactory.Create("Johnny", 69)

И это отстой, потому что теперь у вас запутанный API. Вы могли бы использовать атрибуты '@Description annotations/VB_Description для использования документа, но без Rubberduck в редакторе ничего нет, что показывает вам эту информацию на сайтах вызовов.

Кроме того, члены Property Let доступны, поэтому ваш экземпляр Employee изменен:

empl.Name = "Booba" ' Johnny no more!

Трюк заключается в том, чтобы ваш класс реализовал интерфейс, который только раскрывает то, что должно быть раскрыто:

[IEmployee класс]

Option Explicit

Public Property Get Name() As String : End Property
Public Property Get Age() As Integer : End Property

И теперь вы делаете Employee реализацию IEmployee - последний класс может выглядеть следующим образом:

[Employee класс]

'@PredeclaredId
Option Explicit
Implements IEmployee

Private Type TEmployee
    Name As String
    Age As Integer
End Type

Private this As TEmployee

Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As IEmployee
    With New Employee
        .Name = emplName
        .Age = emplAge
        Set .Create = .Self
    End With
End Function

Public Property Get Self() As IEmployee
    Set Self = Me
End Property

Public Property Get Name() As String
    Name = this.Name
End Property

Public Property Let Name(ByVal value As String)
    this.Name = value
End Property

Public Property Get Age() As String
    Age = this.Age
End Property

Public Property Let Age(ByVal value As String)
    this.Age = value
End Property

Private Property Get IEmployee_Name() As String
    IEmployee_Name = Name
End Property

Private Property Get IEmployee_Age() As Integer
    IEmployee_Age = Age
End Property

Обратите внимание, что метод Create теперь возвращает интерфейс, и интерфейс не предоставляет элементы Property Let? Теперь код вызова может выглядеть так:

Dim empl As IEmployee
Set empl = Employee.Create("Immutable", 42)

И так как клиентский код написан против интерфейса, единственными членами empl являются члены, определенные интерфейсом IEmployee, что означает, что он не видит метод Create, а Self getter или любой из мутаторов Property Let: поэтому вместо работы с "конкретным" классом Employee остальная часть кода может работать с "абстрактным" интерфейсом IEmployee и пользоваться неизменным полиморфным объектом.

  • 0
    Примечание: неизменность не достижима; экземпляр имеет доступ к своим полям и может очень хорошо изменять их значения. Но это лучше, чем выставлять Property Let внешнему миру (или, что еще хуже, публичным полям!)
0

Другой подход

Предположим, что вы создаете класс clsBitcoinPublicKey

В модуле класса создайте ДОПОЛНИТЕЛЬНУЮ подпрограмму, которая действует так, как вы бы хотели, чтобы реальный конструктор вел себя. Ниже я назвал его ConstructorAdjunct.

Public Sub ConstructorAdjunct(ByVal ...)

 ...

End Sub

From the calling module, you use an additional statement

Dim loPublicKey AS clsBitcoinPublicKey

Set loPublicKey = New clsBitcoinPublicKey

Call loPublicKey.ConstructorAdjunct(...)

Единственное наказание - дополнительный вызов, но преимущество в том, что вы можете хранить все в модуле класса, а отладка становится проще.

  • 1
    Если я не пропускаю что-то, это все равно что вручную вызывать мои "InitiateProperties" каждый раз, когда вы создаете экземпляр любого объекта, чего я и хотел в первую очередь избежать.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню