モジュール切り分けの一つの考え方

モジュール切り分けの一つの考え方

長くVBAをやっていると、「このメソッドは、他でも使い回せそうだなー。」という場面に結構出くわす。

今回は、モジュールを切り分けるときの一つの考えかたをば。

文字列操作メソッドに特化したモジュール

文字列を操作するメソッドを括りだして、一つのモジュールにまとめる、というのを例にする。

ただし、複数のメソッドを挙げても煩雑になるだけなので、メソッドは一つだけ、にする。

リスト1 大文字アルファベットを任意の数だけずらすメソッド
'///アルファベットを任意の数分だけずらす'
Public Function getOffsetLargeAlphabet( _
             ByVal targetAlphabet As String, _
             ByVal offsetSize As Long) As String    '……(1)'
  Dim ret As String * 1    '……(2)'
  '文字コードを取得'
  Dim charCode As Long    '……(3)'
  charCode = Asc(targetAlphabet)
  '文字コードを元に文字にする'
  ret = Chr(charCode + offsetSize)    '……(4)'
  '結果を返す'
  getOffsetLargeAlphabet = ret    '……(5)'
End Function

あまりにもしょうもないメソッドゆえ、説明は不要と思うが、一応。

まず(1)の

Public Function getOffsetLargeAlphabet( _
             ByVal targetAlphabet As String, _
             ByVal offsetSize As Long) As String

で引数と返り値を設定。

第1引数targetAlphabetは、変換前のアルファベットを受け取る。

第2引数offsetSizeは、ずらすサイズ。

たとえば、targetAlphabetが「B」で、offsetSizeが「3」だったら、「E」、offsetSizeが「-1」だったら、「A」を返す、ということ。

(2)の

Dim ret As String * 1

で返り値用の変数を準備。返すのは1文字と決まっているので、固定長にしている。

(3)からの2行

Dim charCode As Long
charCode = Asc(targetAlphabet)

では、変数charCodeを準備し、Asc関数を用いて変換前のアルファベットの文字コードを取得。

(4)の

ret = Chr(charCode + offsetSize)

で、今度はChr関数を用いて、必要なだけずらしたアルファベットを取得。

最後に(5)の

getOffsetLargeAlphabet = ret

で値を返しておしまい。

……とまあ、こんなしょうもないメソッド。

予想される不具合

しかし、たいていの人はすぐに気づくと思うが、リスト1のgetOffsetLargeAlphabetメソッドはかなり問題が多い。

まず、第1引数のtargetAlphabetに大文字アルファベットが渡される保障はどこにもないし、2文字以上の文字列が渡されることだってあり得る。

また、第2引数のoffsetSizeにしても、不適切な数値が与えられることに対して、何の対策もない。

だれでも思いつくような問題点に、何の対策もないのだ。

予想される不具合に対応する

こうした場合、対応は大きく分けて次の二つになろう。すなわち、

  • 不適切な引数が与えられたときは、特異な値を返すようにする
  • 不適切な引数が与えられたときは、エラーを起こす

この二つである。

一つ目は、そもそもエラーを起こさないというアプローチ。で、二つ目は積極的にエラーを起こすというアプローチである。

最近は、二つ目のアプローチを取ることが多い。

積極的にエラーを起こす

不適切な使い方が為された場合には、オリジナルのエラーを起こす、ということにする。そのために最近取り入れているのは、次の二つである。すなわち、

  • エラー発生原因を場合分けするために列挙体を用いる
  • メソッドにエラーソースを示す文字列を仕込んでおく

この二つである。順に説明する。

スト2 オリジナルのエラーを発生させる
'///宣言セクション'
'エラー番号の土台になる数字'
Private Const ERR_NUM_BASE As Long = 20000    '……(1)'

'エラー発生源識別用列挙体'
Private Enum ErrorCode    '……(2)'
  suecNotSingleCharacter = 1
  suecNotAllowedSize
  suecNotLargeAlphabet
End Enum

'Methods'
Private Sub raiseError(ByVal errCode As ErrorCode, _
                       ByVal errSource As String)    '……(3)'
  Dim errMsg As String
  'エラーメッセージを取得。適宜追加。'
  Select Case errCode
    Case suecNotSingleCharacter
      errMsg = "Arg ""targetAlphabet"" must be a single character."
    Case suecNotAllowedSize
      errMsg = "Arg ""offsetSize"" is not allowed size."
    Case suecNotLargeAlphabet
      errMsg = "Arg ""targetAlphabet"" is not large alphabet."
    Case Else
      errMsg = "Some error has occurred."
  End Select
  Call Err.Raise(Number:=ERR_NUM_BASE + errCode, _
                 Source:=errSource, _
                 Description:=errMsg)
End Sub

(1)の

Private Const ERR_NUM_BASE As Long = 20000

は、エラー番号の土台となる数字。

組み込みのエラー番号と重ならないように、自作エラー番号は10000番台を使うようにしている。今回は20000番台を使うことにした。用途別に切り分けたモジュールごとに付番しておけば良いと思う。

(2)の

Private Enum ErrorCode
  suecNotSingleCharacter = 1
  suecNotAllowedSize
  suecNotLargeAlphabet
End Enum

は、エラー発生原因識別用の列挙体。

このモジュール内でしか使わないので、Private指定。各要素の先頭にsuecとあるのは、名前が重ならないようにするための接頭語。このモジュールはStringUtilと名づけていて(su)、そのモジュールのErrorCodeという列挙体なので(ec)、というだけ。

(3)からがエラー発生用メソッド。

(3)の

Private Sub raiseError(ByVal errCode As ErrorCode, _
                       ByVal errSource As String)

で引数を指定。

第1引数のerrCodeはエラー発生原因識別用の値。

第2引数のerrSourceについては後述する。

このraiseErrorメソッドの残りの部分については説明を省くが、単に第1引数によって場合分けをして、ErrオブジェクトのRaiseメソッドでエラーを起こしているだけだ。

ただし、Raiseメソッドを用いる際に、引数Sourceまで指定しているところがポイント。

getOffsetLargeAlphabetメソッドの改良

以上のことを踏まえて、リスト1のgetOffsetLargeAlphabetを改良する。

まずは、改良後のコードをどうぞ。

リスト3 getOffsetLargeAlphabetメソッド
Public Function getOffsetLargeAlphabet( _
             ByVal targetAlphabet As String, _
             ByVal offsetSize As Long) As String
  Dim ret As String * 1
  
  'エラーソース文字列……(1)'
  Const ERR_SRC As String = _
        "StringUtil Module : getOffsetLargeAlphabet Method"
  'ガード節'
  '1文字でなかったらエラー……(2)'
  If Len(targetAlphabet) <> 1 Then _
    Call raiseError(suecNotSingleCharacter, ERR_SRC)
  '大文字のアルファベットでなかったらエラー……(3)'
  If Not targetAlphabet Like "[A-Z]" Then _
    Call raiseError(suecNotLargeAlphabet, ERR_SRC)
  '文字コードを取得'
  Dim charCode As Long
  charCode = Asc(targetAlphabet)
  ret = Chr(charCode + offsetSize)
  'ずらした結果、大文字アルファベットでなくなる場合はエラー……(4)'
  If Not ret Like "[A-Z]" Then _
    Call raiseError(suecNotAllowedSize, ERR_SRC)
  
  '結果を返す'
  getOffsetLargeAlphabet = ret
End Function

リスト1から変わったところに番号を付した。

まず(1)の

Const ERR_SRC As String = _
      "StringUtil Module : getOffsetLargeAlphabet Method"

リスト(2)のエラー発生用メソッドraiseErrorに渡す第2引数用の文字列。

このメソッド固有の文字列なので、プロシージャレベルの定数にした。

プロシージャレベルの定数は、こういうふうに使えば良いのだ。

次に、(2)の

If Len(targetAlphabet) <> 1 Then _
  Call raiseError(suecNotSingleCharacter, ERR_SRC)

では、引数targetAlphabetの文字数を調べ、1文字でなかったら、raiseErrorメソッドにsuecNotSingleCharacterERR_SRCを渡す。

同様に(3)の

If Not targetAlphabet Like "[A-Z]" Then _
  Call raiseError(suecNotLargeAlphabet, ERR_SRC)

では、引数targetAlphabetがアルファベット大文字かどうかを調べて、違っていたらraiseErrorメソッドにsuecNotLargeAlphabetERR_SRCを渡す。

(4)の

If Not ret Like "[A-Z]" Then _
  Call raiseError(suecNotAllowedSize, ERR_SRC)

では、変換後の文字について、大文字アルファベットでなくなっていたら、raiseErrorメソッドにsuecNotAllowedSizeERR_SRCを渡す。

このような形で、不適切な引数が渡されたらエラーを吐くようにした。

使ってみる

上記メソッドを搭載したモジュールにStringUtilと名前を付けて、実験してみる。

f:id:akashi_keirin:20190826191829g:plain

f:id:akashi_keirin:20190826191843g:plain

こんな感じ。

また、Err.Raiseメソッドを用いる際に、引数Sourceを指定していることについては、次のようなコードで実験。

リスト4
Private Sub test()
  On Error Resume Next
  Call Err.Clear
  Debug.Print StringUtil.getOffsetLargeAlphabet("AHO", 2)
  Debug.Print Err.Source
  Call Err.Clear
  Debug.Print StringUtil.getOffsetLargeAlphabet("A", 28)
  Debug.Print Err.Source
  Call Err.Clear
  Debug.Print StringUtil.getOffsetLargeAlphabet("a", 2)
  Debug.Print Err.Source
  On Error GoTo 0
End Sub

このように、getOffsetLargeAlphabetメソッドにわざとエラーを起こすような引数を渡し、そのたびにErrオブジェクトのSourceプロパティの値をイミディエイトに吐き出させる。

実行すると、

f:id:akashi_keirin:20190826191814j:plain

こうなる。

どのモジュールのどのメソッドでエラーが発生したのか、追跡できるようになる。

おわりに

異様に長くなってしまった。

メソッドを切り分けるときの一つのパターンとして、ご紹介しました。