Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

リテラルと定数とEnumとOTLTとResourceと……の使いわけ

プログラム中で変更可能なパラメータ(メッセージ、設定値、エラーコードなど)は、その記述方法を工夫することで、ソースコードの可読性やアプリケーション保守の容易性を向上させることができます。

一方で、大規模なチームでアプリケーション開発を行う場合、開発者のスキルや経験はピンキリです。そのため、何をリテラルとして記述するか、何をリソースとして設定ファイルに書くかの判断を個別の開発者が行うと、1つのアプリケーション中でも記述方法がバラバラになりかねません。

この記事では、一般的なエンタープライズアプリケーションに現れる各種のパラメータについて、それをどのように表現するかの案を提示します。

まず、パラメータの表現方法としては、大きく以下のようなものが挙げられます。

リテラル
  • 各ソースでバラバラに記述する。アプリケーション全体に対する一括修正が必要な場合、修正漏れや誤修正が発生しない仕組みが必要となる。
  • 修正にはビジネスロジックの再ビルドが必要。
  • リテラルを定義している箇所以外からはプログラム的に参照できない。
  • IDEの入力補助機能による値の補完は不可能。
  • リテラルの定義内容はビジネスロジックの作成者の責任範囲となる。
  • リテラルの内容に誤りがあった場合、ランタイムエラーとなるか、場合によっては誤ったまま動きつづける。
Enum
  • 修正のためにはEnumクラスの再ビルドが必要。ビジネスロジックの再ビルドは不要。
  • 値の参照範囲をある程度制御可能。
  • IDEの入力補助機能による名前の補完が可能。
  • パラメータごとに格納先が分かれる。パラメータの種別ごとに別々のEnumを作成する必要がある。
  • Enumの名前の打ち間違いはコンパイルエラーになる。
OTLT
  • アプリケーションのビルドを伴わずに修正が可能。
  • キーに対して複数の値を対応づけることができる。利用開始日や有効期限などのメタデータを利用した作りこみが可能。
  • 修正のために特別なアプリケーション(DBアクセス用ツール)が必要。
  • 設定の格納先は一元化されている。
  • IDEの入力補助機能によるキーの補完は不可能。
  • 言語化の仕組みが必要な場合はアプリケーションでの作り込みが必要。
  • OTLTのキーの打ち間違いは、多くの場合ランタイムエラーとなる。たまたま別のキーを参照していた場合は誤ったまま動きつづける。
定数
  • 修正のためにはビジネスロジックの再ビルド(定数を参照している側のプログラムの再ビルド)が必要。
  • 値の参照範囲をある程度制御可能。
  • IDEの入力補助機能による名前の補完が可能。
  • 定数の格納先クラスを分けることは可能だが、定数を参照している側のプログラムの再ビルドが必要なので、修正の責任分界点の明確化にはあまり役に立たない。
  • ビジネスロジック中での定数名の打ち間違いはコンパイルエラーになる。
リソース(Javaの.propertiesなど)
  • アプリケーションのビルドを伴わずに修正が可能。
  • キーと値が一対一対応しており、利用開始日や有効期限などのメタデータを持たせることが難しい。
  • 修正のために特別なアプリケーションのインストールが不要。
  • 設定の格納先をファイル単位で分けられるが、逆に統一的な管理をしたければ作りこみが必要。
  • 言語化の仕組みが処理系に組込まれている。
  • IDEの入力補助機能によるキーの補完は不可能。
  • 処理系によっては、ファイルの内容をキャッシュする仕組みが組込まれている。その場合、アプリケーションを再起動しないと更新できない。
  • プログラム中でのリソースのキー名の打ち間違いは、多くの場合ランタイムエラーとなる。たまたま別のキーを参照していた場合は誤ったまま動きつづける。リソース自体の文法エラーも、多くの場合ランタイムエラーとなる。
  • リソースファイルを扱う言語組込みの仕組みがある場合(例:Java)は便利に使えるが、そうでない場合(例:COBOL)は定数や外部ファイルの方が扱いやすい。
外部ファイル(CSV, XML, JSONなど)
  • アプリケーションのビルドを伴わずに修正が可能。
  • キーに対して複数の値を対応づけることができる。利用開始日や有効期限などのメタデータを利用した作りこみが可能。ただし、データを追加するほど編集がややこしくなり、外部ファイル自体の文法エラーにつながる。
  • 修正のために特別なアプリケーションのインストールが不要。
  • 設定の格納先をファイル単位で分けられるが、逆に統一的な管理をしたければ作りこみが必要。
  • 言語化の仕組みは作りこみが必要。
  • IDEの入力補助機能によるキーの補完は不可能。
  • ファイルの内容をキャッシュしたい場合は自前で実装が必要。
  • プログラム中での外部ファイルのキー名の打ち間違いは、多くの場合ランタイムエラーとなる。たまたま別のキーを参照していた場合は誤ったまま動きつづける。外部ファイル自体の文法エラーも、多くの場合ランタイムエラーとなる。

プログラム中で扱われるパラメータごとに、上記のうちどれを使って表現するかの案を示す。

メッセージID・エラーコード(画面やログに出力されるメッセージと1対1対応するID)
ビジネスロジック中にリテラルとして定義する。もっとも大きい理由は、特にメッセージの数が多い場合、定数化してもSoft Codingにしかならないことが多いため。定数にしない理由は、メッセージの意味を定数の名前とメッセージ本文(後述)の両方に書くことになってしまうため。またEnumにしない理由は、Enumにするとメッセージの追加時にリソースファイルとEnum両方を修正する必要があり、修正をミスしたときにリソースファイルとEnumのどちらを間違えたのかの切り分けが必要になってしまうため。なお、命名規則でメッセージID・エラーコードであることが明確に分かる命名規則にすること*1。また、メッセージIDを他の文字列リテラルの一部に含めたり、メッセージIDをビジネスロジックで組み立てたりしてはいけない。必ずメッセージIDだけを独立したリテラルにすることで、ソース中から任意のメッセージIDを検索した場合に誤検出や検出漏れがないようにする。
メッセージ本文(画面やログに出力されるメッセージ)
リソースファイル中に定義した上で、アプリケーション本体とは独立したファイルとしてデプロイする。こうすることで、業務面の要請からメッセージの修正が必要な場合に、本番環境へのプログラム追加なしに顧客が直接メッセージを修正できる *2。多言語化が必要な場合、基本的には処理系組込みの多言語化の仕組みを利用するのがよい。ただし、言語以外のメタデータが必要な場合はCSVのフォーマットを使うことを検討する。特に、ログ出力用メッセージ本文の管理において、メッセージのメタデータとしてログの出力レベルを持たせる場合は、本番環境での障害原因調査の際に特定のログメッセージの出力レベルを変更できるようにしておく(罠掛けに使う)ため、ResourceやCSVなどの外部ファイルにして、追加プログラムなしで修正可能にしておくと便利。
メッセージパラメタ(メッセージの一部を置き換える値、いわゆる埋め字)
埋め字は変数の名前など、ビジネスロジックの内容と密接に関連することが多い。そのため、多言語化の必要がなければプログラムのロジック中に直接埋め込んでもよい。多言語化が必要な場合はリソースに記述することで、処理系の多言語化の仕組みを利用できる。リソースに記述する場合、ビジネスロジックと密接に結びつく内容ため、warに含めるなどしてビジネスロジックと常にいっしょにデプロイされるようにする*3。なお、リソース化する場合はリソースのファイル名およびキー名の設計が必要。たとえば、サブシステムIDをファイル名に、変数の完全修飾名をキーにしたりする。
ラベル(ビジネスロジックの分岐に関係ない、画面に表示される値)
ユーザが表示言語を変更する操作をした時にサーバサイドでレンダリングが走るアーキテクチャの場合は、表示言語の変更によるレイアウト崩れなどの確認をしやすくするため、表示用リソース(HTMLなど)に直接埋め込む。SPA等で表示言語変更時にリロードが走らない場合は、i18n機能の仕様に合わせて別ファイルにラベルの内容を切り出す。たとえばAngular+ngx-translateであればJSONファイルへ切り出す。この場合、アプリケーションのリビルドなしでラベルの修正が可能となる一方、開発時には画面レイアウトの確認にアプリケーションの実行が必要になる。そのため、特に業務処理の実装に着手できていないアプリケーションの開発初期において、業務処理の開発と画面デザインの検討の並走が難しくなる点に注意すること。
設定値(ビジネスロジックの分岐を制御する値)
再ビルドを伴わずに修正する必要のある値(パッケージソフトウェアの導入担当者が、導入先顧客毎に変更する値など)は、リソース化した上で、アプリケーション本体とは別ファイルとしてデプロイする。これは、ビルド作業とデプロイ作業の責任分界点を明確にするためと、デプロイ作業を行う環境ではOS組込みのエディタしか使えない場合が多いため。一方、アプリケーションのエディションの違いなど、ビルド時に値が決定する場合はEnumにするのが望ましい。これは、ビジネスロジック中で1つの値を参照する箇所が多く、IDEによる補完が効いた方がよいため。また、値の変更にリビルドが必要なようにして、カジュアルにエディションを変更されるのを避けるため。また、たとえばアプリケーションの操作権限(管理者・一般ユーザ・ゲストなど)についても、選択肢が固定であればEnumにすることが望ましい。これは、不正な値を設定しようとした場合に正しくランタイムエラーとなり、セキュリティ上問題となるケースを避けられる可能性があるため *4。もちろん、ユーザ操作でロールを追加・削除できるような仕組みの場合は、権限設定であってもEnumにはできず、ビジネスロジックから変更可能なDBのテーブル上に設定を持たせる必要がある。
業務コード(業務中で使用される何らかの値に対応するID。たとえば成田空港を表す"RJAA")
ビジネスロジック中での参照箇所が限られている場合(特定の業務でしか参照されないなど)、リテラルとして定義するのがよい。参照箇所が少ない一方でコードの種別が多いため、定数化してもSoft Codingにしかならないことが多いため。ビジネスロジック中で広く参照される場合は定数化やEnum化してもよいが、コードテーブルがアプリケーションの再ビルドなしに変更可能な一方、定数やEnumは再ビルドが必要となるため、再ビルド不要という利点を相殺してしまう点に注意すること。いずれの場合も、命名規則がアプリケーションではなく業務上の要求によって決まるため、「ここで使われているこのリテラルは業務コードである」ということが明確に分かるの望ましい。たとえば業務コードは、業務コード値を参照するメソッドの引数にのみ使用してそれ以外の場合は使用しない、など。業務コードの値によってビジネスロジックが分岐する場合も、判定を単純なequals()で行うのではなく、業務コードを格納するためのクラスと、そのクラスの比較専用のメソッドequalsCode(String code)などで行えば、どのリテラルが業務コードかコードの文面から判別がつくため、文字列検索による業務コードの使用箇所調査が容易になる。
業務コード値
単純に業務コードとコード値とが一対一対応するだけの場合はリソース化してもよい。コードに対応する値が「標準値」「省略値」など複数ある場合はCSVまたはOTLTを用意する。特に、業務コードの管理元が一元化されており、コードの種別ごとにCSVファイルが作成するのが煩雑な場合はOTLTとするのがよい。多くの場合、業務コードの追加・削除はシステム修正として扱われる(例:元号の追加)ため、コード値の修正に特別なプログラムが必要だったりシステムの再起動を伴ったりしても、問題になるケースは少ないと思われる。

*1:メッセージIDの命名規則は多くの場合アプリケーションアーキテクチャ内に閉じており、業務面の制約を受けにくいことも、リテラルとする理由のひとつ

*2:アプリケーションの再起動は運用担当者が日常業務として実施するという前提をおいている

*3:ビジネスロジック本体の修正と独立に、埋め字の内容が変更されてしまうのを避ける。

*4:セキュリティ事故の責任を問う裁判で「プログラム中でEnumを使ってれば事故が防げたのに、Enumを使ってなかったから過失」みたいな判例があったように思うのですが、探しても出てきませんでした。