0から始めるPHPプログラミング #1-7 VBAで入力チェックを実装する その7
こんにちは。
前回は入力チェックが上級編に入りました。
定数、変数の宣言をして、初期値を設定したんでしたね。今回はその続きをしていきましょう。[チェック情報]シートに記載した内容を基にチェック処理ができるようにしていきます。
それでは参りましょう(今回、長めです)。
フローチャートの確認
これまでは処理がそこまで複雑ということもなく、処理の流れよりも制御構文やソースの書き方がメインだったので特に用意しなかったのですが、通常、機能の設計書にはフローチャートというものが存在します。
フローチャートとは、とある機能を実現するための処理の流れを図形の組み合わせで示したものです。これをご覧ください。
画像クリックで拡大表示できます。
この上級編辺りから処理全体の流れを把握していただき、かつその中で今どこを実装しているのか、という視点も加えていきましょう。
前回は「変数に初期値を実装する」までやりました。
配列を走査する
それでは実装です。
まず、今のソースが・・・
Sub validation4()
'必須チェックテキスト
Const REQUIRED_TEXT = "が入力されていません。" & vbCrLf
'形式チェックテキスト
Const FORMAT_TEXT As String = "で入力してください。" & vbCrLf
'必須チェックメッセージ
'Const REQUIRED_MESSAGE = "{0}が入力されていません。"
'形式チェックメッセージ
'Const FORMAT_MESSAGE = "{0}は{1}形式の{2}で入力してください。"
'正常メッセージ
'Const NO_PROBLEM_MESSAGE = "入力チェックを行いました。問題ありません。"
'入力フォームシートの名称
'Const FORM_SHEET_NAME = "個人情報記入票"
'チェック情報シートの名称
'Const CHECK_SHEET_NAME = "チェック情報"
'入力フォームのシート
Dim formSheet As Worksheet
'チェック情報のシート
Dim checkSheet As Worksheet
'項目名
Dim itemName As String
'セル番地
Dim cellAddress As String
'正規表現パターン
Dim itemRegExpPattern As String
'項目の形式
Dim itemFormat As String
'項目のデータ型
Dim itemType As String
'項目値
Dim itemValue As String
'メッセージ
Dim message As String
'正規表現
Dim regExp As Variant
'チェック対象の最終行
Dim checkLastRow As Integer
'チェック対象項目情報配列
Dim checkItemArray As Variant
'入力フォームシートの設定
Set formSheet = Worksheets("個人情報記入票")
'チェック情報シートの設定
Set checkSheet = Worksheets("チェック情報")
'正規表現オブジェクトの実体化
Set regExp = CreateObject("VBScript.RegExp")
'チェック対象の最終行
checkLastRow = checkSheet.Cells(checkSheet.Rows.Count, 1).End(xlUp).Row
'チェック情報を配列に保持
checkItemArray = checkSheet.Range("A2:E" & checkLastRow)
End Sub
こういう状態でしたね。
続きのところから早速チェック処理を書いていきます。
まず、以下のソースを下図のように実装してください。
For i = 1 To UBound(checkItemArray)
'項目名を取得
itemName = checkItemArray(i, 1)
'セル番地を取得
cellAddress = checkItemArray(i, 2)
'セル番地から値を取得
itemValue = formSheet.Range(cellAddress).Value
'開発者用。値確認
Debug.Print "itemName:" & itemName & " cellAddress:" & cellAddress & " itemValue:" & itemValue
Next
新しい要素はあまりないのですが、今回は初めて配列でループを回します。
前回までのループ処理は
For i = 4 To 30
こういう風に書いて、入力フォームシートの4行目から30行目まで、というように決め打ちしていました。そこが今回は・・・
For i = 1 To UBound(checkItemArray)
このようになっています。新しい関数の登場です。
UBound(ゆーばうんど)関数は指定した配列の要素数を返してくれます。
↑こちら、公式リファレンスです。
この関数は配列の要素数を返してくれます。で、前回のあとがきでお伝えしたのですが、この変数は二次元配列なんでしたよね。
UBound関数は
UBound( 配列 , 要素数を知りたい配列の次元数 )
という風に指定します。で、第二引数は省略可能です。省略した場合は1が指定されるので、今回の
For i = 1 To UBound(checkItemArray)
この場合、指定した二次元配列の一次元目の要素数を返す、という動きをします。
つまり・・・!
一次元目の要素数である14という数字を返してくれます。
配列が上図の選択範囲を保持している根拠は
'チェック情報を配列に保持
checkItemArray = checkSheet.Range("A2:E" & checkLastRow)
このソースにあるのでしたね。A2セルからE15セルの範囲を格納しています(checkLastRow変数には「15」が入ってる)。
今回の場合
For i = 1 To UBound(checkItemArray)
このソースのUBound(checkItemArray)が14という数字を保持していますので、実質は
For i = 1 To 14
こういう処理ということですね。
では、ループの中身を見ていきます。
'項目名を取得
itemName = checkItemArray(i, 1)
'セル番地を取得
cellAddress = checkItemArray(i, 2)
'セル番地から値を取得
itemValue = formSheet.Range(cellAddress).Value
3つの代入処理があるのですが、最初の2つは配列から値を取得しています。
エクセルの表形式をベースに説明すると
配列名(行番号,列番号)
という指定をします。
ですので、
'項目名を取得
itemName = checkItemArray(i, 1)
'セル番地を取得
cellAddress = checkItemArray(i, 2)
この2つの代入処理は・・・
この赤枠部分に対して行っています。
そして次の処理。
'セル番地から値を取得
itemValue = formSheet.Range(cellAddress).Value
この処理は入力フォームのシートでRange関数を使っています。Range関数に指定する値は先ほどの
'セル番地を取得
cellAddress = checkItemArray(i, 2)
この処理で取得したセル番地(cellAddress変数)の情報です。
はい、ここまではOK。次は入力チェックの処理には関係ないんだけど、ここまでの処理を確認いただく意味で、
'開発者用。値確認
Debug.Print "itemName:" & itemName & " cellAddress:" & cellAddress & " itemValue:" & itemValue
このようなソースを用意しています。ここまでの3つの変数について、変数名と実際に保持してる値を毎周出力します。
実行してみてください。
その結果・・・・
イミディエイトウィンドウにこのように表示されればOK。イミディエイトウィンドウは自動でクリアされないので、これまでのいろんな出力値がたまっていたら一度すべて削除するといいです。
イミディエイトウィンドウにカーソルを合わせてCtrl + A → BackSpace。
はい。ここまでOKですかね。
各項目の値を取得できたので、必須チェックの材料はそろいました。
次行きましょう。
必須チェックを実装する
今の実装の続きに以下のソースを下図のようにお願いします。
'必須チェック
If itemValue = "" Then
message = message & itemName & REQUIRED_TEXT
'message = message & getMessage(REQUIRED_MESSAGE, itemName)
Else
End If
必須チェックは何度も見てきましたね。If文では項目値に値があるかどうかを確認しています。
今回の上級編ではハードコーディングから脱却します。
「ハードコーディング」という言葉は1-1で
と、初めて出てきたのですが
message = "ふりがなが入力されていません。" & vbCrLf
このようにメッセージをソースコード中に直接べた書きしてしまうことをハードコーディングというのでした。
そもそもハードコーディングがなぜだめか?は今回のあとがきで改めて触れます。
これまでの実装で少しずつ動的なソースへとするために
message = message & itemName & "が入力されていません。" & vbCrLf
このように項目名を変数化したりもしましたが、今回いよいよ脱却します。
その方法が
message = message & itemName & REQUIRED_TEXT
これなんですね。変数 & 変数 & 定数という書き方でここに直接固定の値を指定していません。
で、そのすぐ下に
'message = message & getMessage(REQUIRED_MESSAGE, itemName)
こういうのあるじゃないですか。コメントアウトしてあるの。これはもう少し後で解説します。同様にメッセージを設定する処理であることは察しがつくと思いますが、説明の台本上もう少し後で開放します。
message = message & itemName & REQUIRED_TEXT
このソースの説明に戻るのですが、今回はソースの冒頭で定数も宣言しているんでしたよね。
'必須チェックテキスト
Const REQUIRED_TEXT = "が入力されていません。" & vbCrLf
これです。ここでREQUIRED_TEXTに設定している「"が入力されていません。" & vbCrLf」という値が
message = message & itemName & REQUIRED_TEXT
ここで使われています。ですので、砕いてしまうと結局実質は
message = message & itemName & "が入力されていません。" & vbCrLf
こういうことをしているんですが、このメッセージの固定値部分を定数宣言部に移動したのはコーディングのお作法として正しいといえます。
なぜならこれでハードコーディングをしていない状態になったから。
ただ、ハードコーディングの説明を
と言ってしまったため、ひょっとすると
と思った方もいるかもしれません。いえ、これはいいんです。詳しくはあとがきで。
正常メッセージの設定を実装する
次にこのループを抜けた後の処理を書きましょう。
以下のソースを下図のように実装してください。
If Len(message) = 0 Then
'正常メッセージの設定
message = "入力チェックを行いました。問題ありません。"
End If
MsgBox message
これはもう何度も見てきた処理です。
ただ、今実装いただいたソースはアンチパターンを取り入れてしまってますね。そう、先ほど触れたばかりのハードコーディングです。
これを解消しましょう。
前回実装した定数宣言の中に
'正常メッセージ
'Const NO_PROBLEM_MESSAGE = "入力チェックを行いました。問題ありません。"
こういうものがあります。このコメントアウトを開放してください。先頭のシングルクォーテーションを削除します。
この定数を使って、先ほどの正常メッセージのソースを以下のように修正しましょう。
'正常メッセージの設定
message = NO_PROBLEM_MESSAGE
ここで、定数名の重要性もアピールしたいですね。チェック結果に問題がなかった場合に設定するメッセージなので「NO_PROBLEM_MESSAGE」となっています。処理内容と一致しています。えらい。
今この段階で、必須項目にすべて値を入力して実行した場合、
このようになればOKです。
そして、必須項目に未入力のものがある場合は・・・
このようになればOK。
ここまでの実装で必須チェックはOKです。配列にため込んだ[チェック情報]シートの内容を基にきちんとチェックをしてくれています。
まだピンと来ていない方はソースをじっくり読み返してみてください。ここまでの説明と共に。
今、下図のグレーで塗った部分は実装できました。
次、いきましょう。
形式チェックを実装する
ここからは形式チェックの実装です。
ソースを見渡して、どこに実装するかを考えてみてください。
フローチャートで言うと「値があるか」の分岐部分でYesとなった先です。
1-2でも
このように伝えました。つまり、値があった場合に入るロジックに処理を記載します。
実はソースの側にもうお膳立てがしてあるんですよね。
はい。Elseの中身の処理が空っぽになっているのでした。ここに書いていきます。値があるときにのみ入るルートですね。
で、まず最初にやることが
形式チェックを行うかどうか?の分岐です。いきなり形式チェックをやるわけではないんです。
ここ、実はいきなり形式チェックをしても動作に問題はありません。
それだと余計に疑問が深まるのですが、答えとしてはより安全な実装にするためです。安全とはバグの可能性が低い、ということ。
実装を進めた先でもう一度触れます。
実装しましょう。
以下のソースを下図のように追記してください。
'正規表現を取得
itemRegExpPattern = checkItemArray(i, 3)
'形式を取得
itemFormat = checkItemArray(i, 4)
'データ型を取得
itemType = checkItemArray(i, 5)
'正規表現があるかどうかを検証
If Not itemRegExpPattern = "" Then
End If
解説します。
まず、変数への代入処理が3つ。これは先ほど必須チェックの時に
この2列分を代入する処理がありましたが、今度は
こちらの3列をそれぞれ代入しています。
'正規表現を取得
itemRegExpPattern = checkItemArray(i, 3)
'形式を取得
itemFormat = checkItemArray(i, 4)
'データ型を取得
itemType = checkItemArray(i, 5)
それぞれ、3列目、4列目、5列目にあるので、第二引数の指定値をみると処理が合致しますね。二次元配列目なので厳密にはX列目、というよりは2次元目のX番目の要素、ということになります。
そして、次に形式チェックを行うかどうか?の分岐処理を行います。
'正規表現があるかどうかを検証
If Not itemRegExpPattern = "" Then
End If
このソースですね。ここでワンクッション挟むことで形式チェックが必要な項目だけに形式チェックを行うことができます。
当たり前のことに思えますが、ソースコードに落とし込むとこのような実装になるんですね。
そして、上記のソースではこれ見よがしなまでに空行が連続しています。もうお分かりでしょう。その空いているところに形式チェックを実装します。
次のソースを下図のように実装してください。
'正規表現パターンの設定
regExp.Pattern = itemRegExpPattern
'形式チェック
If Not (regExp.Test(itemValue)) Then
message = message & itemName & "は" & itemFormat & "形式の" & itemType & FORMAT_TEXT
'message = message & getMessage(REQUIRED_MESSAGE, itemName, itemFormat, itemType)
End If
ひとつずつ解説していきます。
まずはこれ。
'正規表現パターンの設定
regExp.Pattern = itemRegExpPattern
正規表現オブジェクトのPattern変数に、先ほどの
'正規表現を取得
itemRegExpPattern = checkItemArray(i, 3)
この処理で取得した値を代入しています。
ここ↑ってやってることを上から追っていくと
B = A
C = B
ということをしているので、
B = A
C = A
という風にしても同じことなんですよね。じゃあなんで
'正規表現パターンの設定
regExp.Pattern = checkItemArray(i, 3)
こうしないで
'正規表現パターンの設定
regExp.Pattern = itemRegExpPattern
こうしているのか(わざわざ間に変数を介しているのか)、というと
checkItemArray(i, 3)
このような外部の情報に対してインデックス(要素にアクセスするための数字)を指定して取得する処理の記載は極力少ない方がいいからです。
ソース中に何度も
checkItemArray(i, 3)
これが登場した場合、例えば今のチェック表って
こうじゃないですか。ここにNoという管理番号の列を追加したら
今のソースだと配列を範囲取得するのはこの青枠の部分になるんですよ。そうなると二次元方向のインデックスで正規表現は3番目から4番目にずれることになりますよね(No列は追加しません。例示だけです)。
ってことは当然それに従って
checkItemArray(i, 3)
ここの3も4に変更しないといけなくなる。
そうなったとき、変更箇所はできるだけ少ない方がいいんです。変更漏れがありえますから。人間のやることにはケアレスミスや漏れの可能性が必ずついて回りますからね。
少しでもリスクを減らすために今回のような実装にします。
というのと、
'正規表現パターンの設定
regExp.Pattern = checkItemArray(i, 3)
と書いてあるよりも
'正規表現パターンの設定
regExp.Pattern = itemRegExpPattern
と書いてある方が、直観的にわかりやすいですよね。
「checkItemArray(i, 3)」という文字を初めて見た人はこれがなんなのかわかりません。でも「itemRegExpPattern」だったら正規表現パターンなのだろう、と推測することができます。
長々と説明しましたが、可読性、保守性に考慮した実装、ということです。
「保守」っていう言葉もまだあまり聞きなれないかもしれませんね。今回のあとがきでハードコーディングの件と併せて触れます。
あとすみません。。汗。ここまでダラダラと
checkItemArray(i, 3)
こういう書き方は少ない方がいいことを述べてきましたが、もっと根本的に解消するアプローチもあります。その方法を使えば上記のようなソースがたくさん出てきても問題ないんです。
それどころか、上記のようなソースが頻発する分、変数の数が減って却っていいかもしれない。この点は上級編でつくるもうひとつの入力チェックで解説します。キーワードは「マジックナンバー」です。
さて、次のソースの解説です。
'形式チェック
If Not (regExp.Test(itemValue)) Then
message = message & itemName & "は" & itemFormat & "形式の" & itemType & FORMAT_TEXT
'message = message & getMessage(REQUIRED_MESSAGE, itemName, itemFormat, itemType)
End If
まず、If文のところはもうこれまでにも出てきましたね。正規表現オブジェクトが項目値に対して正規表現と一致するか、その結果を返してくれています。
ただし、regExpのTest関数は一致したときにTrue(真)を返してくるので、それだけだと一致したときにエラー扱いになってしまいます。
それを回避するために
'形式チェック
If Not (regExp.Test(itemValue)) Then
条件式の前に「Not」を付けているんでしたね。結果を反転させます。これで形式チェックに一致しないときにエラーとすることができます。
先ほど
こんな風にお伝えしました。これを解説しましょう。
まず、形式チェックをするかどうかの分岐を挟まずにいきなり形式チェックをするソースには、簡単にできます。
この赤枠部分のように分岐しているソースをコメントアウトしてあげます。
で、ここまでの説明通りこれで実行しても動作は変わりませんと。
ただ、処理の流れとしては
このように正規表現が設定されていない郵便番号以外の項目についても形式チェックを実施することになります。これは言ってしまえば処理の無駄打ちです。無駄なことはしない方がいい。
そして、今回たまたまバグや異常終了(実行時にエラーポップアップがでてくるやつ)をせずに済んでいますが、それは
'形式チェック
If Not (regExp.Test(itemValue)) Then
ここで使っている正規表現オブジェクトのTest関数が
'正規表現パターンの設定
regExp.Pattern = itemRegExpPattern
事前に設定したPatternの内容がブランク(空文字のこと)の場合Trueを返す、という処理をしてくれているからです。
■1■これはTest関数がそういう風に実装されていた、ということですね。公式リファレンスが見つからなかったのですが、動作確認して確認しました。■1■
これがもしも異常終了で終わらせる処理になっていたら機能として非常に脆弱性が高いということになります。
ただ、この説明ではあまりピンとこない方もいるかもしれません。正規表現オブジェクトのTest関数は赤の他人が作ったもので、しかもそれはちゃんと動いている。
だったら、過度に「もしも」に備えた対策をしなくてもいいじゃないかと。
それは、その通りです。ただ、今回は正規表現オブジェクトのTest関数という、MicroSoft公式が提供しているものでしたが、これは当然自分で作成した関数が対象になることもこの先ありえます。
ですから、機能としてバグの可能性を限界まで排除した安全なソースを書くためのアプローチとして捉えてください。
では、残りのソースの解説です。
message = message & itemName & "は" & itemFormat & "形式の" & itemType & FORMAT_TEXT
'message = message & getMessage(REQUIRED_MESSAGE, itemName, itemFormat, itemType)
メッセージを設定しているソースですね。コメントアウトしているところは必須チェックのところ同様に後の説明で開放します。
今設定しているメッセージでは"は"と"形式の"だけ、ハードコーディングしているじゃないですか。これ、なんでだと思います?
答えは定数化するのがばかばかしかったからです。
メッセージって、動的に変わる変数や接続詞、固定文の組み合わせで構成されているじゃないですか。で、そのすべてを馬鹿正直に&で連結すると接続詞の分だけ定数を定義することになりますよね。
今回だったら
'メッセージに使用する接続詞'
Const HA = "は"
'形式チェックのエラーメッセージの一部
Const FORMAT = "形式は"
こんなものまで用意しなきゃいけなくなる。これはソースコードとして美しくないです。今回はエラーメッセージの種類が2つだけだけど、もっといろんなチェックをし、いろんなメッセージがある場合
'メッセージに使用する接続詞'
Const WO = "を"
'メッセージに使用する接続詞'
Const DE = "で"
'メッセージに使用する接続詞'
Const NI = "に"
'メッセージに使用する接続詞'
Const FROM = "から"
'メッセージに使用する接続詞'
Const MADE = "まで"
こんなに用意しなきゃいけない羽目になります。
というわけで、次の項でこうならないようにする術をお伝えします。
メッセージ取得関数を実装する
メッセ―ジを設定するにあたって、要素や言葉を連結して作るのは非効率的だ、という話をしました。
それではどうするか?というと既に定義してある定数がヒントになります。
以下の定数のコメントアウトを開放しましょう。
'必須チェックメッセージ
'Const REQUIRED_MESSAGE = "{0}が入力されていません。"
'形式チェックメッセージ
'Const FORMAT_MESSAGE = "{0}は{1}形式の{2}で入力してください。"
先頭のシングルクォーテーションを削除し、上図のようになればOK。
この2つを見ればわかると思いますが、言葉の連結ではなく、もともとあるメッセージのひな型に対して、変数によって動的に値が変わる部分を当てはめるという方法でメッセージを作ります。
それでは、End Subの後ろに次のソースを下図のように追加してください。
'メッセージのひな型とパラメータから組み立てたメッセージを返す関数
Function getMessage(message As String, ParamArray params() As Variant)
End Function
はい、2回目の自作関数です。関数名の上にコメントで書いている通り、メッセージのひな型とパラメータ(引数)からメッセージを組み立てて呼び出し元に返します。
引数の書き方は以前Module3に実装したcheckRequired関数でご説明した通り■3■
'指定された列の必須項目について
'値があるかをチェックし、エラーメッセージを返す
Function checkRequired(column As Integer)
このように
Function 関数名 ( 第一引数 , 第二引数・・・ )
と書くのですが、■3■今回はここにひとつ、新しい要素が登場しています。
Function getMessage(message As String, ParamArray params() As Variant)
ここの第二引数です。
ParamArray params() As Variant
先頭に「ParamArray」と付いてます。前回の1-6で
と説明しました。「ParamArray」は変数ではないんだけど、この引数は配列ですよ、という意味でついています。
そして変数名の「params()」。後ろに括弧がついてますね。配列のときは括弧をつけます。
どうして配列で受け取るのか?というのが気になると思いますが、2つのメッセージの定数を見比べてみましょう。
'必須チェックメッセージ
Const REQUIRED_MESSAGE = "{0}が入力されていません。"
'形式チェックメッセージ
Const FORMAT_MESSAGE = "{0}は{1}形式の{2}で入力してください。"
必須チェックではパラメータ(埋め込む変数)が1つで、形式チェックはパラメータが3つあります。
つまり、呼び出し元によっていくつの引数が必要になるかわからないんです。こういう時に関数側で引数の数を固定で書いてしまうと、関数側の定義と呼び出す側が渡す引数の数が異なるということでエラーが発生します。
引数の数が呼び出す側の事情によって変わる時は関数側も引数の数を可変で受け取れるように書きます。2つでもいいし4つでもいい、ということですね。
Function getMessage(message As String, ParamArray params() As Variant)
ひとつめはメッセージのひな型を受けとるための引数なので絶対に必要です。そして、2つ目の引数を配列で記載することでいくつ受け取ってもOKという形にできます。
今回の場合、2つ目以降の引数は自動的に
配列の1要素目 = 第二引数
配列の2要素目 = 第三引数
配列の3要素目 = 第四引数
という具合に関数は受け取ります。
それでは中の処理を実装していきましょう。以下のソースを下図のようにお願いします。
Dim returnMessage As String: returnMessage = message
For i = 0 To UBound(params)
returnMessage = Replace(returnMessage, "{" & i & "}", params(i))
Next
getMessage = returnMessage
はい、では見ていきます。
まず最初の1行。
Dim returnMessage As String: returnMessage = message
引数で受け取ったメッセージのひな型をreturnMessageという変数に代入しています。returnMessageは文字通り、最終的に呼び出し元に返す値を格納する変数です。
ここ、別に引数のmessageをそのまま処理の中で加工して呼び出し元に返してもいいんですけど、各変数の役割を明確にするために代入しています。
messageはあくまで引数として渡ってくるだけの役割で、呼び出し元に返すメッセージとして加工するのは別の変数がその役割を担うため、バトンパスさせています。まあ特に深い意味はないです。
では、次に控えているループ処理を見ていきます。
For i = 0 To UBound(params)
returnMessage = Replace(returnMessage, "{" & i & "}", params(i))
Next
はい、結構シンプルな処理が出てきました。まず最初の
For i = 0 To UBound(params)
これ。今回のソースですでに出てきている
For i = 1 To UBound(checkItemArray)
このループでも使用していますが、UBound関数は配列の要素数を返してくれるのでしたね。
なので、今回の実装だとここで設定される変数 i の到達する数字としては1だったり3だったりします。1=必須チェック、3=形式チェックね。
必須チェックのときは1つの引数を、形式チェックのときは3つの引数を受け取りますので。
注意してほしいのがループ変数 i の初期値は0となっています。これはループ内で配列に変数 i でアクセスするとき、配列の最初の要素は「0」の数字がふられているためです。
そして次がこの関数の要の処理です。
returnMessage = Replace(returnMessage, "{" & i & "}", params(i))
Replace(りぷれいす)関数の戻り値を変数returnMessageに代入しています。Replace関数はこれまでも出てきました。
入力チェック中級編までのソースで
'アスタリスクを削除
itemName = Replace(itemName, "*", "")
こんな風に使用していましたね。そう、この関数は第一引数の文字列中に含まれる第二引数の値を第三引数の値に置き換えます。
これを踏まえてもう一度今回の処理を見てみましょう。
returnMessage = Replace(returnMessage, "{" & i & "}", params(i))
カンマが引数の区切り目ですので、
第一引数:メッセージのひな型
第二引数:ループによって変わる置換元の文字列。{0}、{1}、{2}・・・
第三引数:配列形式で受け取った置換先の文字列。
ということになります。
今回の実装ではハードコーディングから脱却しよう、というのをひとつのテーマとして扱ってきました。となると、第二引数である
"{" & i & "}"
ここの部分、気になりませんか?ガッツリ直書きしてますよね「{」と「}」を。これはいいんです。
あとがきでも触れますが、ハードコーディングをやめたいのは実装後に変更する用がある場合にソースコードにできるだけ手を加えたくないからです。
つまり、これを裏返して解釈するとあえてハードコーディングしている部分は今後変更する可能性がない、ということになります。変更する可能性がないのだから、変更しやすい場所で管理する必要もありません。
そのままでいいんです。
そしてもうひとつ。第三引数の
params(i)
これです。前回1-6で
という話をしました。ここで使っている配列は一次元配列なので、インデックス(配列の要素にアクセスするための数字)を2つではなく1つだけの指定としています。
そして先ほども述べた通り、1番目の要素のインデックスは「0」となっていますので、初期値が0のループ変数 i をインデックスとして渡しています。
最初の要素に「0」が当たっているのは決まり事みたいなものです。■2■この決まりごとに合わせて、ソース冒頭の定数に設定したメッセージも
'必須チェックメッセージ
Const REQUIRED_MESSAGE = "{0}が入力されていません。" & vbCrLf
'形式チェックメッセージ
Const FORMAT_MESSAGE = "{0}は{1}形式の{2}で入力してください。" & vbCrLf
このように変数を動的に当てはめたい部分は「{0}」と0から開始しています。
配列のことで注意してほしいのが、この最初の要素(=箱)には「0」でアクセスするという点です。
これ、前回のあとがきでも出てきた絵ですが、1番目の箱には「0」でアクセスします。だから3番目の値が欲しい場合に指定する要素の添え字は「2」ということになります。
配列のX番目と指定するインデックスが異なる、という点は後続記事でいずれ出てくる連想配列にも関連します。今回は数字だから少々のややこしさを感じますが、連想配列はわかりやすいのでお楽しみに。
基本的には一次元配列でも二次元配列でも最初のインデックスが「0」になるのは決まり事として同じなのですがこれを言うと・・・
これ。
ってツッコまれると思うんですよ。うん。つっこむわなあ、そら。
今回使ってる二次元配列ってどうやって作ったかというとエクセルのワークシートの情報を範囲指定してコピーして作ったじゃないですか。
'チェック情報を配列に保持
checkItemArray = checkSheet.Range("A2:E" & checkLastRow)
これね。この場合は例外で1から始まります。
まあこれはエクセル上だけの話なので、基本は1次元配列であれ二次元配列であれ最初の要素のインデックス(要素にアクセスするための添え字)は「0」という点だけ抑えておいてください。
■2■
そして最後の
getMessage = returnMessage
この処理は関数名に値を代入するような書き方をすることで、呼び出し元に値を返す、という意味になるのでしたね。
ここまででこの関数の解説は終了です。この処理の面白いところはループのたびにメッセージを完成形に向けて育てていくところなんですよね。
郵便番号の形式チェックを例に説明したい・・・のですが、もうソース全体を完成形までもっていきましょう。その方がわかりやすいので。
動作確認の準備をしよう
以下の変更を実施してください。
まず、定数に改行文字を連結します。今、
この赤枠部分。すみません・・・私が書くの忘れてしまいました。。それぞれ改行文字を追記してください。
このようにできればOK。
次、今改行文字を追加いただいたすぐ次のソース、コメントアウトしてありますよね。ここ、説明するタイミングを逸してしまいました・・汗。今やりましょう。コメントアウトを解除してください。
これを・・・
こうできたらOK。で、ここを開放したということは、その少し後ろにある
このソースを・・・
このように置き換えましょう。■5■「FORM」と「CHECK」が違うだけなので、紛らわしいね。ふぁーいとっ(∩´∀`)∩。■5■
次にgetMessage関数呼び出しのソースを開放します。
まずは、必須チェック。これを・・・
このようにできればOK。コメントアウトを入れ替えます。
そしてもうひとつ。形式チェックの方も同様にします。
これを・・・
このようにするんだけど・・・赤線のところ見て。。。汗。形式チェックなのに必須チェックの定数を設定してしまってる・・ごめんなさい。。
「REQUIRED_MESSAGE」を「FORMAT_MESSAGE」に変更してください。
これでOK。
それから、少し前の説明で、
ここ、コメントアウトしたじゃないですか。もしもコメントアウトしたままになっている方は解除してください。
これでOK。
そして最後に、次の動作確認の布石として次に示す2つを実施ください。
この開発者用として用意していたデバッグコード、これを
コメントアウトしてください。これがあるとgetMessage関数の確認をイミディエイトウィンドウでするとき、見にくいので。。
そして、次に以下のソースを下図のように実装ください。
Debug.Print returnMessage
2か所です。
はい、ソースコードに対して手を動かしていただくのはここまでです。
ここまでの仕上げで不要になった定数とかコメントアウトしたままのソースがあるじゃないですか。
今までやれ美しいソースだやれコーディングのお作法だって言ってきたのが一気に胡散臭くなるかもしれませんが、残しておくか削除するか、どちらでもいいです。
まあ削除するならそれはそれで不要な記載のない綺麗なソースになるのですが、今は学習段階です。残して置いたらそれはそれで学習の軌跡として意味がありますので。
■4■
ここまで改行文字の入れ忘れを補完させられたり、コメントアウトを開放させられたり、入力ミスを修正させられたり、面倒でしたよね。ええ、すみません。今土下座しながらキーボード打ってるので、許してください。テヘ。
■4■
はい、ではいよいよ元々の目的である動作確認をしましょう。
いざ、動作確認!
それでは動作確認です。全体の動作確認は最後にサクッとやるとして、まずはgetMessage関数のところを見てみましょう。先ほど
と、述べましたんでね(ニヤリ
それでは入力フォームの方を以下のようにして実行してみます。
※キャプチャ外の項目はすべて適切に入力しています。
では、入力チェックボタンを押下してみましょう。すると・・・
はい、想定通りの動作をしてくれています。
そして、イミディエイトウィンドウを見てみましょう。
はい、赤枠が必須チェック、青枠が形式チェックです。
それぞれまず最初は第一引数で受け取ったままのメッセージになっていますね。ユーザに対してそのまま出せない状態です。
これをループの1週目で「{0}」を配列の最初の要素と置き換えます。必須チェックに関してはそれでもうループは終わりです。
形式チェックの方は2周目、3週目と文字を置き換えていますね。これで出来上がったメッセージを呼び出し元に返す(戻す)というわけです。
では全体的な動作確認もしておきましょう。
まずはすべて正常な入力がされている場合。
はい、いいですね。ソース的にはこのメッセージは
'正常メッセージの設定
message = NO_PROBLEM_MESSAGE
こんな風に設定していますから、ちゃんと冒頭で定義した定数が代入されている証拠です。
次に全項目、エラーのパターンです。
スペースの都合で、エラーのポップアップを少し右にずらしています。これまでの実装と異なるのは、エラーメッセージ中の項目名はこの入力フォームにある項目名ではなく、
こちらの[チェック情報]シートのA列から取得した内容である、という点です。
そして、形式チェックは対象項目に入力がある時にだけ働くチェックだ、という点もポイントでした。
ご覧ください。郵便番号にエラーパターンを入力すると形式チェックのエラーメッセージになってますね。先ほどのすべて未入力の実行では
こちらの通り、必須チェックのエラーメッセージだけでした。ちゃんと実装通りに動いています。
そしてもうひとつ、今回のチェック情報を別シートで管理する方法の利点を体感する動作確認もしてみましょう。
携帯電話の番号にも形式チェックを設けてみます。編集するのは[チェック情報]シートです。
このように正規表現に「^\d{3}-\d{4}-\d{4}$」を、形式に「XXX-XXXX-XXXX」をデータ型に「数値」を設定してください。
一度入力フォームの状態をエラーなしの状態に戻します。
これで実行してみると・・・
はい、正常動作ですね。携帯番号の値も正しい。
次にハイフンなしの11桁の数値にしてみます。すると・・・
OK。エラーメッセージでました。チェック内容もエラーメッセージも先ほど[チェック情報]シートに記載した内容を見事に吸い上げてくれています。
さらに、必須チェックの対象を増やしてみましょう。
[チェック情報]シートの8行目を選択し、行を挿入します。上図のように行番号を右クリックし、メニューから「挿入」を選択してください。
このようになるので、
このように設定したい内容を入力します。
これでですね、入力フォームの卒園名をブランク(空文字)にして実行してみましょう。すると・・・
はい、そうですね。必須チェックの対象になったわけですからエラーとして検出されました(携帯番号は正しい値に戻しました)。
ここがね、次回への布石になってるんですよ。上図をよく見てください。なにか違和感ありませんか?この卒園名の必須チェックだけ。
はい、そうです。「卒園名」の項目名にはアスタリスクがついていません。
今回のように[チェック情報]シートでチェック内容を管理する方法にしたことで、入力フォーム上で必須であることを示すアスタリスクのマークと必須チェックの対象項目がリンクしなくなってしまいました。
中級編のループでやったときは項目名についているアスタリスクを頼りに必須チェックの対象かどうかを判断していましたが、外部でチェック情報を管理することにした弊害としてこのようなことが起きました。
もちろん、これは使い手がただ気を付ければいいだけの問題です。今回のシナリオで言うなら、先生であるあなたが保護者の皆さんに送信する前にきちんと確認すればいいだけの話です。
ただ、システムづくりの思想としてはこれはユーザに負荷を強いるわけですから、決していい実装とは言えません。
この連載ではただシステムを作れるようになりさえすればいい、というわけではなく美しいソースを書くコツや、コーディングのお作法、拡張性・保守性・可読性を考慮したソース、システムづくりの考え方等も一緒に伝えられたらなと思っています。
その意味では今回の入力チェックは本当にいい題材です。ちょっと自画自賛感でてますが、「開発」に付随するアレコレも一緒に吸収していってください。
これで上級編の入力チェックその1は完了です。今回実装いただいた入力チェックは完成度としてもそれなりに高いです。
残りもうひとつ作るということはまだ詰めが甘いところはあるのですが、それでも実際に使ってみてもいい程度には完成度が高いです。
次回、完成形を作成するので乞うご期待!
あとがき・ふりかえり
お疲れさまでした。以下の4つについてお伝えします。
・フローチャートの補足
・ハードコーディングと保守の話
・宣言できない変数・定数名について
・初心者特有のもやもやを感じてませんか
-- -- -- -- -- -- -- -- -- --
・フローチャートの補足
今回はじめてフローチャートというものが登場しましたね。
これ。実は今回、実装の助けとなるようにわかりやすさ重視のフローチャートを用意しました。だから実際はちょっと内容が異なるんですよ。そこを説明させてください。
この赤枠で囲った部分、他のフロー(処理の箱たち)と比較してなにか感じませんか。
これは処理の流れというよりも実装の流れですよね。本当だったら、
「入力チェックの内容を取得する」
というひとつの箱だけでループに突入してもいいようなものです。それを今回は
・定数を宣言する
・変数を宣言する
・オブジェクトを実体化する
・変数に初期値を設定する
と、ご丁寧に書いています。くどいようですが、実装に寄り添う意図で用意しました。
フローチャートは書き手やプロジェクトの方針によって記載の粒度が結構違ってきます。今回の上記箇条書き4つほど細かいものはそうそうないですが、まあそういうものがあるんだな、程度に抑えておいてください。
今後も必要に応じてフローチャートは用意しますね。
-- -- -- -- -- -- -- -- -- --
・ハードコーディングと保守の話
今回の実装ではハードコーディングを解消しました。
ハードコーディングとはなんなのか?どうして避ける必要があるのか?を詳細に説明しますね。
ハードコーディングとは、ソースコード中に記載すべきではない情報を直接ソースコード中に埋め込んで(書いて)しまうことを言うのでした。
ではソースコード中に記載すべきではない情報というと、
・あとで変更する可能性のある情報
・ソース中に複数回にわたって登場する同一情報
が該当します。
ハードコーディングを説明するときはよく消費税が例としてあげられます。精算系のシステムはソースコード中に消費税を含む計算が複数回登場することが多く、かつ消費税は世相によって変化する可能性があるためです。
消費税のほかにはユーザに通知するメッセージやファイル・フォルダのパスもソースコード中に直接書かず外部で管理することが多いです。この辺りは今後の実装でおそらく登場しますので、またそのときに改めて。
ハードコーディングの値が数字の場合はそれをマジックナンバーと言います。実装した本人以外はどういう意味の数字なのかわからない、しかし処理を正常に動かしてくれる魔法の数字、というような由来です。
これもアンチパターンですので、次回以降解消します。
今回はメッセージを定数化してソースの冒頭で定義する、という形でハードコーディングを回避しましたが、他にはデータベースや外部ファイルで管理する方法もあります。ここからは保守の話ですね。
まず保守という言葉自体はIT業界に限らず一般にもあるのですが、ここではシステムの保守として説明します。
システムの保守というのは一度完成して運用が始まったシステムに対して不具合が出たときに修正したり、使い手から新たな要望が発生したときにシステムに手を加えたりして対応・適応することです。
これまで実装してきていただいているVBAはソースを修正したらその場ですぐに修正内容を動作確認できるのですが、世の中のシステムにはそう簡単にいかないものもあります。
なにかソースを修正する用があったとき、修正内容を今動いているシステムに適用するまでに
1.ソースを修正する
2.コンパイル済みのファイルに変換する
3.サーバを停止する
4.コンパイル済ファイルを修正前のファイルと置き換える
5.サーバを再起動する
という手順を踏まなければならないケースがあります(「サーバ」については後続記事で説明します)。ただ、修正する対象をソースの外部で管理できている場合、
1.外部ファイルを修正する
の1手順だけで修正が済みます。こういう作業の手間やリスクも勘案した上でハードコーディングは実装のアンチパターンとして広く周知されているんですね。
「リスク」というのは一度動き出したシステムはあまりサーバを止めたりファイルを入れ替えたりしたくないものなんですよ。そこで作業者の操作ミスとかがあったら大変なことになるから。
外部ファイルを修正するだけで済むということは当然ソースコードには外部ファイルから値を取得するように実装されている、ということになります。
この辺りの実装方法はPHPの章に入ってから触れていきますね。
ちなみに、先ほど保守の際にサーバの再起動が必要になるケースがある、という説明をしましたが、PHPは再起動が不要な言語ですので、そこはご安心ください。これもまた初心者がとっつきやすいといわれる所以(∩´∀`)∩。
-- -- -- -- -- -- -- -- -- --
・宣言できない変数・定数名について
本文中に例示として使った定数宣言に
'メッセージに使用する接続詞'
Const FROM = "から"
'メッセージに使用する接続詞'
Const MADE = "まで"
こういうのがありました。実装はしてもらってないからピンとこないかもしれないけど、ありましたね。なんでもかんでも定数化したら定数が増えて大変だぞって説明の中に。
で、このときにたまたま「あ、これ説明しておこう」ってなったんですが、上記の定数名、ちょっとおかしくありません?
「から」の定数名がFROMなのはわかるけど、そうであるなら「まで」の定数名はTOであるべき。FROMとTOはセットですから。でもTOとせずにMADEとしてるんですよね、とてもださいです(ノД`)・゜・。
これ、実はTOという名前の定数・変数は定義できない決まりなんですよ。
それはなぜか。ちょっと考えてみてほしい。勘のいい人なら考えたら答えを導き出せるから!
For i = 1 To UBound(checkItemArray)
これね、入力チェックをするためにループを開始しているところですけど、ここに「To」って使っているじゃないですか。ループ処理の書き方の決まりとして。
このようなプログラミング言語側で予め「こっちで使いまっせ」と予約されている言葉を予約語といって、変数や定数名として実装者が使えないことになっています。
これはVBAに限らずいろんなプログラミング言語でそういうルールになっています。
ですから、ForやNext、Setなんかも変数・定数名として定義することはできません。定義しようとするとどうなるかというと・・・
はい、このようなエラーになります。
気を付けましょうね。
-- -- -- -- -- -- -- -- -- --
・初心者特有のもやもやを感じてませんか
私自身、プログラミング初心者の頃は各種説明を聞き・読み解き、咀嚼しながら学習を進めつつも、なかなか自分の中にすんなりと落ちてこない、もやもやした感覚があったんですよね。
もしもあなたもそういうのを感じているとしたら、その気持ちをケアしたいと思います。
ここまでの入力チェック機能の実装で
だとか
とかってお伝えしてきたのですが、なんかこう、反論したい気持ちありませんかね。
という具合に。
他にも、ハードコーディングに関する詳細な説明や変数・定数の命名に関する注意、可読性に長けたソースコードを実装する心がけ等をクドクドとお伝えしてきています。
IT業界にはシステムのライフサイクルという言葉がありまして、システムの誕生からその後の流れを表現したものです。そのライフサイクルは
・企画プロセス
・要件定義プロセス
・開発プロセス
・運用プロセス
・保守プロセス
の5つで構成されます。
で、この連載を通してあなたにお伝えするのは間違いなく開発プロセスの話なのですが、観点としては結構保守プロセスの話も入ってくるんですよね。ハードコーディングを避ける意図しかり。
開発プロセスからしたら保守プロセスって結構先の話です。
というのが、もやもやの正体ではないかと思うのです。ソースコードはぐちゃぐちゃでもいいからとにかく開発できさえすればいい、ということでしたらもっとサクサク進みますから。
ただこれはしょうがないことと言いますか、あなたたちがプログラミングを使って目指す方向性は様々だと思いますが、現場のエンジニアがよく苦労したりストレスを感じたりするのはこの保守の部分なんですよね。
実装のクオリティが低いと保守のときに苦労させられる羽目になります。教える側の人間はそれを知っているから、どうしても保守の観点からの説明も多くなってしまうんです。
ここはまあ、勘弁してください、という感じですよね。
「身長」項目や「No」列のように、システムを作る時に「こういう使い方をしたらバグにならないか?」といちいち「もしも」を気にするのは、なかなかにストレスかもしれません。
ですが、この姿勢はこの世界では正しいです。この観点に違和感やストレスを抱く方は脳ミソのこれまで使ってこなかった領域を解放する作業だと思ってついてきてください。
「もしも~~だったらどうなるのか?」という視点は私生活においてもリスク管理能力として活きてきますので。
実装時というよりはシステムの仕様を考えるときに重要になりますね、この観点は。
そう、これまでは入力チェックの機能を実装しながら同時に仕様を伝えてきましたが、本当は使い方や仕様が確定した上で実装に入るんです。
・・・という部分を次回への導入として、今回は終わりにしましょう。
私も気を付けてはいるのですが、保守のステップや複数人での開発を前提とした説明になってしまっている箇所がこの先も出てくると思います。
その際は頭に浮かぶ疑問符は少なめにしてよしなに解釈してもらえると助かります。
おわりに
終わりです!入力チェックは次に作るものが最後です。
プログラミングの基礎を学ぶ材料として入力チェック機能を選択しましたが、これまで知らなかった新しい知識がドドドっと流れ込んでくるのはそれなりに大変ではなかったかなと、推察します。
ここまで続けてこれたご自身を褒めてあげてください。すごい!
入力チェックの章が終わるとその先はプログラミングでできることがだんだん増えていきますから、驚きとともに楽しさも増していくことと思います。
引き続き、よろしくお願いしますね。
今回は長くなりました。甘いもの食べてゆっくり休んでから次に進んでください。
ありがとうございました。
※ちょっと心身が不調でして、次の更新は未定です。すみません。
2024/1/11追記
すみません。ここには帰ってこれませんでした。この連載はここで終了します。続きはこちらに譲ります。
この記事が気に入ったらサポートをしてみませんか?