Skip to content.

Sections
Personal tools
You are here: Home » コミュニティ » masarl memorial » masarl.cocolog-nifty.com » main » 2004 » 04 » Cotton Bolls: Compositeパターンによる入力チェック

Cotton Bolls: Compositeパターンによる入力チェック

Document Actions

« DBのテスト | トップページ | ポケットCVSリポジトリ »

2004.04.25

Compositeパターンによる入力チェック

目に見えない大きな流れ――
それを「世界」と言うのか「宇宙」と言うのかわかんないけど
オレもアルもその大きい流れの中のほんの小さなひとつ 全の中の一
だけどその一が集まって全が存在する
この世は想像もつかない大きな法則に従って流れている
その流れを知り分解して再構築する
それが錬金術
(荒川 弘:鋼の錬金術師6 より)

業務系のWebアプリでは,画面上にある数多くの項目に対して入力チェックを行う必要がある.例えば,次のような項目を考えてみよう:

期間: 日 〜

一見どこにでもありそうな開始日付と終了日付を入力するだけの項目だ.しかし,こんな単純なものでさえ入力チェックは複雑になる.少し考えただけでもチェック項目が次々と思い浮かぶだろう:

  • 「月」は1から12までの数値が入る
  • 「日」は1から31までの数値が入る
  • 4月31日は不正
  • うるう年でない年の2月29日は不正
  • 「年」と「日」には値が入っているのに,「月」が空欄なのは不正
  • 開始日付が終了日付より未来になっているのは不正

このように,数え上げたらきりがない.しかも実際の画面はもっと複雑で,上の期間入力は画面の片隅にある一項目にすぎない.一般には画面フォームに何十個も入力項目が並んでいるのが普通だ.それらすべてについて入力チェックを行わなければならない.

こういった複雑な処理を扱うときは抽象化が鍵となる.つまり,入力チェックとはどういうことなのかをつきつめて考えてみればよい.

例えば,上の期間入力は6つの項目で構成される.一つ一つは数値入力項目に過ぎないが,年,月,日3つの項目で一つの日付項目とみなすことができる.さらに開始日と終了日2つの日付項目を組み合わせると,全体として期間項目とみなすことができる.

これを画面全体まで広げよう.この期間入力は検索条件の一つに過ぎないかもしれない.例えば,顧客情報検索画面における契約日付のしぼり込み条件なのかもしれない.ということは,逆に画面全体は顧客情報検索項目という一つの入力項目とみなせる.すなわち「一は全,全は一」(鋼の錬金術師)だ.ここにCompositeパターンが適用できる余地がある:

input-classes.gif


Inputが入力項目を表す抽象クラスだ.verifyメソッドを使って入力チェックを行う.TextInputがテキスト入力項目を表し,InputContainerがInputのCompositeクラスになっている.InputContainerの子クラスであるDateInputが日付項目だ.年,月,日3つのTextInputを持っている.また,DatePeriodInputが期間項目で,開始日と終了日2つのDateInputを持っている.

以上のことをJavaScriptで実装してみよう(ここからはJavaScriptによるオブジェクト指向プログラミングの知識が必要).目標は,入力チェックが

var FormInput = new InputContainer("顧客情報検索項目", ...);
FormInput.addInput(...);
...
FormInput.verify();

のように書けることだ.FormInputは画面全体の入力項目で,verifyメソッドを使って入力チェックする.もしエラーがあればエラーメッセージが表示され,エラー項目が選択されるとしよう.

まず入力エラーを表すクラスを定義する:

function InputError(message, input)
{
    this.message = message;
    this.input = input;
}
InputError.prototype.handleIt = function()
{
    alert(this.message);
    this.input.select();
}

InputErrorのコンストラクタにはエラーメッセージ(message)とエラー項目(input)を指定する.handleItメソッドでアラートを表示し,エラー項目を選択してユーザの再入力を促すことになる.

次にInputクラスを定義しよう:

function Input(subject, required)
{
    this.subject = subject;
    this.required = required;
}
Input.prototype.verify = function()
{
    var err = this.searchError();
    if (err != null) {
        err.handleIt();
        return false;
    }
    return true;
}

Inputクラスのコンストラクタには項目名(subject)と必須かどうか(required)を指定する.verifyメソッドでエラーを検索し,見つかればエラー処理を行いfalseを返すが,見つからなければtrueを返す.

次にInputクラスのエラー処理(searchError)の詳細を見てみる.一般に,入力エラーには次の2種類がある:

  • 必須項目の未入力エラー
  • 不正な値のエラー

このことから,searchErrorの中身を次のようにすればいい:

Input.prototype.searchError = function()
{
    if (this.isBlank()) {
        if (this.required)
            return this.makeMissingError();
        else
            return null;
    }
    return this.searchValueError();
}

前半で未入力エラー,後半で不正な値のエラーを検索する.メソッド内のisBlankとsearchValueErrorは子クラスでオーバーライドすべきメソッドだ.makeMissingErrorメソッドは未入力エラーを生成するユーティリティメソッド.具体的には次のようになる(isBlank, searchValueError, selectメソッドは省略):

Input.prototype.makeMissingError = function()
{
    return new InputError(this.subject + "を指定してください", this);
}

今度はInputContainerクラスを定義しよう.インスタンス変数inputsにInputオブジェクトを格納し,addInputメソッドでInputオブジェクトを追加する:

function InputContainer(subject, required)
{
    this.temp = Input;
    this.temp(subject, required);
    this.inputs = new Array();
}
inherit(InputContainer, Input);
InputContainer.prototype.addInput = function(input)
{
    this.inputs.push(input);
}

InputクラスからオーバーライドすべきisBlankメソッドとsearchValueErrorメソッドは次のようになる:

InputContainer.prototype.isBlank = function()
{
    for (var i = 0; i < this.inputs.length; ++i) {
        if (!this.inputs[i].isBlank())
            return false;
    }
    return true;
}
InputContainer.prototype.searchValueError = function()
{
    for (var i = 0; i < this.inputs.length; ++i) {
        err = this.inputs[i].searchError();
        if (err != null)
            return err;
    }
    return this.searchTotalError();
}

最後出てきたsearchTotalErrorは全体として入力項目に矛盾がおきていないか調べるためのテンプレートメソッドだ.例えば,DateInputで4月31日のようなおかしい日付になっていないかのチェックはここで行う.

さて,テキスト入力項目を表すTextInputクラスを定義しよう.TextInputは,内部にTextboxオブジェクトを保持している.ここではコードの一部を紹介しよう:

function TextInput(subject, required, textbox)
{
    this.temp = Input;
    this.temp(subject, required);
    this.textbox = textbox;
}
inherit(TextInput, Input);
TextInput.prototype.value = function() {
    return this.textbox.value;
}
TextInput.prototype.setValue = function(arg) {
    this.textbox.value = arg;
}
TextInput.prototype.select = function() {
    this.textbox.select();
}

こいつの使い方は次の通りだ.例えば,「注文数」の数値入力項目があり,最大99個まで指定できるとする.HTMLが

注文数: <input id="QuantityText" size="2"> 個
<input id="OrderButton" type="button" value="注文"> 

のようになっているとき,TextInputを使ったJavaScriptのコードは次のようになる:

<script type="text/javascript">
var QuantityInput = new TextInput("注文数", true, document.all.QuantityText);
QuantityInput.numeric = true;
QuantityInput.min = 1;
QuantityInput.max = 99;
document.all.OrderButton.onclick = function() {
    if (QuantityInput.verify())
        alert(QuantityInput.value() + "個注文しました");
}
</script>

以上のようなことをDateInput, DatePeriodInputについて適用していけば,入力チェックの管理がだいぶ楽になる.

長くなりすぎたので具体的なコードはこの辺にしよう.詳しく知りたい人は,以下のファイルをダウンロードして解析してほしい.

さて,Inputクラスライブラリを作っていて予想外だったのが,未入力チェックの扱いがかなり自然にできることだ.例えば,InputContainer全体で任意項目でも,部品を必須項目として登録することでうまく設定できる.例えば次の「請求日」項目が任意項目だったとしよう:

var BillingDateInput = new DateInput("請求日", false,
                            document.all.BillingYearText,
                            document.all.BillingMonthText,
                            document.all.BillingDayText);
FormInput.addInput(BillingDateInput);

DateInputコンストラクタの第2引数がfalesなので任意項目ということになる.ところが,DateInputコンストラクタ内部では年,月,日を必須項目として登録している:

function DateInput(subject, required, yearText, monthText, dayText)
{
    this.temp = InputContainer;
    this.temp(subject, required);

    this.yearInput = new TextInput(subject + "の年", true, yearText);
    this.yearInput.numeric = true;
    this.yearInput.min = 1900;
    this.yearInput.max = 2100;
    this.addInput(this.yearInput);

    this.monthInput = new TextInput(subject + "の月", true, monthText);
    this.monthInput.numeric = true;
    this.monthInput.min = 1;
    this.monthInput.max = 12;
    this.addInput(this.monthInput);

    this.dayInput = new TextInput(subject + "の日", true, dayText);
    this.dayInput.numeric = true;
    this.dayInput.min = 1;
    this.dayInput.max = 31;
    this.addInput(this.dayInput);
}

こうすると,年,月,日すべての項目が未入力の場合はエラーが発生しないが,月項目だけ入力すると年または日項目が未入力エラーと判断されるということだ.このように,Compositeパターンが未入力エラーの管理に対してもうまく働いている.

ところで,実際には上のようなJavaScriptだけでは不十分でサーバーサイドでも同じような入力チェックを行う必要がある.僕の場合,サーバー側でもパラレルにInputクラスライブラリ(もちろんJavaScriptではなくサーバーサイドの言語で)を作っているが,やはり2重管理になるのが嫌なところだ.この辺もいろいろ検討しているが,コードの自動生成ぐらいしか解答が見つかっていない.

01:03 PM | 固定リンク

トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/453390

この記事へのトラックバック一覧です: Compositeパターンによる入力チェック:

コメント

入力チェックのパターン化を探していたらたどり着きました。
とても参考になりました。
Inputクラスで補えないチェック(例:偶数の整数のみ)や、
各変数間の相関チェックなどは、DatePeriodInputのように、
その都度クラスを実装すると理解してよろしいのでしょうか?

投稿者: とおりすがり (October 14, 2004 03:57 PM)

はい,そうです.InputやInputContainerのサブクラスを作ることになると思います.

投稿者: masarl (October 14, 2004 05:40 PM)

4

投稿者: (November 28, 2004 11:12 AM)

いつも大変参考にさせてもらっています。
InputContainerクラスのisBlankメソッドでは

  for (var i = 0; i < this.inputs.length; ++i) {
    if (!this.inputs[i].isBlank())
     return false;
  }
  return true;

としていて、コンテナに含まれている最初のリーフクラスがブランクではない場合、
コンテナもブランクではないとしていると思いますが、そうするとコンテナに含まれる
2つ目以降のリーフクラスがInputクラスのブランクチェックから外れてしまわないでしょうか?
誤解析でしたらすいませんm(__)m

投稿者: あいばっち (December 2, 2004 03:29 PM)

コンテナのisBlankはand条件ですよね.つまりリーフがすべてブランクのときだけコンテナはブランクになる,と.
だからブランクじゃないリーフが一つでも見つかればコンテナは無条件にブランクじゃないといえると思うのですが….

投稿者: masarl (December 2, 2004 11:11 PM)

あっ、コンテナのisBlankの役割はコンテナ自身のブランクを返すことだけですか。
とすると、コンテナに格納されているリーフのブランクチェックはどの部分でやっているのでしょうか?
InputContainer formInput = new InputContainer();
formInput.AddInput(new TextInput("注文数", true, text);
if (formInput.Verify())
  MessageBox.Show("OK");
こーいう風にするとリーフのブランクチェックがされてなく、
必須のtextをブランクにしても素通りしてしまい悩んでます。(私の移し間違いがなければですが・・・)

投稿者: あいばっち (December 3, 2004 03:22 PM)

formInputが必須になっていないとリーフのブランクチェックはしません.
常に注文数を必ず入れないといけないという仕様なら,それを含むformInput自体も必須項目になりますね.
InputContainer formInput = new InputContainer("注文フォーム", true);
でどうでしょう?

投稿者: masarl (December 3, 2004 05:08 PM)

長々と質問すいません。
そうですね、コンテナを必須にしておかないとInputクラスのsearchErrorでブランクエラーは返してくれないようになってますね。失礼しました。
InputContainer formInput = new InputContainer("注文フォーム", true);
formInput.AddInput(new TextInput("注文数", true, text1);
formInput.AddInput(new TextInput("注文金額", true, text2);
if (formInput.Verify())
 MessageBox.Show("OK");
として、「注文数」はブランクではなく、「注文金額」がブランクであった場合、コンテナとしてはブランクではないから「注文金額」ブランクエラーを返さず
searchValueErrorのチェックにいってしまうと思いますが、どうでしょうか?

投稿者: あいばっち (December 3, 2004 06:05 PM)

そうですが,InputContainer.searchValueErrorの中でちゃんとリーフのsearchErrorを呼んでます.
ここでブランクチェックがかかりますよね?

投稿者: masarl (December 4, 2004 10:28 AM)

いろいろと誤解していましたが、やっとわかりました。
ありがとうございました。

投稿者: あいばっち (December 8, 2004 10:31 AM)

コメントを書く