Как вы можете конструировать объекты, передающие аргументы непосредственно вашим собственным классам?
Что-то вроде этого:
Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)
Невозможность сделать это очень раздражает, и вы оказываетесь в грязных решениях для работы над этим.
Вот небольшой трюк, который я использую в последнее время и приносит хорошие результаты. Я хотел бы поделиться с теми, кому приходится часто сражаться с 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
Что лучше.
factory.CreateEmployee
чтобы уменьшить двусмысленность ...
Я использую один модуль 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
К сожалению, каждый экземпляр объекта создается каждый раз, поэтому любая статическая переменная будет потеряна после выполнения. Набор строк и любая статическая переменная, используемая в этом псевдостатическом методе, должны быть определены в модуле.
Factory
? определение Sub
влечет за собой возвращаемого значения. 2 : даже с учетом того, что мне не хватает, ваша Factory
делает почти то же самое, что и моя: создаю объект (я делаю это в два шага, ваш синтаксис явно короче), вызывает метод Init
/ InitiateProperties
и в мой случай, явно вернуть.
Когда вы экспортируете модуль класса и открываете файл в "Блокноте", вы заметите в верхней части кучу скрытых атрибутов (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
и пользоваться неизменным полиморфным объектом.
Property Let
внешнему миру (или, что еще хуже, публичным полям!)
Другой подход
Предположим, что вы создаете класс 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(...)
Единственное наказание - дополнительный вызов, но преимущество в том, что вы можете хранить все в модуле класса, а отладка становится проще.