「DataTable.Compute」という甘い罠を捨てろ:堅牢な数式エンジンを自作するアーキテクチャ設計論
業務アプリケーションの開発において、我々は常に「手軽さ」という誘惑に直面する。
例えば、ユーザーが入力した「(Price * Tax) + Shipping」といった動的な計算式を処理したいとき、多くの開発者は `DataTable.Compute` メソッドを呼び出す。これは一見、非常に効率的で賢明な選択に見えるだろう。なぜなら、追加のコードを書く必要がなく、標準機能だけで完結するからだ。
しかし、プロフェッショナルなアーキテクトの視点から言わせてもらえば、それは設計の放棄である。
本稿では、なぜ `DataTable.Compute` がプロダクション環境において「技術的負債」となり得るのかを論理的に解明し、その限界を突破するための「独自の数式解析エンジン(パーサー)」をVB.NETで構築する極限の設計手法を伝授する。
1. なぜ `DataTable.Compute` では不十分なのか
まず、君たちが直面している問題の正体を明確にしよう。`DataTable.Compute` には、致命的な3つの弱点がある。
① セキュリティリスク:式インジェクションの脅威
`DataTable.Compute` は、渡された文字列をそのまま式として解釈する。もし、ユーザー入力が適切にバリデーションされずにこのメソッドに渡された場合、意図しない計算ロジックの実行や、アプリケーションのクラッシュを招く「式インジェクション」の脆弱性となり得る。ブラックボックス化された評価エンジンは、制御不能な挙動の温床だ。
② パフォーマンスの限界:オーバーヘッドの蓄積
`DataTable.Compute` は、内部的に `DataRow` のコンテキストを介して動的に型を解決し、評価を行う。数千、数万件のレコードに対してループ内でこれを呼び出す設計は、CPUサイクルを無駄に浪費し、UIスレッドを凍結させる原因となる。
③ 拡張性の欠如:独自の関数が書けない
「社内の特殊な計算ロジック(例:営業日計算、特定の税率ロジック)」を組み込みたいと思った瞬間、`DataTable.Compute` は行き詰まる。標準的な算術演算以外のロジックを注入するには、結局、外側で複雑な条件分岐を書くことになり、コードの可読性は著しく低下する。
2. 目指すべきアーキテクチャ:再帰下降構文解析 (Recursive Descent Parsing)
我々が構築すべきは、単なる「文字列置換」ではない。字句解析 (Lexer) と 構文解析 (Parser) を分離した、堅牢な計算エンジンである。
設計のステップは以下の通りだ:
1. Tokenization (字句解析): 文字列を「数値」「演算子」「変数」「括弧」といった意味のある最小単位(Token)に分解する。
2. Parsing & Evaluation (構文解析と評価): 演算子の優先順位(乗算は加算より先など)を考慮しながら、トークン列を解析し、計算結果を導き出す。
この手法を採用することで、計算式の「どこが間違っているか」を正確に特定でき、かつ独自の関数を容易に追加できる構造が手に入る。
3. 実装:プロダクション・グレードの数式エンジン
以下に、外部ライブラリを一切使わず、変数(パラメータ)の注入にも対応した、堅牢な数式パーサーの完全な実装を示す。これをそのままプロジェクトのコアモジュールとして組み込めるレベルまで仕上げている。
''' <summary>
''' 堅牢な数式計算エンジン
''' </summary>
Public Class FormulaEngine
' トークンの種類を定義
Private Enum TokenType
Number
Plus
Minus
Multiply
Divide
LeftParenthesis
RightParenthesis
Variable
EndOfFile
End Enum
' トークン構造体
Private Structure Token
Public Property Type As TokenType
Public Property Value As String
Public Property NumberValue As Double
End Structure
Private _tokens As New List(Of Token)
Private _position As Integer
Private _variables As Dictionary(Of String, Double)
''' <summary>
''' 式を評価する
''' </summary>
''' <param name="expression">計算式 (例: "(price * 1.1) + tax")</param>
''' <param name="variables">使用する変数辞書</param>
''' <returns>計算結果</returns>
Public Function Evaluate(expression As String, variables As Dictionary(Of String, Double)) As Double
_variables = variables
_tokens.Clear()
_position = 0
' 1. 字句解析 (Lexer)
Tokenize(expression)
' 2. 構文解析と評価 (Parser)
Return ParseExpression()
End Function
''' <summary>
''' 文字列をトークンに分解する
''' </summary>
Private Sub Tokenize(input As String)
Dim i As Integer = 0
While i < input.Length
Dim c As Char = input(i)
If Char.IsWhiteSpace(c) Then
i += 1
ElseIf Char.IsDigit(c) OrElse c = "." Then
' 数値の抽出
Dim start As Integer = i
While i < input.Length AndAlso (Char.IsDigit(input(i)) OrElse input(i) = ".")
i += 1
End While
Dim val As String = input.Substring(start, i - start)
_tokens.Add(New Token With {.Type = TokenType.Number, .Value = val, .NumberValue = Double.Parse(val)})
ElseIf Char.IsLetter(c) Then
' 変数の抽出
Dim start As Integer = i
While i < input.Length AndAlso Char.IsLetterOrDigit(input(i))
i += 1
End While
Dim val As String = input.Substring(start, i - start)
_tokens.Add(New Token With {.Type = TokenType.Variable, .Value = val})
Else
' 演算子の抽出
Select Case c
Case "+" : _tokens.Add(New Token With {.Type = TokenType.Plus, .Value = "+"})
Case "-" : _tokens.Add(New Token With {.Type = TokenType.Minus, .Value = "-"})
Case "*" : _tokens.Add(New Token With {.Type = TokenType.Multiply, .Value = "*"})
Case "/" : _tokens.Add(New Token With {.Type = TokenType.Divide, .Value = "/"})
Case "(" : _tokens.Add(New Token With {.Type = TokenType.LeftParenthesis, .Value = "("})
Case ")" : _tokens.Add(New Token With {.Type = TokenType.RightParenthesis, .Value = ")"})
Case Else
Throw New Exception($"不正な文字を検出しました: {c}")
End Select
i += 1
End If
End While
_tokens.Add(New Token With {.Type = TokenType.EndOfFile})
End Sub
' --- 再帰下降パーサーのコアロジック ---
' 式 (Expression): 加減算
Private Function ParseExpression() As Double
Dim result As Double = ParseTerm()
While _tokens(_position).Type = TokenType.Plus OrElse _tokens(_position).Type = TokenType.Minus
Dim op = _tokens(_position).Type
_position += 1
Dim right = ParseTerm()
If op = TokenType.Plus Then result += right Else result -= right
End While
Return result
End Function
' 項 (Term): 乗除算
Private Function ParseTerm() As Double
Dim result As Double = ParseFactor()
While _tokens(_position).Type = TokenType.Multiply OrElse _tokens(_position).Type = TokenType.Divide
Dim op = _tokens(_position).Type
_position += 1
Dim right = ParseFactor()
If op = TokenType.Multiply Then
result *= right
Else
If right = 0 Then Throw New DivideByZeroException("ゼロ除算が発生しました。")
result /= right
End If
End While
Return result
End Function
' 因子 (Factor): 数値、変数、括弧
Private Function ParseFactor() As Double
Dim token = _tokens(_position)
Select Case token.Type
Case TokenType.Number
_position += 1
Return token.NumberValue
Case TokenType.Variable
_position += 1
If _variables.ContainsKey(token.Value) Then
Return _variables(token.Value)
Else
Throw New Exception($"未定義の変数です: {token.Value}")
End If
Case TokenType.LeftParenthesis
_position += 1
Dim result = ParseExpression()
If _tokens(_position).Type <> TokenType.RightParenthesis Then
Throw New Exception("括弧の閉じ忘れを検出しました。")
End If
_position += 1
Return result
Case Else
Throw New Exception($"構文エラー: 期待されていないトークン '{token.Value}' が見つかりました。")
End Select
End Function
End Class
実装のポイント解説:
1. 再帰下降構文解析の採用: `ParseExpression` $\rightarrow$ `ParseTerm` $\rightarrow$ `ParseFactor` という階層構造にすることで、数学的な演算優先順位(括弧 $>$ 乗除 $>$ 加減)を自然に、かつ完璧に処理している。
2. 変数注入の安全性: `Dictionary(Of String, Double)` を通じて外部から値を注入する設計にしている。これにより、式自体はテンプレートとして固定し、値だけを動的に変えることが可能。これはDBのパラメータとして扱う際に極めて安全だ。
3. 厳格なエラーハンドリング: 「未知の変数」「ゼロ除算」「括弧の不整合」など、業務アプリで発生しうる異常系を、明確な例外として投げている。これにより、ユーザーへのエラー表示も容易になる。
4. 実務への統合:DB・ファイル連携時の注意点
このエンジンを実際の業務アプリに組み込む際、設計者は以下の3点に留意しなければならない。
① パラメータの型管理
DBから取得した値が `Decimal` や `Integer` の場合、必ず `Double`(あるいは精度を極限まで求めるなら `Decimal`)にキャストして辞書に格納すること。型変換の不備は、計算誤差という目に見えないバグを生む。
② 大量データ処理の最適化
もし、数万件のデータに対して同じ数式を適用する場合、毎回 `Evaluate` を呼ぶのではなく、「式の解析結果(AST)」をキャッシュする設計を検討せよ。今回のコードはシンプルにするため毎回解析しているが、究極のパフォーマンスを求めるなら、解析済みのツリー構造を保持し、値の注入のみを行うフェーズを分けるべきだ。
③ バリデーションの二重化
ユーザーがUI上で式を入力する場合、エンジンに投げる前に「使用可能な変数リスト」に基づいた簡易的な正規表現チェックを行うこと。これにより、エンジンに到達する前に不正な入力を弾くことができ、システムの堅牢性が一層高まる。
結論
`DataTable.Compute` は「動けばいい」という段階のプロトタイプには適している。しかし、業務の根幹を支える「計算ロジック」を委ねるには、あまりにも不透明で、リスクが高い。
自前でパーサーを実装することは、一見遠回りに見えるかもしれない。しかし、「制御権をコードの手に取り戻す」ことこそが、バグを最小限に抑え、将来の拡張性に耐えうる、真にプロフェッショナルなエンジニアリングの姿である。
この知識を武器に、単なる「ツール作成者」から、信頼される「システムアーキテクト」へと昇華してほしい。
