web-dev-qa-db-ja.com

VBAのコンストラクターに引数を渡す

独自のクラスに引数を直接渡すオブジェクトをどのように構築できますか?

このようなもの:

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

これを行うことができないのは非常に迷惑であり、これを回避するための汚い解決策になります。

71
bgusach

ここに私が最近使用している小さなトリックがあり、良い結果をもたらします。 VBAと頻繁に戦わなければならない人たちと共有したいと思います。

1 .-各カスタムクラスにパブリック開始サブルーチンを実装します。すべてのクラスでInitiatePropertiesと呼びます。このメソッドは、コンストラクターに送信する引数を受け入れる必要があります。

2 .- factoryというモジュールを作成し、「Create」という単語にクラスと同じ名前を追加し、コンストラクターが必要とする同じ入力引数を使用してパブリック関数を作成します。この関数は、クラスをインスタンス化し、ポイント(1)で説明した開始サブルーチンを呼び出して、受け取った引数を渡す必要があります。最後に、インスタンス化され開始されたメソッドを返しました。

例:

カスタムクラスEmployeeがあるとします。前の例のように、名前と年齢でインスタンス化する必要があります。

これはInitiatePropertiesメソッドです。 m_nameおよびm_ageは、設定するプライベートプロパティです。

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

    m_name = name
    m_age = age

End Sub

そして今、工場モジュールで:

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.CreateClassA(arguments)factory.CreateClassB(other_arguments)などを呼び出すだけでインスタンス化するだけです。

編集

Stenciが指摘したように、コンストラクター関数でローカル変数を作成することを避けることにより、terser構文で同じことを行うことができます。たとえば、CreateEmployee関数は次のように記述できます。

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

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

End Function

どちらがいいですか。

107
bgusach

各クラスのFactoryメンバーを呼び出すクラスごとに1つ(または複数)constructorを含むInitモジュールを1つ使用します。

たとえば、Pointクラスの場合:

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

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

このアプローチの優れた点の1つは、式内でファクトリ関数を簡単に使用できることです。たとえば、次のようなことができます。

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

または:

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

それはきれいです:ファクトリはほとんど何もせず、すべてのオブジェクトにわたって一貫して行います。作成と、各creatorでのInit呼び出しだけです。

そして、それはかなりオブジェクト指向です:Init関数はオブジェクト内で定義されます。

編集

これにより静的メソッドを作成できることを付け加えるのを忘れました。たとえば、次のようなことができます(パラメーターをオプションにした後):

NewLine.DeleteAllLinesShorterThan 10

残念ながら、オブジェクトの新しいインスタンスは毎回作成されるため、実行後に静的変数は失われます。この擬似静的メソッドで使用される行のコレクションおよびその他の静的変数は、モジュールで定義する必要があります。

32
stenci

クラスモジュールをエクスポートしてメモ帳でファイルを開くと、上部近くに隠された属性の束があります(VBEはそれらを表示せず、機能のほとんどをTweakに公開しません)。それらの1つはVB_PredeclaredId

Attribute VB_PredeclaredId = False

モジュールをTrueに設定して保存し、モジュールをVBAプロジェクトに再インポートします。

PredeclaredIdを持つクラスには、UserFormモジュールとまったく同じように無料で取得できる「グローバルインスタンス」があります(ユーザーフォームをエクスポートすると、predeclaredId属性がtrueに設定されます)。

多くの人は、状態を保存するために事前宣言されたインスタンスを喜んで使用しています。それは間違っています-インスタンスの状態を静的クラスに保存するようなものです!

代わりに、そのデフォルトインスタンスを活用してファクトリメソッドを実装します。

[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 'returns the newly created instance
    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デフォルトインスタンスで機能しています。つまり、typeのメンバーと見なされ、デフォルトインスタンスからのみ呼び出されます。

問題は、これも完全に合法であることです:

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

これは、ややこしいです。これは、混乱を招くAPIができたからです。 '@Descriptionアノテーション/ VB_Description属性を使用して使用を文書化できますが、Rubberduckがなければ、呼び出しサイトでその情報を表示するエディターには何もありません。

また、Property Letメンバーにアクセスできるため、Employeeインスタンスはmutableです。

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

コツは、クラスにinterfaceを実装させて、公開する必要があるものだけを公開することです:

[IEmployeeクラス]

Option Explicit

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

そして今、EmployeeimplementIEmployeeを作成します-最終的なクラスは次のようになります:

[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 'returns the newly created instance
    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メソッドがインターフェイスを返し、インターフェイスdoes n'tProperty Letメンバーを公開することに注意してください。呼び出しコードは次のようになります。

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

また、クライアントコードはインターフェイスに対して記述されているため、emplが公開するメンバーはIEmployeeインターフェイスで定義されたメンバーだけです。つまり、Createメソッド、Selfゲッター、Property Letミューテーター:したがって、「具象」Employeeクラスを使用する代わりに、残りのコードは「抽象」IEmployeeインターフェースを使用して、不変のポリモーフィックオブジェクトを使用できます。

20
Mathieu Guindon

トリックを使用する

Attribute VB_PredeclaredId = True

私は別のよりコンパクトな方法を見つけました:

Option Explicit
Option Base 0
Option Compare Binary

Private v_cBox As ComboBox

'
' Class creaor
Public Function New_(ByRef cBox As ComboBox) As ComboBoxExt_c
  If Me Is ComboBoxExt_c Then
    Set New_ = New ComboBoxExt_c
    Call New_.New_(cBox)
  Else
    Set v_cBox = cBox
  End If
End Function

ご覧のように、クラスのプライベートメンバーの作成と設定(initなど)の両方を行うためにNew_コンストラクターが呼び出されるのは、非静的インスタンスで呼び出された場合、プライベートメンバーを再初期化するためです。ただし、フラグを設定することで回避できます。

2
Tomasz