概要
題名通りです。HSPにはモジュール機能がありますが、モジュール内部の変数はモジュール外部の変数とは独立なことから、内部に「状態」を持つことができます。このことから、ややこしい概念をラップし、オブジェクト指向風に書くことができます。以下、簡単なメモ帳アプリのコードをもってそのことを示します。
無難に書いた場合
このアプリは次のような外見をしています。操作方法についての詳しい説明は不要でしょう。
こうした外見のアプリを作成する場合、HSPで普通に書くと次のようなコードになります。
sdim text_buffer, 32768 //本文
sdim file_path, 260 //ファイルパス
// GUIを作成
screen 0, 400, 400 :title "メモ帳 - 無題"
objsize 60, 20
pos 10, 10 :button gosub "new", *new
pos 80, 10 :button gosub "load", *load
pos 150, 10 :button gosub "save", *save
pos 10, 40 :mesbox text_buffer, 380, 350, 5 :mesbox_id = stat
stop
// 新規作成
*new
text_buffer = ""
file_path = "無題"
gosub *redraw_mesbox
gosub *redraw_title
return
// ファイル読み込み
*load
dialog "txt", 16 :file_path = refstr
notesel text_buffer :noteload file_path
gosub *redraw_mesbox
gosub *redraw_title
return
// ファイル保存
*save
dialog "txt", 17 :file_path = refstr
notesel text_buffer :notesave file_path
gosub *redraw_mesbox
gosub *redraw_title
return
// テキストボックスを再描画
*redraw_mesbox
objprm mesbox_id, text_buffer
return
// タイトルバーを再描画
*redraw_title
title "メモ帳 - " + getpath(file_path, 8)
return
特に問題ないように見えますが、このコードには重大な問題があります。
つまり、モジュール機能を使用してないため、使用する変数が全てグローバルな名前空間に置かれるのです。
グローバル変数はどこからでも変更できることから、デバッグがしづらくなるとして他のプログラミング言語では嫌われがちです。
ところがHSPの場合、なまじBASICライクで簡単な文法を意識したばかりに、グローバル変数から逃れることが難しいです。おまけにシステム変数もグローバル変数なので、システム変数を使用するコードを書く際は、いつその変数を使用したかを考えないと予想外の挙動を招くことになります。
そこで、なるべく変数のスコープを制御する方向にコードを書き換えます。
オブジェクト指向を意識した場合
#module GUI
// コンストラクタ
#deffunc local init
// 変数を初期化
sdim text_buffer, 32768 //本文
sdim file_path, 260 //ファイルパス
// GUIを作成
screen 0, 400, 400 :title "メモ帳 - 無題"
objsize 60, 20
pos 10, 10 :button gosub "new", *new
pos 80, 10 :button gosub "load", *load
pos 150, 10 :button gosub "save", *save
pos 10, 40 :mesbox text_buffer, 380, 350, 5 :mesbox_id = stat
return
// テキストボックスを再描画
#deffunc redraw_mesbox
objprm mesbox_id, text_buffer
return
// タイトルバーを再描画
#deffunc redraw_title
title "メモ帳 - " + getpath(file_path, 8)
return
// 新規作成
*new
text_buffer = ""
file_path = "無題"
redraw_mesbox
redraw_title
return
// ファイル読み込み
*load
dialog "txt", 16 :file_path = refstr
notesel text_buffer :noteload file_path
redraw_mesbox
redraw_title
return
// ファイル保存
*save
dialog "txt", 17 :file_path = refstr
notesel text_buffer :notesave file_path
redraw_mesbox
redraw_title
return
#global
// メインルーチン
init@GUI
このようにモジュールに置き換えることにより、次のようなメリットがあります。
- 初期化ロジックをキレイに書ける
- グローバル空間に書くメインルーチンを最小限に留められる
- 変数もラベルもモジュール内に封じ込められるので、うっかり被ってしまう心配がない
- 変数がモジュール毎に管理されるので、knowbugでデバッグする際に見やすい
- 他のプログラミング言語と同様に、何らかの処理を行う「固まり」(オブジェクト)をモジュールとして表現できる
- (↑に関連して)「GUI」のように「状態」(ステート)を持つオブジェクトを表現できる
- サブルーチンをなるべくユーザー定義関数/命令として記述すればすっきり
- アクセッサをユーザー定義関数/命令として記述すれば、他モジュールやグローバル空間との依存を小さくできる
HSPは大規模なコードを組むのには元来向いてない言語ですが、それでも長く書きたくなることはありえます。
その際に、上記のコーディングスタイルが役に立てば幸いです。
よくある(?)質問
ラベルもモジュール内に封印するのは何故?
そもそもラベル……もといサブルーチンは、BASIC時代の遺物というべき代物です。
GOTOプログラミングを招いたり、ステートの概念がないので初期化処理が面倒などの欠点が目立つためなるべく使いたくないのですが、HSPの場合、buttonなどを使うと仕様上必ずラベルを使わなければなりません。
結合度の観点からも、関係ないモジュールやグローバル空間からラベルが叩かれないようにした方が見通しが良くなるでしょう。
変数名やユーザー定義命令/関数名に決まりはあるの?
ありません……が、HSPの場合、ユーザー定義命令/関数において、引数の変数名とそれ以外の「モジュール内で使う」変数名が被ってはならないといった意味不明な制約があるため、「引数の末尾に_を付ける」などの自衛策は施しておくべきでしょう。
また、変数名はデバッガ上では全て小文字になりますので、キャメルケースよりスネークケースの方が見やすいのではないでしょうか。
ちなみに、命令・関数を定義する際、『#deffunc local 名前
』とlocal
キーワードを挟むことで、同じ名前の命令・関数でも『init@GUI』や『init@Database』のように使い分けることができます。ただ、こうすると当該モジュール内でも@必須になるため(モジュール内で定義された変数との挙動差に注意)、その名前を1回しか使わない場合はlocal
キーワードを挟まない選択肢もあります。
もっとも、この仕様はヘルプに一切書かれていない上、**10年前に要望が報告されているものの未だにヘルプに記述がない**という救いがたい状況なのですが。
onclickとかoncmdとかはどこに書くの?
今のところはグローバル空間に書いています。モジュール内で使えるかは試していませんが、なまじ全てのモジュール/グローバル空間に影響するので、モジュール内で記述すると却って混乱の元になりそうな気がします。
@を付ければどのモジュール内の変数・ラベルも叩けるんだからラップする意味ないんじゃ?
確かに@を付ければどうにでも弄れますが、その辺はコーディング規約で対処できる範囲内でしょう。
C#と違い、getter/setter周りの便利機能は存在しませんので、「@禁止」を厳密に守ろうとすると、わざわざ「setValue」「getValue」などといったユーザー定義命令/関数を書く必要があり、面倒に感じることも多いでしょう。
特に一人で書くプロジェクトの場合、サボって@を使って参照してもいいと思います……もちろん、ユーザー定義関数/命令で書けないかは検討するべきでしょうけれど。