🦒

Excel VBAをクラスを使って改善しよう: タイプセーフEnum

2022/07/04に公開

タイプセーフEnumとは

Excel VBAにはEnumという機能があります。
数値に変数名を付けることができ、値を自動で指定することも、任意の値を指定することもできます。

この機能を利用して、配列の列名に使ったり、区分値に使ったりすることがあります。

例えば、以下のようなアンケート紹介会社の会員一覧を表示する例を考えてみます。

標準モジュール:sample_enumation
Option Explicit

Public Enum UserTableColumnPosition
    POSITON_ID = 1
    POSITON_NAME
    POSITON_AGE
    POSITON_MARRIED
End Enum

Public Enum MarriedType
    TYPE_NO = 0
    TYPE_YES = 1
    TYPE_NO_ANSWER = 9
End Enum

Public Sub Test()

    ' 例えばシートからデータを取得して二次元配列に格納されていたとする
    Dim sampleInputData() As Variant

    With ThisWorkbook.Worksheets("Sheet1").Range("A1").CurrentRegion
        sampleInputData = .Rows(2 & ":" & .Rows.Count).value
    End With

    ' 表示確認
    Dim rowIndex As Long
    For rowIndex = LBound(sampleInputData, 1) To UBound(sampleInputData, 1)
        Call Output( _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITON_NAME), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITON_AGE), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITON_MARRIED))
    Next
End Sub

Private Sub Output(pUserName As Variant, pAge As Variant, pMarriedType As Variant)
    Debug.Print pUserName + "/" + CStr(pAge) + "歳" + "/" + "結婚歴:" + MarriedTypeName(CLng(pMarriedType))

End Sub


Private Function MarriedTypeName(value As MarriedType) As String

    Select Case value
        Case MarriedType.TYPE_NO
            MarriedTypeName = "未婚"
        Case MarriedType.TYPE_YES
            MarriedTypeName = "既婚"
        Case MarriedType.TYPE_NO_ANSWER
            MarriedTypeName = "非公開"
        Case Else
            Debug.Print "未定義の区分値が引数に指定された!"
            Debug.Assert False
    End Select

End Function

Test()プロシージャを実行すると、イミディエイトウィンドウには、

山田/25歳/結婚歴:未婚
田中/40歳/結婚歴:既婚
佐藤/35歳/結婚歴:非公開

と表示されます。

Enumの問題点とは

ただし、Enumには次のような問題があります。

  • 引数で型を指定しても実体はLong型であるため、不正な値を指定できる
  • Enum値に応じた名前や個別の処理を行いたい場合、至る所にEnum値を使ったSelect Case文が必要になるため、修正が大変

例で言うと、MarriedTypeName()ファンクションの引数はMarriedTypeですが、Long型の数値であれば実は何でも渡すことができます。
結果、Case Elseに該当し、アサーションが発生してしまいます。

Public Sub TestNoEnum()
    Call MarriedTypeName(2) 'これはDebug.Asssetで一時中断する
End Sub

このケースのように例外を扱えるようにしていればまだしも、Case Elseがなく、予期しない動作となった場合、デバッガでコードを追いかけることになります。

追加の要件が発生

さて、ここで追加の要件があり、指定した結婚歴に応じて一覧から対象者を抽出することとなりました。

例えば、

  1. 独身者向けアンケートでは、独身であることが条件です。
  2. 既婚者向けアンケートでは、結婚していることが条件です。

このようなケースを考えてみます。

一般的な方法では、各アンケート分、サブルーチンを作って対応することになるかと思います。

Public Sub TestMarried()
    Dim sampleInputData() As Variant
    With ThisWorkbook.Worksheets("Sheet1").Range("A1").CurrentRegion
        sampleInputData = .Rows(2 & ":" & .Rows.Count).value
    End With

    ' 表示確認
    Dim rowIndex As Long
    For rowIndex = LBound(sampleInputData, 1) To UBound(sampleInputData, 1)
        Call OutputMarried( _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_NAME), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_AGE), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_MARRIED))
    Next
End Sub

Public Sub TestNoMarried()

    Dim sampleInputData() As Variant
    With ThisWorkbook.Worksheets("Sheet1").Range("A1").CurrentRegion
        sampleInputData = .Rows(2 & ":" & .Rows.Count).value
    End With

    ' 表示確認
    Dim rowIndex As Long
    For rowIndex = LBound(sampleInputData, 1) To UBound(sampleInputData, 1)
        Call OutputNoMarried( _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_NAME), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_AGE), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_MARRIED))
    Next
End Sub

Private Sub OutputMarried(pUserName As Variant, pAge As Variant, pMarriedType As Variant)

    Select Case CLng(pMarriedType)
        Case MarriedType.TYPE_NO
            '何もしない
        Case MarriedType.TYPE_YES
            Call Output(pUserName, pAge, pMarriedType)
        Case MarriedType.TYPE_NO_ANSWER
            '何もしない
        Case Else
            Debug.Print "未定義の区分値が引数に指定された!"
            Debug.Assert False
    End Select

End Sub

Private Sub OutputNoMarried(pUserName As Variant, pAge As Variant, pMarriedType As Variant)

    Select Case CLng(pMarriedType)
        Case MarriedType.TYPE_NO
            Call Output(pUserName, pAge, pMarriedType)
        Case MarriedType.TYPE_YES
            '何もしない
        Case MarriedType.TYPE_NO_ANSWER
            '何もしない
        Case Else
            Debug.Print "未定義の区分値が引数に指定された!"
            Debug.Assert False
    End Select

End Sub

さらなる追加の要件が・・・

現在は名前と結婚歴だけを表示していますが、紹介するアンケートには離婚歴などで紹介可否がある場合があるので、結婚歴に状態を追加したとします。
この時、

  • 離婚歴がない場合は現状と変わらない表示
  • 離婚して結婚していない場合は、離婚と表示
  • 離婚して結婚した場合は、再婚と表示

という要件が発生した場合、どうしたらよいでしょうか。
例でいうと以下のようになります。

サンプル(未婚、離婚歴なし) => 名前/年齢/結婚歴:未婚
サンプル(未婚、離婚歴あり) => 名前/年齢/結婚歴:離婚
サンプル(既婚、離婚歴なし) => 名前/年齢/結婚歴:既婚
サンプル(未婚、離婚歴あり) => 名前/年齢/結婚歴:再婚
サンプル(非公開、離婚歴有無どちらでも) => 名前/年齢/結婚歴:非公開

ワークシート上は以下のような入力になります。
この時、結婚歴の値2は離婚、値3は再婚とします。

この要件に対応した場合、上記の表示以外にもアンケート対象者の抽出条件にも影響があります。

  • 独身者向けアンケートの対象に離婚歴があるユーザも追加する
  • 既婚者向けアンケートの対象に再婚しているユーザも追加する

この時のコードは次のようになります。Select Caseの修正があちこちに発生している点に注意して下さい。

Public Enum MarriedType '数値が読みにくいので並び変えました
    TYPE_NO = 0
    TYPE_YES = 1
    TYPE_X_NO = 2 '離婚を追加
    TYPE_X_YES = 3 '再婚を追加
    TYPE_NO_ANSWER = 9
End Enum

Private Function MarriedTypeName(value As MarriedType) As String

    Select Case value
        Case MarriedType.TYPE_NO
            MarriedTypeName = "未婚"
        Case MarriedType.TYPE_YES
            MarriedTypeName = "既婚"
        Case MarriedType.TYPE_X_NO
            MarriedTypeName = "離婚" '追加
        Case MarriedType.TYPE_X_YES
            MarriedTypeName = "再婚" '追加
        Case MarriedType.TYPE_NO_ANSWER
            MarriedTypeName = "非公開"
        Case Else
            Debug.Print "未定義の区分値が引数に指定された!"
            Debug.Assert False
    End Select

End Function

これで問題なく表示ができます。

山田/25歳/結婚歴:未婚
田中/40歳/結婚歴:既婚
佐藤/35歳/結婚歴:非公開
石井/24歳/結婚歴:離婚
古田/50歳/結婚歴:再婚

アンケートの条件については、既存のプロシージャに次のような拡張を行いました。

Public Sub TestMarriedEx()
    Dim sampleInputData() As Variant
    With ThisWorkbook.Worksheets("Sheet2").Range("A1").CurrentRegion
        sampleInputData = .Rows(2 & ":" & .Rows.Count).value
    End With

    ' 表示確認
    Dim rowIndex As Long
    For rowIndex = LBound(sampleInputData, 1) To UBound(sampleInputData, 1)
        Call OutputMarriedEx( _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_NAME), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_AGE), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_MARRIED))
    Next
End Sub

Public Sub TestNoMarriedEx()
    Dim sampleInputData() As Variant
    With ThisWorkbook.Worksheets("Sheet2").Range("A1").CurrentRegion
        sampleInputData = .Rows(2 & ":" & .Rows.Count).value
    End With

    ' 表示確認
    Dim rowIndex As Long
    For rowIndex = LBound(sampleInputData, 1) To UBound(sampleInputData, 1)
        Call OutputNoMarriedEx( _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_NAME), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_AGE), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_MARRIED))
    Next
End Sub

Private Sub OutputMarriedEx(pUserName As Variant, pAge As Variant, pMarriedType As Variant)
    Select Case CLng(pMarriedType)
        Case MarriedType.TYPE_NO
            '何もしない
        Case MarriedType.TYPE_YES
            Call Output(pUserName, pAge, pMarriedType)
        Case MarriedType.TYPE_X_NO
            '何もしない
        Case MarriedType.TYPE_X_YES
            Call Output(pUserName, pAge, pMarriedType)
        Case MarriedType.TYPE_NO_ANSWER
            '何もしない
        Case Else
            Debug.Print "未定義の区分値が引数に指定された!"
            Debug.Assert False
    End Select
End Sub

Private Sub OutputNoMarriedEx(pUserName As Variant, pAge As Variant, pMarriedType As Variant)
    Select Case CLng(pMarriedType)
        Case MarriedType.TYPE_NO
            Call Output(pUserName, pAge, pMarriedType)
        Case MarriedType.TYPE_YES
            '何もしない
        Case MarriedType.TYPE_X_NO
            Call Output(pUserName, pAge, pMarriedType)
        Case MarriedType.TYPE_X_YES
            '何もしない
        Case MarriedType.TYPE_NO_ANSWER
            '何もしない
        Case Else
            Debug.Print "未定義の区分値が引数に指定された!"
            Debug.Assert False
    End Select
End Sub

ここまで何とかなったように見えますが、実は修正不足があります。
Enum値に2(離婚)を追加したことで、

Public Sub TestNoEnum()
    Call MarriedTypeName(2) 'これはDebug.Asssetで一時中断する
End Sub

このコードはアサーションで一時中断せず、正常終了するようになってしまいました。
Enum値の判断ロジックは全てSubプロシージャ内で実行されいたため、なかなか気が付きにくい不具合です。
修正する場合は、引数に10など、Enum値にない値を修正する必要があります。

以上のように、クラスを導入しなくても要件を満たすことは可能です。
ただし、機能性以外の観点(保守性、拡張性、可読性など)は著しく損なわれています。

クラスを導入する

それではクラスを使ってみて、違いを見ていきます。
まず、一番はじめの要件を満たすコードです。

Enum値をまとめて扱うクラスです。

クラスモジュール:MarriedEnum
Option Explicit

Private OBJENUM_TYPE_NOSET_ As MarriedEnumValue
Private OBJENUM_TYPE_NO_ As MarriedEnumValue
Private OBJENUM_TYPE_YES_ As MarriedEnumValue
Private OBJENUM_TYPE_NO_ANSWER_ As MarriedEnumValue

Private valueOfDic_ As Dictionary

Private Sub Class_Initialize()
    Set OBJENUM_TYPE_NOSET_ = New MarriedNoSetValue
    Set OBJENUM_TYPE_NO_ = New NoMarriedValue
    Set OBJENUM_TYPE_YES_ = New MarriedValue
    Set OBJENUM_TYPE_NO_ANSWER_ = New NoAnswerValue
    
    Set valueOfDic_ = New Dictionary
    Call Init
End Sub

Private Sub Init()
    Call valueOfDic_.Add(OBJENUM_TYPE_NOSET.Value, OBJENUM_TYPE_NOSET)
    Call valueOfDic_.Add(OBJENUM_TYPE_NO.Value, OBJENUM_TYPE_NO)
    Call valueOfDic_.Add(OBJENUM_TYPE_YES.Value, OBJENUM_TYPE_YES)
    Call valueOfDic_.Add(OBJENUM_TYPE_NO_ANSWER.Value, OBJENUM_TYPE_NO_ANSWER)
End Sub

Public Function ValueOf(pValue As MarriedType) As MarriedEnumValue
    If Not valueOfDic_.Exists(pValue) Then
        Set ValueOf = OBJENUM_TYPE_NOSET_
        Exit Function
    End If
    
    Set ValueOf = valueOfDic_(pValue)
End Function

Public Property Get OBJENUM_TYPE_NOSET() As MarriedEnumValue
    Set OBJENUM_TYPE_NOSET = OBJENUM_TYPE_NOSET_
End Property

Public Property Get OBJENUM_TYPE_NO() As MarriedEnumValue
    Set OBJENUM_TYPE_NO = OBJENUM_TYPE_NO_
End Property

Public Property Get OBJENUM_TYPE_YES() As MarriedEnumValue
    Set OBJENUM_TYPE_YES = OBJENUM_TYPE_YES_
End Property

Public Property Get OBJENUM_TYPE_NO_ANSWER() As MarriedEnumValue
    Set OBJENUM_TYPE_NO_ANSWER = OBJENUM_TYPE_NO_ANSWER_
End Property

Enum値を表すクラスです。

クラスモジュール:MarriedEnumValue
Option Explicit

Public Property Get Value() As Long
End Property

Public Property Get Name() As String
End Property

Public Function IsMarried() As Boolean
End Function

Public Function IsNoMarried() As Boolean
End Function

Public Function Equals(pTarget As MarriedEnumValue) As Boolean
End Function

設定なしのEnum値クラス

クラスモジュール:MarriedNoSetValue
Option Explicit
Implements MarriedEnumValue

Private value_ As MarriedType
Private name_ As String

Private Sub Class_Initialize()
    value_ = MarriedType.TYPE_NO_SET
    name_ = "設定なし"
End Sub

Private Property Get MarriedEnumValue_Name() As String
    MarriedEnumValue_Name = name_
End Property

Private Property Get MarriedEnumValue_Value() As Long
    MarriedEnumValue_Value = value_
End Property

Private Function MarriedEnumValue_Equals(pTarget As MarriedEnumValue) As Boolean
    MarriedEnumValue_Equals = (value_ = pTarget.Value)
End Function

Private Function MarriedEnumValue_IsMarried() As Boolean
    MarriedEnumValue_IsMarried = False
End Function

Private Function MarriedEnumValue_IsNoMarried() As Boolean
    MarriedEnumValue_IsNoMarried = False
End Function

既婚を表すEnum値クラス

クラスモジュール:MarriedValue
Option Explicit
Implements MarriedEnumValue

Private value_ As MarriedType
Private name_ As String

Private Sub Class_Initialize()
    value_ = MarriedType.TYPE_YES
    name_ = "既婚"
End Sub

Private Property Get MarriedEnumValue_Name() As String
    MarriedEnumValue_Name = name_
End Property

Private Property Get MarriedEnumValue_Value() As Long
    MarriedEnumValue_Value = value_
End Property

Private Function MarriedEnumValue_Equals(pTarget As MarriedEnumValue) As Boolean
    MarriedEnumValue_Equals = (value_ = pTarget.Value)
End Function

Private Function MarriedEnumValue_IsMarried() As Boolean
    MarriedEnumValue_IsMarried = True
End Function

Private Function MarriedEnumValue_IsNoMarried() As Boolean
    MarriedEnumValue_IsNoMarried = False
End Function

未婚を表すEnum値クラス

クラスモジュール:NoMarriedValue
Option Explicit
Implements MarriedEnumValue

Private value_ As MarriedType
Private name_ As String

Private Sub Class_Initialize()
    value_ = MarriedType.TYPE_NO
    name_ = "未婚"
End Sub

Private Property Get MarriedEnumValue_Name() As String
    MarriedEnumValue_Name = name_
End Property

Private Property Get MarriedEnumValue_Value() As Long
    MarriedEnumValue_Value = value_
End Property

Private Function MarriedEnumValue_Equals(pTarget As MarriedEnumValue) As Boolean
    MarriedEnumValue_Equals = (value_ = pTarget.Value)
End Function

Private Function MarriedEnumValue_IsMarried() As Boolean
    MarriedEnumValue_IsMarried = False
End Function

Private Function MarriedEnumValue_IsNoMarried() As Boolean
    MarriedEnumValue_IsNoMarried = True
End Function

実行を確認する標準モジュール。

標準モジュール:sample_enumation_class
Option Explicit

Public Enum MarriedType
    TYPE_NO_SET = -1 '設定していない状態を表すEnum値を追加
    TYPE_NO = 0
    TYPE_YES = 1
'    初めの要件を満たすためコメントアウト
'    TYPE_X_NO = 2 '離婚を追加
'    TYPE_X_YES = 3 '再婚を追加
    TYPE_NO_ANSWER = 9
End Enum

Public Sub TestWithClass()
    Dim sampleInputData() As Variant
    With ThisWorkbook.Worksheets("Sheet1").Range("A1").CurrentRegion
        sampleInputData = .Rows(2 & ":" & .Rows.Count).Value
    End With

    ' Enumクラスを作成
    Dim vMarriedEnum As MarriedEnum
    Set vMarriedEnum = New MarriedEnum

    ' 表示確認
    Dim rowIndex As Long
    For rowIndex = LBound(sampleInputData, 1) To UBound(sampleInputData, 1)
        
        Dim vEnumValue As Long
        vEnumValue = CLng(sampleInputData(rowIndex, UserTableColumnPosition.POSITION_MARRIED))
        ' Enumクラスから値クラスを取得
        Dim vMarriedEnumValue As MarriedEnumValue
        Set vMarriedEnumValue = vMarriedEnum.ValueOf(vEnumValue)
        
        Call Output( _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_NAME), _
            sampleInputData(rowIndex, UserTableColumnPosition.POSITION_AGE), _
            vMarriedEnumValue.Name) '値のクラスの名前を引数として渡す
    Next
End Sub

Private Sub Output(pUserName As Variant, pAge As Variant, pMarriedTypeName As String)
    Debug.Print pUserName + "/" + CStr(pAge) + "歳" + "/" + "結婚歴:" + pMarriedTypeName
End Sub

Public Sub TestNoEnumWithClass()
    ' Enumクラスを作成
    Dim vMarriedEnum As MarriedEnum
    Set vMarriedEnum = New MarriedEnum
    
    ' Enumクラスから値クラスを取得
    Dim vMarriedEnumValue As MarriedEnumValue
    Set vMarriedEnumValue = vMarriedEnum.ValueOf(2)
        
    If vMarriedEnumValue.Equals(vMarriedEnum.OBJENUM_TYPE_NOSET) Then
        Debug.Print "Enumにない値が指定されたのでNoSetが返る。期待通り"
    Else
        Debug.Print "想定外のEnum値が追加されているのでNoSetではない"
        Debug.Print vMarriedEnumValue.Value, vMarriedEnumValue.Name
        Debug.Assert False
    End If
End Sub

なお、クラス化とはあまり関係ありませんが、未設定状態を表すEnum値を追加しています。

TYPE_NO_SET = -1 '設定していない状態を表すEnum値を追加

タイプセーフEnumの実装方法

MarriedEnumクラスは各Enum値クラスをメンバーとして持っています。
ただ、持ち方に特徴があって、MarriedEnumValue型として持っています。

メンバーはコンストラクタ(Class_Initialize())で初期化していますが、各Enum値を表す実クラスで初期化しています。

これはポリモーフィズムを使って各実装クラスをMarriedEnumValue型として扱っています。
MarriedEnumValue型としていても、インスタンス(オブジェクト)はNewした型に依存します。

クラス図で示すと以下のようになります。

図のIマークはインターフェースを表しています。Excel VBAではインターフェースとは呼称しません(前述のコードのとおり、classで定義しています)が、一般的なオブジェクト指向言語における使われ方からインターフェースとしてします。

インターフェースは期待される振る舞いを定義する(メソッドの引数と戻り値)だけで、実装方法はimplementsした実装クラスに依存します。

MarriedEnumクラスは各Enum値を読み取り専用プロパティとして公開しています。
各Enum値のメンバはPrivateにして隠蔽します。

また、内部にDictionaryで値とオブジェクトのマッピングを作成しています。
これはSelect Caseの代替です。
ただし、クラスを使わない実装では複数のSelect Caseがありましたが、クラスではここだけです。

次にEnum値クラスを見ていきます。
MarriedEnumValueインターフェースにはValue()、Name()の読み取り専用プロパティを公開しています。実装クラスは適切な値を返すよう期待されています。
実装クラスは、これらのプロパティに定数を返しています。

Private value_ As MarriedType
Private name_ As String

Private Sub Class_Initialize()
    value_ = MarriedType.TYPE_YES '元のEnum定義の値を指定
    name_ = "既婚" '名前を設定
End Sub

Private Property Get MarriedEnumValue_Name() As String
    MarriedEnumValue_Name = name_
End Property

Private Property Get MarriedEnumValue_Value() As Long
    MarriedEnumValue_Value = value_
End Property

各Enum値に1つの実装クラスを定義していきます。

また、各Enum値には既婚か未婚かを返すメソッドが定義されています。

Public Function IsMarried() As Boolean
End Function

Public Function IsNoMarried() As Boolean
End Function

これは次の利用方法で詳細を見ていきます。

タイプセーフEnumの利用方法

利用方法は簡単です。
通常のクラス利用と同様、Newしてオブジェクトを生成します。
次いで、入力値からEnum値オブジェクトをValueOf()メソッドで取得します。
ValueOf()メソッドはMarriedEnumValueを返します。

' Enumクラスを作成
Dim vMarriedEnum As MarriedEnum
Set vMarriedEnum = New MarriedEnum

' Enumクラスから値クラスを取得
Dim vMarriedEnumValue As MarriedEnumValue
Set vMarriedEnumValue = vMarriedEnum.ValueOf(vEnumValue)

あらかじめ必要なEnum値が分かっている場合は、読み取り専用プロパティから選択できます。

vMarriedEnum.OBJENUM_TYPE_NOSET

取得したEnum値は実装クラスに依存した未婚、既婚を返します。
クラスを利用しない場合は、Enum値で分岐して処理を行うという発想です。
クラスを利用する場合は、それぞれのEnum値に適切な振る舞いを期待するという発想に切り替えます。

MarriedEnumValue型でオブジェクトを利用すると、ポリモーフィズムの機能によって適切な未婚、既婚を返すことができます。

追加要件への対応

ここまでクラスを使った実装方法を確認してきましたが、これだけではあまりメリットは感じられません。

まず、通常のEnum定義に値を追加します。

Public Enum MarriedType
    TYPE_NO_SET = -1 '設定していない状態を表すEnum値を追加
    TYPE_NO = 0
    TYPE_YES = 1
    TYPE_X_NO = 2 '離婚を追加
    TYPE_X_YES = 3 '再婚を追加
    TYPE_NO_ANSWER = 9
End Enum

次に、Enum値クラスを作成します。MarriedXValue型としました。定数を設定するコンストラクタのみ抜粋します。

Private Sub Class_Initialize()
    value_ = MarriedType.TYPE_X_YES
    name_ = "再婚"
End Sub

同様に離婚も追加します。

Private Sub Class_Initialize()
    value_ = MarriedType.TYPE_X_NO
    name_ = "離婚"
End Sub

MarriedEnumクラスは以下の修正を行います。

Private OBJENUM_TYPE_NOSET_ As MarriedEnumValue
Private OBJENUM_TYPE_NO_ As MarriedEnumValue
Private OBJENUM_TYPE_YES_ As MarriedEnumValue
Private OBJENUM_TYPE_X_YES_ As MarriedEnumValue '追加
Private OBJENUM_TYPE_X_NO_ As MarriedEnumValue '追加
Private OBJENUM_TYPE_NO_ANSWER_ As MarriedEnumValue
Private valueOfDic_ As Dictionary

Private Sub Class_Initialize()
    Set OBJENUM_TYPE_NOSET_ = New MarriedNoSetValue
    Set OBJENUM_TYPE_NO_ = New NoMarriedValue
    Set OBJENUM_TYPE_YES_ = New MarriedValue
    Set OBJENUM_TYPE_X_YES_ = New MarriedXValue '追加
    Set OBJENUM_TYPE_X_NO_ = New NoMarriedXValue '追加
    Set OBJENUM_TYPE_NO_ANSWER_ = New NoAnswerValue
    
    Set valueOfDic_ = New Dictionary
    Call Init
End Sub

Private Sub Init()
    Call valueOfDic_.Add(OBJENUM_TYPE_NOSET.Value, OBJENUM_TYPE_NOSET)
    Call valueOfDic_.Add(OBJENUM_TYPE_NO.Value, OBJENUM_TYPE_NO)
    Call valueOfDic_.Add(OBJENUM_TYPE_YES.Value, OBJENUM_TYPE_YES)
    Call valueOfDic_.Add(OBJENUM_TYPE_X_YES.Value, OBJENUM_TYPE_X_YES) '追加
    Call valueOfDic_.Add(OBJENUM_TYPE_X_NO.Value, OBJENUM_TYPE_X_NO) '追加
    Call valueOfDic_.Add(OBJENUM_TYPE_NO_ANSWER.Value, OBJENUM_TYPE_NO_ANSWER)
End Sub

'追加
Public Property Get OBJENUM_TYPE_X_YES() As MarriedEnumValue
    Set OBJENUM_TYPE_X_YES = OBJENUM_TYPE_X_YES_
End Property
'追加
Public Property Get OBJENUM_TYPE_X_NO() As MarriedEnumValue
    Set OBJENUM_TYPE_X_NO = OBJENUM_TYPE_X_NO_
End Property

追加と記述された部分を変更しています。
他のコードは修正しなくても動作します。

また、テストで追加したTestNoEnumWithClass()ファンクションはこの修正で期待通りでないことを明示してくれますので、すぐに修正できます。

これ以外の修正は不要です!
判断するロジックに変更が入らないため、修正箇所もMarriedEnum内に閉じています。他はクラスを追加するだけですので影響もありません。

この拡張性、感じてもらえたでしょうか。

まとめ

長い説明となりましたが、クラスの効能を理解し、Excel マクロでクラスを使う人が増えることを願っています。

また、ちょっと難しいなと感じた人向けにそもそもクラスとは何か、オブジェクト指向開発とは、といった入門書を執筆中です。
公開しましたらそちらもよろしくお願いします。

Discussion