モジュール切り分けの一つの考え方
モジュール切り分けの一つの考え方
長く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
メソッドにsuecNotSingleCharacter
とERR_SRC
を渡す。
同様に(3)の
If Not targetAlphabet Like "[A-Z]" Then _ Call raiseError(suecNotLargeAlphabet, ERR_SRC)
では、引数targetAlphabet
がアルファベット大文字かどうかを調べて、違っていたらraiseError
メソッドにsuecNotLargeAlphabet
とERR_SRC
を渡す。
(4)の
If Not ret Like "[A-Z]" Then _ Call raiseError(suecNotAllowedSize, ERR_SRC)
では、変換後の文字について、大文字アルファベットでなくなっていたら、raiseError
メソッドにsuecNotAllowedSize
とERR_SRC
を渡す。
このような形で、不適切な引数が渡されたらエラーを吐くようにした。
使ってみる
上記メソッドを搭載したモジュールにStringUtil
と名前を付けて、実験してみる。
こんな感じ。
また、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
プロパティの値をイミディエイトに吐き出させる。
実行すると、
こうなる。
どのモジュールのどのメソッドでエラーが発生したのか、追跡できるようになる。
おわりに
異様に長くなってしまった。
メソッドを切り分けるときの一つのパターンとして、ご紹介しました。