クラシックASPのコーディング考慮点
いまさらだが、クラシックASPのシステムに携わることになったので、ASPのコーディングについて整理してみる。
言語
VBScriptとJScriptがあるが、VBScriptを使うのでVBScriptについて。 ブラウザ側はjavascriptなのでJScriptを使ったほうがいいのかもしれない。 後日まとめようと思う。
気をつけること
エラーハンドリングが貧弱
VBScriptでは、エラーハンドリングなしとOn Error Resume Nextの2つしかない。
資源開放が必要な操作については、On Error Resume Nextを使うしかないのだが、 エラーの発生箇所を調べるようとすると1行ごとにチェックを入れる破目になることがある。
エラーが発生した行などの情報を取得できればいいが使えそうな手段がないようである。 エラー箇所特定の常套手段としては、エラーハンドリングなしにする。 そうすればASP側のエラー出力をブラウザ側で見ることができる。
ただ、エラーハンドリングなしにするには、事前にOn Error Resume Nextの使用箇所を考えておく。 最小限の利用で済むようにしておく。
特に資源開放が必要なものについては、必ず終了処理が実行されるようにする。 データベースのコネクション、結果セットやファイルアクセスが該当する。
グローバル変数の多用
コレクションが組み込まれてないので、画面に表示する項目一つ一つに変数を割り当ててしまいがちになる。 一応、クラス機能があるので画面で使うものを定義しておくことができるが、 IDEのサポートがないためクラス定義してもあまり便利でない。
VBScriptにはコレクションは組み込まれてないが、ScriptランタイムのDictionaryオブジェクトがあるので連想配列のように使える。
Server.CreateObject("Scripting.Dictionary") で生成できる。
サンプル
実際にコードを書いて検討してみる。
サンプル1
sample1.asp
<%@ LANGUAGE=VBScript %> <% Option Explicit %> <!-- #include file="config/settings.inc" --> <%'環境依存定義のInclude%> <!-- #include file="../common/inc/common.inc" --> <%'共通の定数と関数のInclude%> <% On Error Resume Next 'グローバル変数 Dim gErrInfo, gData Dim gDbCon 'DB Connecgtion '=== エントリー部 === Set gErrInfo = CreateCollection() 'Scripting.Dictionary Set gData = CreateCollection() Set gDbCon = Nothing InitializePage If Err.Number <> 0 Then gErrInfo.Add gErrInfo.Count, CreateErrorInfo("ERR-0001", "sample.asp", "初期化に失敗しました。", "0010 Initialize Error.") End If If gErrInfo.Count = 0 Then LoadPage End If TerminatePage '=== プロシージャ、ファンクションの定義 === Sub InitializePage() End Sub Sub LoadPage() gData("sample_value") = "サンプル値" End Sub Sub TerminatePage() If Err.Number <> 0 Then gErrInfo.Add "ERR-9990", "sample.asp", "エラーが発生しました。", "9990 No Handling Error." End If On Error Resume Next If Not gDbCon Is Nothing Then gDbCon.Close Set gDbCon = Nothing End If On Error GoTo 0 End Sub Function CreateErrorInfo(sErrCd, sSource, sErrMsg, sDetailMsg) Dim info Set info = CreateCollection() info.Add "Timestamp", Now() info.Add "ErrCode", sErrCd info.Add "Source", sSource info.Add "Message", sErrMsg info.Add "DetailMessage", sDetailMsg info.Add "Err.Number", Err.Number info.Add "Err.Source", Err.Source info.Add "Err.Description", Err.Description Err.Clear Set CreateErrorInfo = info End Function %> <html> <head> </head> <body> <!-- コンテンツ --> </body> </html>
ポイント
- グローバル変数を少なくする
コレクションを使って、画面に出力するデータを集約してみた。 そうすることによって、グローバル変数を少なくすることができる。
- エラー情報生成関数の定義
上記例では、CreateErrorInfo()という関数を定義して、エラー発生時の情報を集めている。 また、実行時エラーのエラーハンドリングを行った場合、Errオブジェクトから必要な情報を取得してからErr.Clearとしている。そうすることで、後続処理でErr.Numberを見るだけでエラーハンドリングしているかどうかがわかる。
- 初期処理、メイン処理、終了処理の関数化
上記サンプルでは、InitializePage、LoadPage、TerminatePageの3つの関数とした。
初期化処理、メイン処理、終了処理の3つの構成にすることで、プログラムの見通しをよくし、TerminaitePageで資源開放の処理を行っている。
問題点
- 共通側でのハンドリングが難しい
共通側で認証チェックなどの共通的な処理が必要な場合、個々のaspに修正が発生してしまう。 事前処理だけであれば、common.incで処理して別ページへのRedirectなどの手段が使え、Response.Endでスクリプトの処理を停止することは可能。
- htmlの出力時に一時的な変数の扱い
ループで使用するカウンタや局所的に出力内容を編集したい場合、グローバルスコープで変数を宣言する必要がある。
- その他
エラー情報を単純にコレクションとしているが、やはり拡張性や各aspでの利用方法の標準化を考えるとクラス化したほうがよいだろう。
サンプル2
サンプル1の問題点を改善してみる。
common.incの抜粋
' エラー情報 Class ErrorInfo Private mList Private mIsNoError Private Sub Class_Initialize() Set mList = CreateCollection() mIsNoError = False End Sub Function AddErr(sErrCd, sSource, sErrMsg, sDetailMsg) Dim info Set info = CreateCollection() info.Add "Timestamp", Now() info.Add "ErrCode", sErrCd info.Add "Source", sSource info.Add "Message", sErrMsg info.Add "DetailMessage", sDetailMsg info.Add "Err.Number", Err.Number info.Add "Err.Source", Err.Source info.Add "Err.Description", Err.Description Err.Clear mIsNoError = True Set AddErr = info End Function Public Default Property Get Items(i) Set Items = mList(i) End Property Public Property Get Count Count = mList.Count End Property Public Property Get IsNoError() IsNoError = (Not mIsError) End Property Public Function ToString() '省略 End Function End Class ' ページコントローラ Class PageController Private mIsRedirectReuqested Private mIsTransferReuqested Private mNextPageUrl Private Sub Class_Initialize() mIsRedirectReuqested = False mIsTransferReuqested = False mNextPageUrl = "" End Sub Public Property Get IsRedirectReuqested IsRedirectReuqested = mIsRedirectReuqested End Property Public Property Get IsTransferReuqested IsTransferReuqested = mIsTransferReuqested End Property Public Property Get NextPageUrl NextPageUrl = mNextPageUrl End Property Public Sub SetRedirect(sNextPageUrl) mIsRedirectReuqested = True mNextPageUrl = sNextPageUrl End Sub Public Sub SetTransfer(sNextPageUrl) mIsTransferReuqested = True mNextPageUrl = sNextPageUrl End Sub Public Function MovePageIfRequired() If Me.IsRedirectReuqested And Me.NextPageUrl <> "" Then Response.Clear Response.Redirect Me.NextPageUrl MovePageIfRequired = True Exit Function End If If Me.IsTransferReuqested And Me.NextPageUrl <> "" Then Response.Clear Server.Transfer Me.NextPageUrl MovePageIfRequired = True Exit Function End If End Function End Class
common_page_template.inc
<% Dim gNoErrorHandlingForDebug 'デバッグ用・エラーハンドリングなし設定 Dim gErrInfo 'エラー情報 Dim gPageCntr 'ページコントローラ Dim gData '画面データ Dim gCnn 'DBコネクション ' このTemplateで共通的に使うGlobal変数の初期化 gNoErrorHandlingForDebug = False Set gErrInfo = New ErrorInfo Set gPageCntr = New PageController Set gData = CreateCollection() Set gCnn = Nothing ' ページのメイン処理 Sub PageMain() If Not gNoErrorHandlingForDebug Then On Error Resume Next End If GetSourceName 'check implementation InitializePage If Err.Number <> 0 Then gErrInfo.Add "ERR-0001", GetSourceName(), "初期化でエラーが発生しました。", "" End If If gPageCntr.MovePageIfRequired() Then Exit Sub End If If gErrInfo.Count = 0 Then LoadPage End If TerminatePage If gPageCntr.MovePageIfRequired() Then Exit Sub End If If Err.Number <> 0 Then gErrInfo.Add "ERR-9991", GetSourceName(), "エラーが発生しました。", "9991 No Handling Error." End If RenderHtml End Sub 'このTemplateの初期処理 Sub TemplateInitializePage() End Sub 'このTemplateの終了処理 Sub TemplateTerminatePage() If Err.Number <> 0 Then gErrInfo.Add "ERR-9990", GetSourceName(), "エラーが発生しました。", "9990 No Handling Error." End If On Error Resume Next If Not gCnn Is Nothing Then gCnn.Close Set gCnn = Nothing End If On Error GoTo 0 End Sub '=== Page側実装関数 === Function GetSourceName() Err.Raise vbObjectError + 1, "common_page_template.inc", "GetSourceName()はページ側で定義する必要があります。" 'GetSourceName = "common_page_template.inc" End Function Sub InitializePage() Err.Raise vbObjectError + 1, "common_page_template.inc", "InitializePage()はページ側で定義する必要があります。" 'TemplateInitializePage End Sub Sub LoadPage() Err.Raise vbObjectError + 1, "common_page_template.inc", "LoadPage()はページ側で定義する必要があります。" End Sub Sub TerminatePage() Err.Raise vbObjectError + 1, "common_page_template.inc", "TerminatePage()はページ側で定義する必要があります。" 'TemplateTerminatePage End Sub Sub RenderHtml() Err.Raise vbObjectError + 1, "common_page_template.inc", "RenderHtml()はページ側で定義する必要があります。" End Sub %>
sample02.asp
<%@ LANGUAGE=VBScript %> <% Option Explicit %> <!-- #include file="config/settings.inc" --> <!-- #include file="../common/inc/common.inc" --> <!-- #include file="../common/inc/common_page_template.inc" --> <% ' デバッグ用・エラーハンドリングなし設定 'gNoErrorHandlingForDebug = True 'コメントを外すとエラーハンドリングしない。 If Not gNoErrorHandlingForDebug Then On Error Resume Next End If 'グローバル変数 '=== エントリー部 === Call PageMain() '=== プロシージャ、ファンクションの定義 === Function GetSourceName GetSourceName = "sample02.asp" End Function Sub InitializePage() TemplateInitializePage End Sub Sub LoadPage() gData("sample_value") = "サンプル値" End Sub Sub TerminatePage() TemplateTerminatePage End Sub Sub RenderHtml() If Not gNoErrorHandlingForDebug Then On Error Resume Next End If %> <html> <head> </head> <body> <!-- コンテンツ --> </body> </html> <% End Sub 'RenderHtml %>
ポイント
- htmlの出力を関数化
上記では、RenderHtmlでhtmlの出力を行っている。 これのメリットは、出力内容を編集する際にグローバル変数を多用しなくて済むこと。 リストのループカウンタや簡単な文字列編集等で一時的な変数を使用したい場面でローカル変数を使うことができる。
aspをhtmlのテンプレートとして見てしまうと、少しわかりづらいかもしれないが変数を局所化するメリットのほうを選択したい。
- 処理のテンプレート化
common_page_template.incにてメインの処理をテンプレート化すると、共通的な処理を組み込みしやすい。 例えば、テンプレート側で認証のチェックを行って、認証されてなければログイン画面に遷移させることも比較的簡単にできる。
ここで1つ注意したい点がある。 同じ名前の関数を定義している点。 vbscriptでは、同一名の関数を定義してもエラーが発生しない。 重複している場合、後から宣言した関数が有効となる。 あまり多用するのもどうかと思うが、ページ側で実装すべき関数を明示する意味で使っている。
- その他
今回は、基本的なエラー処理とページ遷移処理をクラス化してみた。 そのほか考慮すべき点はいろいろあると思うが、少なくとも入力値の扱い方やデータの表示の仕方についてだけは検討すべきだろう。別途取り上げることにする。