boxlang-best-practices について
開発者やAIエージェントがBoxLangコードを書く際のリファレンススキルです。命名、スコープ、エラー処理、ヌル安全性、パフォーマンス、コード構成に加え、BoxLangとCFMLの構文の違いも扱います。
主な機能
- 01BoxLangの命名規則とスコープ規約
- 02引数と戻り値の型指定
- 03エラー処理とヌル安全性の指針
- 04パフォーマンスとコレクション操作の推奨事項
- 05BoxLangとCFMLの構文比較
活用シーン
- 01BoxLangコードの慣用パターンの確認
- 02スコープ漏れや組み込みスコープの遮蔽防止
- 03BoxLangのクラスとアプリケーション層の構成
- 04CFMLからの移行時のBoxLang構文確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ortus-boxlang/ skills --skill boxlang-best-practicesインストール後、Claude Code で/boxlang-best-practicesを入力して読み込まれたか確認します。
詳細
BoxLang のベストプラクティス
BoxLang は、開発者と AI エージェントが連携してアプリケーションを構築、モダナイズ、実行するための、AI ネイティブなソフトウェア生産性プラットフォームです。
概要
BoxLang は、最新の動的 JVM 言語です。ここで紹介するベストプラクティスは、言語の設計、CFML の伝統、JVM のパフォーマンス特性を踏まえた、BoxLang らしいパターンを反映しています。これらに従うことで、読みやすく、保守しやすく、高パフォーマンスで安全なコードを記述できます。
命名規則
| 項目 | 規則 | 例 |
|---|---|---|
| 変数 | キャメルケース | userProfile、orderTotal |
| 関数/メソッド | キャメルケース | getUserById()、processOrder() |
| クラス | パスカルケース | UserService、OrderProcessor |
| 定数 | 大文字スネークケース | MAX_RETRIES、DEFAULT_TIMEOUT |
| ファイル(クラス) | パスカルケース | UserService.bx |
| ファイル(テンプレート) | キャメルケースまたはケバブケース | userProfile.bxm、order-details.bxm |
| ファイル(スクリプト) | キャメルケース | buildReport.bxs |
// Good
class UserService {
function getUserById( required numeric id ) {
var MAX_RETRIES = 3
var userId = arguments.id
return userRepository.find( userId )
}
}
変数のスコープ
variables スコープ(コンポーネントレベルのスコープ)を汚染しないよう、関数内では常に var を使ってローカル変数を宣言してください。
// BAD — leaks into variables scope
function process() {
result = doWork()
return result
}
// GOOD — properly local
function process() {
var result = doWork()
return result
}
曖昧さが生じる場合は、明示的なスコープ接頭辞を使用してください。
function getUser( required numeric id ) {
// Explicitly scope to avoid confusion
var userData = variables.userRepo.find( arguments.id )
return userData
}
組み込みスコープ名のシャドーイングを避ける
組み込みスコープと同じ名前のローカル変数は、決して宣言しないでください。BoxLang はスコープチェーンをたどってスコープ指定のない変数参照を解決します。スコープ名をシャドーイングすると、そのスコープのデータにアクセスできなくなり、原因の分かりにくいバグが発生します。
変数名として使用を避ける予約済みスコープ名:
session、server、request、url、form、application、cgi、thread
// BAD — shadows the `session` scope, breaking all session access
function login( required string user ) {
var session = { user: arguments.user } // DON'T do this
session.user = arguments.user // writes to local var, not session scope
}
// BAD — shadows the `url` scope
var url = parseUrl( input ) // url.params is now broken
// GOOD — use descriptive names that don't collide
function login( required string user ) {
var sessionData = { user: arguments.user }
session.user = arguments.user // correctly writes to session scope
}
// GOOD — use a different name
var urlParts = parseUrl( input )
var queryParams = url.params // url scope still accessible
スコープデータへのローカル参照が必要な場合は、スコープ自体ではなく、データの内容を表す名前を使用してください。
| 代わりに | 使用する名前 |
|---|---|
var session = ... | var sessionData、var userSession、var sess |
var request = ... | var requestData、var req、var httpRequest |
var url = ... | var urlParts、var currentUrl、var uri |
var form = ... | var formData、var formFields、var payload |
var application = ... | var appConfig、var appData、var settings |
var cgi = ... | var cgiData、var serverInfo |
var thread = ... | var threadInfo、var workerThread |
var server = ... | var serverInfo、var hostConfig |
スコープ検索のパフォーマンス
BoxLang は、スコープ指定のない変数にアクセスするたびにスコープチェーンをたどります。頻繁に実行されるコードパス(タイトなループやトラフィックの多いリクエストハンドラー)では、予測可能なパフォーマンスを得るために、変数のスコープを明示してください。
// GOOD in hot paths — no scope chain walk
var len = arguments.items.len()
for ( var i = 1; i <= len; i++ ) {
// process arguments.items[ i ]
}
関数
引数の型と必須かどうかを常に宣言する
// BAD — no type information
function processOrder( order, userId ) { ... }
// GOOD — self-documenting, validated at runtime
function processOrder( required struct order, required numeric userId ) {
...
}
明確さのために名前付き引数を使用する
// Hard to read
createUser( "John", "Doe", "[email protected]", true )
// GOOD — named arguments document intent
createUser(
firstName = "John",
lastName = "Doe",
email = "[email protected]",
active = true
)
戻り値の型
公開関数には戻り値の型を宣言し、契約を明示するとともに、IDE のサポートを向上させてください。
struct function getUser( required numeric id ) {
return userService.find( arguments.id )
}
array function listActiveUsers() {
return userService.findByStatus( "active" )
}
エラー処理
具体的な例外型をキャッチする
// BAD — catches everything, hides bugs
try {
processOrder( order )
} catch ( any e ) {
logError( e )
}
// GOOD — handle specific cases, re-throw unknown
try {
processOrder( order )
} catch ( "Database" e ) {
handleDatabaseError( e )
} catch ( "Validation" e ) {
return { success: false, message: e.message }
} catch ( any e ) {
// Re-throw unexpected errors
rethrow
}
クリーンアップには cffinally を使用する
transaction {
try {
updateOrder( order )
chargePayment( payment )
transactionCommit()
} catch ( any e ) {
transactionRollback()
rethrow
}
}
Null 安全性
ヌルポインターエラーを避けるため、安全ナビゲーション演算子(?.)とエルビス演算子(?:)を使用してください。
// Null-safe chained access
var city = user?.address?.city ?: "Unknown"
// Null-safe method calls
var count = order?.items?.len() ?: 0
null との直接比較よりも、isNull() を優先してください。
if ( isNull( result ) ) {
return getDefault()
}
クロージャとラムダ
引数だけを対象とする純粋で決定的な操作にはラムダ(->)を使用してください。外側のスコープ変数にアクセスしたり、外部関数や BIF を呼び出したりする場合は、クロージャ(=>)を使用してください。
// Lambda — only uses the item argument (pure transform)
var doubled = numbers.map( ( n ) -> n * 2 )
// Closure — accesses outer variable `threshold`
var filtered = numbers.filter( ( n ) => n > threshold )
// Closure — calls external BIF
var upper = words.map( ( w ) => uCase( w ) )
構造体と配列のリテラル
コンストラクター関数よりもリテラル構文を優先してください。
// GOOD — literal syntax
var user = {
name: "Alice",
email: "[email protected]",
roles: [ "admin", "user" ]
}
// Avoid unless dynamic keys are needed
var user = structNew()
user.name = "Alice"
キーの順序が重要な場合は、順序付き構造体のリテラル構文を使用してください。
// Ordered struct (insertion order preserved)
var config = [=
host: "localhost",
port: 5432,
database: "myapp"
=]
文字列補間
文字列やテンプレートでの補間には #expression# を使用してください。複雑な式は、読みやすさのために、先に変数へ代入してください。
// Simple interpolation
var message = "Hello, #user.name#!"
// Complex — extract first
var formattedDate = dateTimeFormat( now(), "long" )
var header = "Report generated on #formattedDate#"
コンポーネント(クラス)の設計
コンストラクターパターン
コンストラクターとして init() を使用してください。流れるような構築を実現するため、this を返してください。
class UserService {
property name="userRepo" inject="UserRepository"
function init( required UserRepository userRepository ) {
variables.userRepo = arguments.userRepository
return this
}
}
クラスの責務を絞る(単一責任)
各 .bx クラスには、主な目的を 1 つだけ持たせてください。関連のない処理を扱う「神クラス」は避けてください。サービス、リポジトリ、モデルの各レイヤーに分割してください。
パフォーマンスのヒント
- コストの高い検索結果をキャッシュする — 共有する読み取り専用データは
applicationスコープに保存し、変更時に無効化してください。 - 本番環境では
trustedCache=trueを使用する — クラスファイルのチェック時にディスク I/O が発生するのを防ぎます。 - コンストラクターで事前計算する — 値が変わらない場合は、インスタンス生成時に一度だけ計算してください。
- コレクションの処理には手動ループより
each()/map()/filter()を優先する — 可読性が高く、JIT にも適しています。 - I/O バウンドの非同期処理には仮想スレッドを使用する(
runAsyncのデフォルト)。CPU バウンドの処理には固定プールのエグゼキューターを使用してください。
コードの構成
/app
/models -- Business domain classes (.bx)
/services -- Service layer classes (.bx)
/repositories -- Data access classes (.bx)
/handlers -- Request handlers / controllers (.bx)
/views -- Templates (.bxm)
/includes -- Reusable partial templates (.bxm)
/scripts -- Standalone scripts (.bxs)
Application.bx -- Application lifecycle
避けるべき一般的なアンチパターン
| アンチパターン | 問題 | 対策 |
|---|---|---|
| 関数内のスコープ指定のない変数 | 変数がコンポーネントスコープに漏れます | 常に var を使用する |
スコープ名(session、url、form など)のシャドーイング | 組み込みスコープへのアクセスが妨げられます | sessionData、formData などの具体的な名前を使用する |
暗黙的にすべてを捕捉する catch(any) | 想定外のエラーが握りつぶされます | 不明な例外は再スローする |
| テンプレート内のロジック | テストしにくく、分離が不十分です | サービス/ハンドラーに移動する |
| ハンドラー内の直接的な SQL | 再利用できず、SQL インジェクションのリスクがあります | パラメーター化クエリーを使うリポジトリークラスを使用する |
| コード内へのシークレットの保存 | セキュリティリスクがあります | 設定内で ${env.VAR_NAME} を介して環境変数を使用する |
application スコープの過剰な使用 | 並行処理の不具合につながります | 書き込みには適切なロック(bx:lock)を使用する |
arr[ 0 ](ゼロベースのインデックス) | ArrayIndexOutOfBoundsException | BoxLang の配列は1 始まりです。arr[ 1 ] または arr.first() を使用する |
文末の ; | 冗長で、スタイルに一貫性がなくなります | 文のセミコロンは省略可能です。付けないでください |
cfheader() / <cfabort> | CFML の構文であり、BoxLang のものではありません | bx:header、bx:abort などを使用する |
| Java オブジェクトへの名前付き引数 | サポートされておらず、実行時エラーが発生します | Java メソッドの呼び出しには、常に位置引数を使用する |
呼び出しごとの createObject("java","path") | 冗長で、同じ定型コードを繰り返すことになります | import java:fully.qualified.Class を一度記述し、その後はクラス名を直接使用する |
BoxLang と CFML のクイックリファレンス
BoxLang は CFML から進化しましたが、多くの構文要素で異なる構文を使用します。BoxLang のソースファイルでは、cf で始まるタグや関数を使用しないでください。
| CFML | BoxLang の同等構文 |
|---|---|
cfheader(name="X", value="Y") | bx:header name="X" value="Y"; |
cflocation(url="...") | bx:location url="..."; |
cfabort | bx:abort; |
cfparam name="x" default="" | bx:param name="x" default=""; |
cfinclude template="f.cfm" | bx:include template="f.bxm"; |
<cfsilent> | bx:silent { ... } |
createObject("java","path.Class") | import java:path.Class の後に new java:path.Class() |
配列のベストプラクティス
BoxLang の配列は1 始まりです。Java/JavaScript を使用してきた開発者にとって、これはよくある不具合の原因です。
var items = [ "a", "b", "c" ]
// CORRECT
var first = items[ 1 ] // "a"
var last = items[ items.len() ] // "c"
var first = items.first() // preferred — more readable
var last = items.last() // preferred
// WRONG (throws ArrayIndexOutOfBoundsException)
var first = items[ 0 ]
// Looping — i starts at 1
for ( var i = 1; i <= items.len(); i++ ) {
process( items[ i ] )
}
Java の可変長引数への単一の値の渡し方
Java の可変長引数メソッドには、単独のスカラー値ではなく、BoxLang の配列が必要です。
// CORRECT — wrap in array
storage.query( "SELECT * FROM t WHERE id = ?", [ requestId ] )
// WRONG — bare value is not accepted by Java varargs
storage.query( "SELECT * FROM t WHERE id = ?", requestId ) // throws