独自クラスを使ったXPagesの開発

例えば、注文書を管理するようなXPagesアプリを開発するとなると、注文品ごとに品目、単価、数などを管理することになる。 さらに注文品の数は可変。

このような構造のデータを扱う場合、品目、単価、数を管理するオブジェクトを配列で管理するように設計することが多いと思う。

  var 注文品 = {品目:"ビール", 単価:620, 数:5};
  var 注文品list = [];
  注文品list.push(注文品);
  

データの構造が決まったら、データを操作する関数をいろいろ用意することになる。

などなど。

これらの関数は、データの外にある。しかし、本来は、データ構造とデータ処理は不可分なものなので、本来は、データ操作の関数はデータの中にあるべき。 これぞオブジェクト指向。

Notesには、データベース、文書、フィールド、ビューなどの数々の複雑なデータ構造がてんこ盛りだけど、それらのデータを操作する関数(メソッド)はすべてデータ構造の中で定義されているため、 プログラムが実に書きやすい。

ここでは、JavaScriptで独自にクラスを定義し、どのように使えばいいかを解説してみよう。

作成するサンプル

こんな感じのページを作ってみよう。

サンプル

アプリの構造はこんな感じ。 サンプルアプリの構造

クラスを使ったサンプルは、Test_Class.nsfからダウンロードできる。

クラスについて

クラスとは、内部データとそのデータを処理する方法をセットにしたもの。

クラスからインスタンス(オブジェクトとも言う)を生成し、そのオブジェクトに対して指示を出していくことで処理を進めていく。

クラス化するメリット

やや複雑な構造をもつデータをクラス化するメリットは、ずばり、コードが読みやすくなるということ。あと、変更に強くなる。

クラス化しておくと、あるオブジェクトに対して、つぎつぎに指示を出すような気持ちでコードがかける。指示はオブジェクトの内部でごちゃごちゃやってることになるが、 指示を出す側は内部の処理の都合はあまり気にしなくていい。

指示を受ける側も、言われたことをやってればよくて、自分の外のことをあまり気にする必要がないようなコードがかける。

結果的に、各クラスの独立性が高くなり、変更をかける場合も影響がクラスの内部で済むことが多いので、結果的に変更に強くなる。

クラスを使わない場合、処理はスクリプトライブラリに、おもいつく限りのいろんな処理をずらずら並べる羽目になる。このスタイルだと、データを関数に渡してやって、 その結果を次の関数に渡してやるといった書き方になる。

こんなスタイルだと、処理がちょっと複雑になってくると関与する関数が膨大になり、すぐに破綻する。 一月後に自分が書いたコードを読んでも、何をしたいのか見当がつかなくなる。

汎用ライブラリとクラスライブラリ

JavaScriptでのクラスの書き方

JavaScriptには、いわゆる普通のオブジェクト指向における「クラス」という概念はないらしい。だが、Rubyなどのオブジェクト指向に慣れていると、「クラス」の存在を前提にしないと、 どうも説明がしにくい。というわけで、以下の説明では、「クラス」とか「インスタンス」なんて単語を使いまくるが、かんべんしてほしい。

クラスの定義

クラスはこんな感じで定義する。ここでは、先のサンプルの1つの品目に対応するOrderItemを定義してみよう。

  var OrderItem = function() {
    this.title  = "";  // 品目
    this.value  = "";  // 価格
    this.number = "";  // 数
    this.klass = "OrderItem";  // 単なるマイルール
  }
  
  // メソッド定義
  OrderItem.prototype = {
    // シリアライズ。単純オブジェクトを返す
    serialize : function() {
      var obj = {};
      obj.title  = this.title;
      obj.value  = this.value;
      obj.number = this.number;
      return obj;
    },
  
    // 単純オブジェクトの読み込み
    deserialize : function(obj) {
      this.title  = obj.title;
      this.value  = obj.value;
      this.number = obj.number;
    }
  }
  

クラス定義時の注意点

XPagesでのクラスの利用

クラス定義をライブラリ化

クラスの定義コードはサーバサイドのJavaScriptライブラリ内に組み込んでおく。 基本的には、クラス名と同じファイル名のライブラリにすべきだが、クラスの数が多くなると、JavaScriptライブラリだらけになってしまうので、 関連するクラスは1つのファイルにまとめればいいだろう。

例えば、1つのデータ単位を示すクラスと、そのデータ単位をまとめて管理するクラスなど。

ライブラリを用意できたら、クラスを実際に使うXPageで「リソース」として組み込んでおこう。

インスタンスはviewScopeに

クラスが定義できたら、インスタンス(オブジェクト)をつくろう。 作ったオブジェクトは、viewScopeの適当なメンバーにセットしておけば、あるページを抜けるまでそのオブジェクトは継続して存在してくれる。

  viewScope.order_item = new OrderItem();
  

ただし、viewScopeに独自のインスタンスをセットするには、以下の設定が必要となる。

ページをメモリに保存する設定 これをやらないと、実行時にエラーが起きてしまう。

通常、postNewDocumentとpostOpenDocumentイベントにインスタンスを生成し、viewScopeにセットしておく。 postOpenDocumentイベントでは、文書からデータを読み出してインスタンスにセットしておく。

あとは、保存処理に文書のフィールドにデータを書き戻す処理をいれておく。

これで、あとは表示処理や操作ボタンにviewScopeにセットしてあるインスタンスに指示を出す処理を入れておけばOK。

もうちょっと具体的に書くとこんな感じになる。

作ったオブジェクトに「文書のフィールドからJSONを読み込んで、データを自分の中にセットしておけ」と指示すれば、そのように動いてくれる。 指示を出すコードはこんな感じになる。

  viewScope.itemList = new OrderItemList();
  viewScope.itemList.loadField(document1, "itemList");
  viewScope.itemInput = new OrderItem();
  

クラスを使わないときはこんな感じになるので、だいぶ趣が違う。関数化しても同じこと。どうみても指示じゃなくて、データの手を引いて、いろんな関数に導いてあげる感じになってしまう。

  var json_string = document1.getItemValueString("itemList");
  try {
    var itemList = fromJson(json_string);
  } catch(err) {
    debug("うまくJson処理できなかった。");
    var itemList = [];
  }
  viewScope.itemList = itemList;
  viewScope.itemInput = {};
  

クラス化のメリット

もちろん、クラス内のメソッドの定義は同じようなコードになるが、クラス内に押し込められているので、データとの関連性が明確にわかる。 関数ライブラリだといろんなデータ用の関数がごったまぜになるので、 「このデータを処理する関数はどれだっけ?」と毎回、探すというか、関係性を人間が覚えておく必要がある。関数名とコメントだけが頼りになる。 クラスの場合は、「このデータはこのクラス」という関係だけ理解しておけば、データを処理するメソッドはそのクラス内で定義されているので探す範囲が非常に限定される。

汎用ライブラリとクラスライブラリ

クラスを使ったサンプル

今回のサンプルでは、数が可変の品目リストがやや複雑な構造を持っている。こういうのはクラスにしておくと、見通しのよいプログラムが書ける。

こんな感じ。

このように1単位のデータに対応するクラス名に"List"や"Collection"という名前をつけると、そのデータを複数管理するデータ構造に対応するというルールをつけておくと、コードが読みやすくなる

OrderItemクラス

今回のサンプルのレベルなら無理してクラス化するほどでもないが、作っているうちにいろいろ機能拡張したくなると、「クラス化しておいてよかったー」という時が必ず来るので、めんどうがらずに 最初からクラス化しておこう。

  // ================================
  //   OrderItem
  // ================================
  var OrderItem = function() {
    this.title  = "";  // 品目
    this.value  = "";  // 価格
    this.number = "";  // 数
    this.klass = "OrderItem";  // 単なるマイルール
  }
  
  // メソッド定義
  OrderItem.prototype = {
    // シリアライズ。単純オブジェクトを返す
    serialize : function() {
      var obj = {};
      obj.title  = this.title;
      obj.value  = this.value;
      obj.number = this.number;
      return obj;
    },
  
    // 単純オブジェクトの読み込み
    deserialize : function(obj) {
      this.title  = obj.title;
      this.value  = obj.value;
      this.number = obj.number;
    }
  }
  

メソッドは2つだけ。

インスタンス変数をとる出すときには、インスタンス変数名で直接アクセス可能。カプセル化の原則からすると、あまりよくないのだが、XPagesの<xp:inputText>などでバインド先に OrderItemオブジェクトを指定したりするときには役にたつ。

OrderItemListクラス

OrderItemオブジェクトを束ねるクラス。

  // ================================
  //   OrderItemList
  // ================================
  var OrderItemList = function() {
    this.list = [];
    this.klass = "OrderItemList";  // 単なるマイルール
  }
  
  // メソッド定義
  OrderItemList.prototype = {
    // フィールドからの読み込み
    loadField : function(doc, fieldname) {
      this.list = [];
      var json_text = doc.getItemValueString(fieldname);
      debug("loadField(), json_text=", json_text);
      try {
        var obj_list = fromJson(json_text);
        for(var index=0; index < obj_list.length; index++) {
            var obj = obj_list[index];
            var item = new OrderItem();
            item.deserialize(obj);
            this.list.push(item);
        }
      } catch(err) {
        debug("json変換時にエラー");
      }
    },
  
    // フィールドへの書き込み
    writeField : function(doc, fieldname) {
      debug("this.list=", this.list);
      var temp_list = [];
      for(var index=0; index < this.list.length; index++) {
        var item = this.list[index];
        temp_list.push(item.serialize());
      }
      var json_text = toJson(temp_list);
      doc.replaceItemValue(fieldname, json_text);
    },
  
    // 格納データ数。プロパティ形式
    length : function() {
        return this.list.length;
    },
  
    // データ取得. [index]の代わり。
    at : function(index) {
      return this.list[index];
    },
  
    // OrderItemオブジェクトの追加
    push : function(orderitem) {
      this.list.push(orderitem);
    }
  
  }
  

サンプルに機能追加をしていくと、メソッドもどんどん追加されていく。例えば、データを消去するメソッドとか書き換えるメソッドなど。

これをスクリプトライブラリで定義して、XPageにリソースとして組み込めばOK。

文書を開くときの処理

文書を開く時のコードはこんな感じになる。

  <xp:this.data>
      <xp:dominoDocument formName="main" var="document1">
          <xp:this.postNewDocument><![CDATA[#{javascript:
            viewScope.itemList = new OrderItemList();
            viewScope.itemInput = new OrderItem();
            }]]>
          </xp:this.postNewDocument>
          <xp:this.postOpenDocument><![CDATA[#{javascript:
            viewScope.itemList = new OrderItemList();
            viewScope.itemList.loadField(document1, "itemList");
            viewScope.itemInput = new OrderItem();
          }]]></xp:this.postOpenDocument>
      </xp:dominoDocument>
  </xp:this.data>
  

文書の保存処理はこんな感じ。

  <xp:button id="button2" value="Save">
    <xp:eventHandler event="onclick" submit="true" refreshMode="complete">
      <xp:this.action><![CDATA[#{javascript:
        viewScope.itemList.writeField(document1, "itemList");
        document1.save();
        context.redirectToPage("./list.xsp");
      }]]></xp:this.action>
    </xp:eventHandler>
  </xp:button>
  

 

入力値を表に表示する処理

  入力部分はこんな感じになる。<xp:inputText>のデータソースがviewScope.itemInput.titleなどにバインドされているのがわかる。

  <xp:table border="1" style="width:400.0px;border-style:solid;border-collapse:collapse;">
      <xp:tr>
          <xp:td>品目</xp:td>
          <xp:td>価格</xp:td>
          <xp:td>数</xp:td>
      </xp:tr>
      <xp:tr>
          <xp:td>
              <xp:inputText id="inputText1" value="#{viewScope.itemInput.title}">
              </xp:inputText>
          </xp:td>
          <xp:td>
              <xp:inputText id="inputText2" value="#{viewScope.itemInput.value}">
              </xp:inputText>
          </xp:td>
          <xp:td>
              <xp:inputText id="inputText3" value="#{viewScope.itemInput.number}">
              </xp:inputText>
          </xp:td>
      </xp:tr>
  </xp:table>
  

[↓]ボタンがおされたときの処理はこんな感じ。入力データをaddItemに組み込んで、viewScope.itemInputに詰め込んでいる。

  <xp:button id="button4" value="↓">
      <xp:eventHandler event="onclick" submit="true" refreshMode="complete">
          <xp:this.action><![CDATA[#{javascript:
            var addItem = viewScope.itemInput;
            viewScope.itemList.push(addItem);
            viewScope.itemInput = new OrderItem(); 
          }]]></xp:this.action>
      </xp:eventHandler>
  </xp:button>
  

viewScope.itemListをリスト表示する部分は、こんな感じになる。

  <xp:dataTable id="dataTable1" rows="30" value="#{javascript:viewScope.itemList.list}" var="item2"
      indexVar="item_index">
      <xp:column id="column4">
          <xp:text escape="true" id="computedField4" value="#{javascript:item_index+1}">
              <xp:this.converter>
                  <xp:convertNumber type="number" integerOnly="true">
                  </xp:convertNumber>
              </xp:this.converter>
          </xp:text>
          <xp:this.facets>
              <xp:span xp:key="header">No</xp:span>
          </xp:this.facets>
      </xp:column>
      <xp:column id="column1" >
          <xp:text escape="true" id="computedField3" value="#{javascript:item2.title}">
          </xp:text>
          <xp:this.facets>
              <xp:span xp:key="header">品目</xp:span>
          </xp:this.facets>
      </xp:column>
      <xp:column id="column2" >
          <xp:text escape="true" id="computedField1" value="#{javascript:item2.value}">
          </xp:text>
          <xp:this.facets>
              <xp:span xp:key="header">価格</xp:span>
          </xp:this.facets>
      </xp:column>
      <xp:column id="column3">
          <xp:text escape="true" id="computedField2" value="#{javascript:item2.number}">
          </xp:text>
          <xp:this.facets>
              <xp:span xp:key="header">数</xp:span>
          </xp:this.facets>
      </xp:column>
  </xp:dataTable>
  

データ量の増加に備える

この方法は、あるフィールドにJSON形式の文字列を保存する。扱うデータ量が増えてくると、JSON形式の文字列もそれなりに巨大になり、そのうち32KBの壁を越えることになる。 そうなると、テキストフィールドに保存できなくなるので、なんらかの手を考えないといけない。

クラシカルNotesの開発者なら「容量制限のないリッチテキストフィールドを使えばよくね?」とすぐに気づくだろう。

読み出す方法

まずはフォームの設計をいじって、JSON文字列を格納するフィールドの種類をリッチテキストに変えておく。こうすれば、Notesクライアントで文書を開き、保存すれば格納フィールドがリッチテキスト フィールドに変換される。これを読み出してみよう。

先ほどのloadFieldメソッドは以下のように変更すればいい。

  // フィールドからの読み込み
  loadField : function(doc, fieldname) {
    this.list = [];
    var back_doc = doc.getDocument();
    var field = back_doc.getFirstItem(fieldname);
    var json_text = field.getText();
    json_text = json_text.replace(/\r|\n/g, "");
    try {
      var obj_list = fromJson(json_text);
      for(var index=0; index < obj_list.length; index++) {
          var obj = obj_list[index];
          var item = new OrderItem();
          item.deserialize(obj);
          this.list.push(item);
      }
    } catch(err) {
    }
    // 作ったDomino系オブジェクトは開放を忘れずに
    field.recycle();
    back_doc.recycle();
  },
  

書き出す方法

リッチテキストフィールドに書き出すには以下のようにする。

  // フィールドへの書き込み
  writeField : function(doc, fieldname) {
    var back_doc = doc.getDocument(true);
    back_doc.removeItem(fieldname);
    var field = back_doc.createRichTextItem(fieldname);
    var temp_list = [];
    for(var index=0; index < this.list.length; index++) {
      var item = this.list[index];
      temp_list.push(item.serialize());
    }
    var json_text = toJson(temp_list);
    field.appendText(json_text);
    field.recycle();
    back_doc.recycle();
  },