クラシックASPのコーディング考慮点

いまさらだが、クラシックASPのシステムに携わることになったので、ASPのコーディングについて整理してみる。

言語

VBScriptJScriptがあるが、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では、同一名の関数を定義してもエラーが発生しない。 重複している場合、後から宣言した関数が有効となる。 あまり多用するのもどうかと思うが、ページ側で実装すべき関数を明示する意味で使っている。

  • その他

今回は、基本的なエラー処理とページ遷移処理をクラス化してみた。 そのほか考慮すべき点はいろいろあると思うが、少なくとも入力値の扱い方やデータの表示の仕方についてだけは検討すべきだろう。別途取り上げることにする。