連載:Excel VBAを「設計」してみよう #03
はじめに
Excel VBAでオブジェクト指向を勉強し始めると、
クラスモジュールを使わなければいけない
という気持ちになることがあります。
せっかくオブジェクト指向を勉強しているのだから、標準モジュールに処理を書くのは古いやり方で、できるだけクラスにした方がよいのではないか。
私も、そんなふうに考えてしまうことがあります。
でも、本当にそうなのでしょうか。
今回は、あえてオブジェクト指向から少し距離を置いて、
標準モジュールだけではダメなのか?
というところから考えてみたいと思います。
先に私の考えを書いてしまうと、
標準モジュールで分かりやすく作れるなら、それでもよい。
と思っています。
クラスを使うことそのものが目的ではないからです。
では、それでもクラスが欲しくなるのは、どんなときなのでしょうか。
今回は、その一歩手前まで考えてみます。
前回のおさらい
前回は、「大きなVBAがなぜつらくなるのか」を考えました。
例えば、1つのSubの中に、
- データを読み込む
- 入力内容を確認する
- 金額を計算する
- 結果を書き込む
といった異なる仕事が全部入っていると、コード全体を理解するのが難しくなります。
そこで、仕事ごとに処理を分けてみました。
Sub Main()
ReadData
ValidateData
Calculate
WriteResult
End Sub
かなり見通しがよくなります。
Mainを見るだけでも、
読む
↓
確認する
↓
計算する
↓
書く
というプログラム全体の流れが見えるようになりました。
では、ここで一つ疑問が出てきます。
これで十分なのではないでしょうか?
標準モジュールでも整理できる
例えば、商品の数量と単価から金額を計算して、シートへ結果を書き込む処理を考えてみます。
最初は、こんなコードだったとします。
Sub Main()
Dim ws As Worksheet
Dim quantity As Long
Dim unitPrice As Currency
Dim totalPrice As Currency
Set ws = ThisWorkbook.Worksheets("受注")
quantity = ws.Range("B2").Value
unitPrice = ws.Range("C2").Value
If quantity <= 0 Then
MsgBox "数量を確認してください。"
Exit Sub
End If
If unitPrice < 0 Then
MsgBox "単価を確認してください。"
Exit Sub
End If
totalPrice = quantity * unitPrice
ws.Range("D2").Value = totalPrice
End Sub
小さなマクロなら、これでも困らないかもしれません。
しかし処理が増えてくると、少しずつ読みにくくなります。
そこで、役割ごとに分けてみます。
Sub Main()
Dim quantity As Long
Dim unitPrice As Currency
Dim totalPrice As Currency
ReadData quantity, unitPrice
If Not ValidateData(quantity, unitPrice) Then
Exit Sub
End If
totalPrice = CalculateTotal(quantity, unitPrice)
WriteResult totalPrice
End Sub
それぞれの処理を別のプロシージャにします。
Private Sub ReadData( _
ByRef quantity As Long, _
ByRef unitPrice As Currency)
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("受注")
quantity = ws.Range("B2").Value
unitPrice = ws.Range("C2").Value
End Sub
Private Function ValidateData( _
ByVal quantity As Long, _
ByVal unitPrice As Currency) As Boolean
If quantity <= 0 Then
MsgBox "数量を確認してください。"
ValidateData = False
Exit Function
End If
If unitPrice < 0 Then
MsgBox "単価を確認してください。"
ValidateData = False
Exit Function
End If
ValidateData = True
End Function
Private Function CalculateTotal( _
ByVal quantity As Long, _
ByVal unitPrice As Currency) As Currency
CalculateTotal = quantity * unitPrice
End Function
Private Sub WriteResult(ByVal totalPrice As Currency)
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("受注")
ws.Range("D2").Value = totalPrice
End Sub
まだクラスは一つも使っていません。
全部、標準モジュールです。
でも最初のコードより、それぞれの処理が何をしているのか分かりやすくなりました。
「クラスを使っていないからダメ」ではない
ここは、かなり大切だと思っています。
オブジェクト指向を学び始めると、
標準モジュール
↓
初心者向け
クラスモジュール
↓
高度な設計
のように考えたくなることがあります。
でも、私はそうは考えていません。
先ほどのコードで、
ReadData
ValidateData
CalculateTotal
WriteResult
と役割がきれいに分かれ、
「どこに何が書いてあるか」
が分かるのであれば、それは十分に価値のある整理です。
クラスを使っていないからといって、わざわざ複雑にする必要はありません。
むしろ、
クラスを使うことによって以前より分かりにくくなるなら、それは何のためのクラスなのだろう?
と立ち止まりたいところです。
では全部FunctionやSubに分ければよい?
ここで、反対方向の疑問も出てきます。
役割ごとにFunctionやSubへ分ければよいなら、クラスモジュールは必要ないのでは?
これは、いい疑問だと思います。
例えば今のコードなら、
Main
├ ReadData
├ ValidateData
├ CalculateTotal
└ WriteResult
と整理されています。
この程度なら、標準モジュールだけでも十分かもしれません。
では、少しだけプログラムを大きくしてみます。
データが増えたらどうなる?
今度は、受注データとして次の情報を扱うことにします。
注文番号
顧客ID
商品コード
数量
単価
標準モジュールで作るなら、例えばこんな形になるかもしれません。
Sub Main()
Dim orderNo As String
Dim customerId As String
Dim productCode As String
Dim quantity As Long
Dim unitPrice As Currency
Dim totalPrice As Currency
ReadOrderData _
orderNo, _
customerId, _
productCode, _
quantity, _
unitPrice
If Not ValidateOrderData( _
orderNo, _
customerId, _
productCode, _
quantity, _
unitPrice) Then
Exit Sub
End If
totalPrice = CalculateTotal(quantity, unitPrice)
WriteOrderResult _
orderNo, _
customerId, _
productCode, _
quantity, _
unitPrice, _
totalPrice
End Sub
処理そのものは分かれています。
でも、少し違う問題が見えてきました。
引数が増えています。
orderNo
customerId
productCode
quantity
unitPrice
しかも、この5つはバラバラの情報ではありません。
すべて、
「一つの注文についての情報」
です。
処理は分けられた。でもデータは?
前回から、処理の役割を分けることを考えてきました。
例えば、
読み込む
チェックする
計算する
書き込む
という仕事です。
これは標準モジュールとSub、Functionでもある程度整理できます。
ところが今度は、別のまとまりが見えてきます。
注文番号
顧客ID
商品コード
数量
単価
これらは別々の変数ですが、業務上は、
「注文」
という一つのまとまりとして扱っています。
ここで少し面白い問題が出てきます。
私たちは今まで、
「処理をどう分けるか」
を考えていました。
しかし、そろそろ、
「データをどうまとめるか」
についても考えたくなってきました。
標準モジュールが悪いのではない
ここで一度整理します。
問題は、
標準モジュールを使っていること
ではありません。
標準モジュールでも、
Sub Main()
ReadData
Calculate
WriteResult
End Sub
のように処理を整理できます。
小さなプログラムなら、それだけで十分なこともあります。
だから、
「OOPを勉強したから、とりあえず全部クラスにする」
という方向には進みたくありません。
考えたいのは、
今、自分が扱っている問題を、どう表現すれば分かりやすいのか?
です。
その結果として標準モジュールが分かりやすければ、標準モジュールを使えばよい。
一方で、
注文番号
顧客ID
商品コード
数量
単価
のように、
関連するデータを一つのまとまりとして扱いたい
と思い始めたとき、別の方法も考えられそうです。
ここでクラスを作る?
では、
注文番号
顧客ID
商品コード
数量
単価
を見た瞬間に、
Dim order As Order
Set order = New Order
とすればよいのでしょうか。
まだ少し待ちたいと思います。
もちろん、最終的にはOrderというクラスを作る方法も考えられます。
でも、
「変数が増えたからクラスにする」
だけでは、クラスを作る理由として少し弱い気がします。
なぜ、そのデータを一つにまとめたいのでしょうか。
そもそも「注文」とは何なのでしょうか。
プログラムの中には、
注文
顧客
商品
のようなものが、本当に存在するのでしょうか。
ここを考えずにクラスだけ作ってしまうと、
Order
Customer
Product
という名前の箱を作っただけで終わってしまうかもしれません。
「処理」から「モノ」へ
これまでのコードでは、
読む
チェックする
計算する
書き込む
という「処理」を中心にプログラムを見てきました。
次は、見方を少し変えてみます。
受注業務には何が存在するでしょうか。
例えば、
顧客
商品
注文
注文明細
があります。
一方で、
顧客を登録する
商品を検索する
注文金額を計算する
は処理です。
この違いは何でしょうか。
そして、
なぜオブジェクト指向では、「処理」だけでなく「モノ」に注目するのでしょうか。
ここが、クラスを考え始める入口になると思っています。
AIに「OOPにして」と頼む前に
第1回で、AIが設計案まで出してくれる時代に、人間が設計を学ぶ意味を考えました。
例えばAIに、
この受注処理をオブジェクト指向で設計してください
と頼めば、
Order
Customer
Product
OrderItem
といったクラスを提案してくれるかもしれません。
でも、今回考えたことを踏まえると、そのときに確認したいことがあります。
そもそも、この規模でクラスは必要なのでしょうか?
そして、
なぜOrderというクラスが必要なのでしょうか?
クラスを使っていること自体を評価するのではなく、
そのクラスによって、何が分かりやすくなるのか?
を考える。
これも、AIが出した設計を評価するときの判断軸の一つになると思います。
まとめ
今回は、
「標準モジュールだけではダメなのか?」
を考えてみました。
私の答えは、
ダメではありません。
です。
1. 標準モジュールでも処理は整理できる
Main
├ ReadData
├ ValidateData
├ Calculate
└ WriteResult
のように役割を分ければ、クラスを使わなくても分かりやすいコードを作れます。
2. クラス化しないことが悪い設計なのではない
クラスは目的ではありません。
標準モジュールで十分分かりやすいなら、わざわざ複雑にする必要はないと思います。
3. それでも、別の「まとまり」が見えてくる
プログラムが大きくなると、
注文番号
顧客ID
商品コード
数量
単価
のように、
「これらは全部、一つの注文に関するデータでは?」
というまとまりが見えてきます。
ここから、「処理を分ける」だけではない設計を考えられそうです。
次回
次回は、
「処理」ではなく「モノ」を探してみる
です。
これまでは、
読む
計算する
書く
といった「処理」を中心にコードを見てきました。
次回はコードを書く前に、実際の業務に何が登場するのかを考えてみます。
顧客
商品
注文
注文明細
これらは単なる変数の集まりなのでしょうか。
それとも、プログラムの中でも一つの「モノ」として考えた方が自然なのでしょうか。
ここから少しずつ、オブジェクト指向の世界に入っていきたいと思います。