[開発プロセス#7] 開発対象システムのモデル記述

システム開発でモデルを作成することにより、これから作るシステムが要求に合致したものであるのかといった側面の検討だけではなく、効率の良い実装が可能であるのかといった側面や、予想される改変や拡張に対して実現可能性やその効率性、テストのしやすさなど、実装に直結すること以上に利用できる。しかしながら、その作成するモデルは、様々な手法がある。要求の記述という点ではユースケース図とユースケース記述が出発点にはなるが、データベース主体の開発だと、ここでER図の作成を主体にすることが多い。データベースの構造は対象ドメインの状況を記述できるようになっている必要があるのだが、その結果、全てがデータベースの設計結果であるスキーマに集約されているという見方をしてしまいがちである。ある作業を、スキーマで解決するか、あるいは実装するプログラムで解決するかはもちろん考えるのではあるが、では「ここの処理はプログラムで」というアイデアはER図やクラス図では不明確である。メモを残すなどの工夫が必要になる。

一方、Webシステムではワイアフレームといったざっくりとした画面のデザインをもとに、画面遷移を定義することで、システムの全体像を示そうとする。しかしながら、画面デザインが反映するのは、システムへの実装以前にドメインに対して実施される分析をもとにして作成されるビジネスモデルであり、実装に直結したER図等のモデルからのズレが生じする場合もある。しかしながら、HTMLでの動的なユーザーインタフェース作成機能は限られていたので、例えばチェックボックスであればどういう動作をすべきかということを逐一記述しなくても、半ば「常識的な判断」で進めることであまり大きなブレは発生しなかった。さらに、結果的にビジネスモデルとER図で記述されるスキーマとはあまり変わらないことお多いので、HTMLのモックアップが「設計」として扱うのは、デザイン主体の開発会社では頻繁に見られる方法である。

このように、ある程度のスキルや、ワークフローができてくると、途中を省略する傾向は強く、単一の会社でそれが回っている間は特に問題視はされないだろう。しかしながら、Webの世界は変化が激しい。特にここ5年くらいの時期はクライアントサイドの作り込みの度合いが高くなり、ユーザーインタフェースは複雑化している。その結果、Webのクライアントサイドは多階層的な機能分離を図る必要が出るなど、スマホのクライアントのネイティブアプリケーションに近い構造になっている。

こうした状況が変わってきた時、ワークフローを見直す必要があると考える。今までの知見や常識として暗黙知的に取り扱っていた知識だけでは、新しい問題に対する解法がない可能性が高く、最適な設計ができなかったり、チーム内での意識の疎通が悪くなる。そのために、本来的な詳細なモデルを記述するということが必要なのである。そこを出発点にして、省略やあるいは詳細化をするとしても、Webシステムのモデル記述の手法を一定のレベルで確立する必要があると考える。

UML自体の汎用性は高く、その点では有用性も高いのは事実である。しかしながら、UMLやその周辺手法を使ってWebシステムの記述をするにあたり、次の点に対して何らかの解決策が必要と考える。

  1. 詳細化するとき、明確なルールは特にない
  2. クラス図を主体にすると、オブジェクト指向が前提となる
  3. ボックスがクラスやオブジェクトで、呼び出しが矢印線的な切り分けによる分かりやすさはあるが、処理の記述では呼び出し線に名称を書くことになり、図を見やすく作ることが困難になりがちである

まず、1の詳細化についてである。作成したモデルと、使用するフレームワークの提供機能を比較検討して、実装箇所を特定するには、極端に言えば、プログラムの1行レベルまでを、モデル内の1つの要素として記述することも必要とされる。また、ロバストネス図やシーケンス図では「画面」を1つの要素として扱うが、実際に処理の流れを追う場合、ボタン1つ1つは原則として異なる動作をするので、ボタン1つ1つを要素として表したいということになる。このための指針が費用である。

2つ目のオブジェクト指向である点は、Javaのプログラムだけを作るような場合には確かに有効ではあるが、HTML、CSS、JavaScriptが絡む世界では必ずしもオブジェクト指向ではない。つまり、クラスやオブジェクトを1つの単位として考える場面は実装時にはあるかもしれないが、分析時にはどちらかと言えば、より細かい単位での要素を記述したい。最終的にはメソッドが集まってクラスになるとしても、対象としたのはメソッドであり、その中でどんな処理をしたいかということである。

3つ目は、コミュニケーション図でオブジェクトからオブジェクトに線を引き、線に添えてメッセージ呼び出しを記述する。この方法は実体あるいは実体化可能なものはボックスであり、メッセージは矢印線であるという明確な分類があるものの、簡単なモデルでも、オブジェクトとメッセージを見やすく配置するのはかなり大変である。ましてや、複雑なモデルだとなおさらである。ここではメッセージの記述が作図作業を煩雑にしている点は明白であり、それに代わる表記方法を検討すべきであると考える。

これらの懸案をまずは見える範囲での解決を行った上で、Webシステムのモデルを記述することにする。まず、1〜3に対する提案を次回より行う。

 

[開発プロセス#2]何のための何を目指したプロセスか?

開発プロセスにおけるアーキテクチャ設計として先日記事を公開しましたが、継続して記事を続けるにあたって、ヘッダに [開発プロセス#N] (Nはシリアル番号)を入れることにします。前回は大まかな話でしたが、今回は検討する開発プロセスの目的を定めます。今回は、当たり前の話ばかりだと思います。

開発において、いきなりプログラミングすることはまずないと思いますが、何らかの検討を経て実装に入ります。実装なくしては当然ながら完成しないので、実装、すなわちプログラミングは開発の中心と思われているかもしれません。しかしながら、実際に開発経験があると理解できることは、事前に検討して問題解決していることと、実装しながら問題解決に当たるのとでは、前者の方がはるかに効率的であり、出来上がったコードの質が高くなるということです。「仕様書をちゃんと書けよ」ということになる一方、仕様に関する成果物に労力をかけないまま実装に入ることもあります。これらの上流工程にどの程度時間をかけて、どの程度の成果物を得ればいいのかということの議論は昔から今に至るまで、そしてきっと未来永劫続くことと思います。ウォーターフォール形式など、開発プロセスが決まれば、1つのプロセスの終了と、検討結果に関する成果物とが対応することになり、いわば自動的に仕様書を書くことが前提になります。一方、仕様書を記載したり、あるいは変更結果を反映させる作業は決して小さな負荷ではありません。アジャイル開発のように、完全な仕様書を書くよりも動くコードを書くことを優先させる手法も出てきている一方、このことを都合よく解釈して「アジャイルはドキュメントは書かなくてもいい」と決めつける人たちがいることも事実です(アジャイル勘違い集#3)。しかしながら、こうした決めつけが出てくる背景には、設計の成果物の労力に見合う結果が得られるのかという懸念があるのではないでしょうか。

実装に入るまでのプロセスを重要視し、仕様書を作るという立場は崩しません。しかしながら、アジャイルソフトウェア宣言には「包括的なドキュメントよりも動くソフトウェアを、」とあります。包括的な、すなわち完全なドキュメントでないけども、設計する上で考えたアイデアや制約などがドキュメントとして参照できる必要があります。ではコードをしっかり書けばいいという考え方もあります。Googleへの注目が増大される時代に、「Googleでは仕様書ではなくコードを見る」のだというイメージが定着し、ここでも勝手な解釈になるのでしょうけど「仕様書はなくてもすごいシステムは作れる」と思ったと考えられます。また、コードは常に最新であるという主張は間違いはありません。しかしながら、コードはコードであり仕様ではありません。コメント欄に記述できる内容も限られていますし、コード上で明示的ではないあるいはその真髄に迫るには時間をかけて解析しないといけないような様々なノウハウが実装には注ぎ込まれているはずです。コードで仕様に関するコミュニケーションがとれるとしたら、その内容に精通したプログラマ同士だけでしょう。オープンソース開発では、コミッターの時間的制約などでそうした状況になりがちであり、単体テスト等のテストを自動化することと組み合わせれば、十分にワークする方法であることは多数のオープンソースプロジェクトから言えるのではないでしょうか。しかしながら、コードを読み解くということは、時間をかけて解析したプログラマ以外に可能かというと、ほぼ無理でしょう。顧客や発注側はそこまではしない、あるいはできないでしょう。「しない」理由は、それをやるくらいなら自分で開発できるから発注しないからです。プロジェクト管理者、あるいはユーザーテストの管理者、導入教育担当、こうした開発に関わる様々な人が、プログラマ並みにコードを読むことができるかといえば、能力的なことだけでなくビジネスに直面するコストの問題でできないと言えます。

受託開発では顧客の要望がコロコロ変わるようなことは普通にあり、これは「消極的なアジャイル」とも見ることができます。追加の開発に対しても費用が出ないことあるのに、ドキュメントの更新に追加の費用を請求する雰囲気は現場にはありません。こうして「使えないドキュメント」が山積みされることになるのですが、その使えないドキュメントでも、過去のある時点をキャプチャした情報として活用せざるを得ない場合もあるわけです。

こうした現在の開発プロセスにおいて、成果物として何が求められるのかを再考するのが、一連の[開発プロセス]記事の1つの大きな目的です。すなわち、ドキュメントに記述されることを、ドキュメントに対する負荷を可能な限り最小化したプロセスとして再考するのが目的です。以下、まとめてみました。

  • 仕様書は作成する必要がある。特に強くコミットしているプログラマ以外のコミュニケーションにはなくてはならない。
  • 仕様書が膨大になると、更新が滞り、最新の結果が反映されなくなる。仕様の変更に対してドキュメントの更新は必要だが、その負荷を最小化したい。

この次は、開発されるシステムがどうなることを意図しているのかということを記載します。こちらの方が先に記載すべきかもしれませんが、目的の話題が何度か続くとういうところです。

開発プロセスにおけるアーキテクチャ設計

INTER-Mediatorに適した開発プロセスを考えていましたが、うまくまとまりません。なんでかなと考えたのですが、INTER-Mediatorを使うという前提なしで考えるべきであり、それを元にINTER-Mediatorへの適用をする必要があると考えます。つまり、一般的な意味でのWebアプリケーションだとかスマホアプリといったものを開発するときに、どんなアーキテクチャが前提となり、設計上何を考えれば要求・要件から実装に近づけるのかという一般論をまずはまとめるべきなのでしょう。最近、たまたま、いろんな仕事の中でアーキテクチャ関連の話題の基本を再勉強する機会があり、その中で一般論になんとなく近づいている感があるので、いっそのこと、考えながらブログでまとめていくという過程で進めようかと考えた次第です。

システム開発では、要求や要件を出発点として、仕様を策定して実装に持ち込みます。外部仕様、内部仕様という区分けがある場合もあります。また、データベースを使う場合は原則としてレイヤー構造になり、データベースが底辺を担うことからドメイン分析した結果をスキーマとして記述して、実装段階では確定したデータベース定義にすることになります。こうした、設計の定石とも言えることはたくさんあり、特定の開発チームや流派、カルチャーに応じてその表現は違うものではありますが、システムを意図した通りに稼働させるという意味では目指す方向は同じであり、場面によって言い方が違うだけの場合もよくあるわけです。一方、様々なノウハウがあるものの、ある要件を満たすようにシステムを作る場合の実装に向けた機能分解は、単一の答えが存在する問題ではありません。同程度に正しい解答はいくつも作れるでしょう。結果的に、「状況に合わせて最適化する」というなんとでも取れるようなアドバイスしか出てこないことにもなりかねません。しかしながら、機能をどのように分解して、それぞれの個別の動作をどのように実装するのかということが、開発の上では非常に重要な決定であることは言うまでもないことです。ベテランや才能のある人は、そういうことをスラスラとできるのかもしれませんが、初心者あるいはある領域の経験が浅いと悩むものです。手っ取り早く知りたいという要求は年々高まり、「ベストプラクティス」や「ティップス」といった情報が重宝されるようにもなります。

こうした設計を完成させるに至る知見は、例えばオブジェクト指向分析や、コンポーネント分析、パターンなど、様々な手法があります。それらをうまく束ねた統一的な開発手法も多数存在します。私が考えていることは、そうした開発手法の1つを作り上げることであり、その結果をもとにして、INTER-Mediatorでの開発手法を確立するということです。車輪の再発明っぽく見えるかもしれませんが、問題点を検討すると、やはり時代に応じた道具に対応した手法はその都度更新されていくものではないかとも考えられます。

現在の様々な開発手法が発展したのは、オブジェクト指向プログラミングが当たり前の時代になり、コンピューターの性能が高まり、そしてインターネットで世界中が繋がって、Webという基盤ができた頃の時代です。一方、現在はスマホ登場やフロントエンドの発展が著しく、サーバー単体での開発手法以上のものが求められています。従来はWeb開発とネイティブ開発、つまりOSのAPIを直接利用する開発は、大きく違っていたのに対して、現在は言語やフレームワークは違っても同じような実装をしているという感覚があります。この領域で業務をしている人は薄々気づいていることではないでしょうか。これは、何か共通のアーキテクチャを持ち込むべきではないかと考えました。まずは、そのための問題意識として持ったことを箇条書きにします。

  • Webシステムはサーバー中心なのは仕方ないとしても、クライアント側のアーキテクチャをどう表現するのが適切だろうか?
  • スマホアプリはサーバー接続する場合が多く、「サーバー側」「スマホ側」という2つのアプリケーションの連動を行っている。最近のWebも「スマホ側」が「JavaScript処理」に置き換わるだけの違いとも言える。つまり、今時のシステムは、サーバーサイド、クライアントサイドを統合的に議論できるアーキテクチャを基盤として持つ必要があるのではないか。
  • コンピューターが速くなり、メモリーも豊富になると、RDBである必要性は薄くなる一方、やはり安定した保存はRDBが理解しやすいとも言える。RDB前提のシステム設計はある意味安定するのだが、逆に必然としてRDBが出てくる理由付けはないのだろうか?
  • サーバーサイドのアーキテクチャはともかくMVCである。いろんなMVCが存在する一方、正しいとか正しくないとか、本物だとかそうじゃないという議論があるものの、MVCは大雑把な機能分類として考えれば、役立つ場面は多いのではないだろうか。
  • クライアントのアーキテクチャはMVCなのだろうか? 違うのだろうか? クライアントサイドの主要な機能はUIの運用とその高度化であるとしたら、MVVMがいろいろな意味で包括性の高いアーキテクチャではないだろうか? ただし、バインディングとオブザーバーを基調としているだけに、それらを実装していないフレームワークはMVVMとして俯瞰できるかどうかが難しい点であると言える。
  • MVCとMVVMを出発点に考えるとしたら、それらの中での機能分割の指標としては、ロバストネス分析をベースに検討できるのではないか、ロバストネス図の要素はビュー、コントロール、エンティティであり、ほぼMVCのコンポーネントに対応するものの1対1ではない。しかしながら、要件をロバストネス分析して、MVCやMVVMを基本としたアーキテクチャとして表現することは、オブジェクトし志向分析の結果を見ても可能であると考えられる。
  • MVCなどのアーキテクチャパターンで設計しても、実装できないかもしれないし、実装するために変形が必要な場合もあるのかもしれない。過去の様々な設計手法では、汎用化がされる一方で、使用するフレームワークの制約やあるいは特有のコールドポイントの扱いなどが考慮されたものは見たことはない。しかしながら、フレームワークによる制約は可能な限り早期に検討する必要がある事柄ではないだろうか。あるいは、フレームワーク制約を組み込む段階がいつなのかを明確にすべきではないだろうか。
  • サーバーとクライアントの分類はどうすべきだろうか? 最初はそれらは関係なく、要件満たすような必要な処理に分割し、あるところで「線を引く」という作業になるのだろうか? そうすれば、サーバーとクライアントの両方を巻き込むシステムの設計はスムーズにできそうだ。

とりあえず、思いつくまま挙げましたので、ポエムに近づきつつあります。これらの考えを少しずつほぐして、手法として使えるまでにすることが目標です。

(続く)

[IM] Ver.5.3よりサポートのsourceキー

Ver.5.3より、コンテキスト定義(定義ファイルのIM_Entry関数の第一引数の配列の要素)に、sourceキーを入れるようになっています。データベース上のエンティティ名に関連するキーとしては、name、view、tableがありますが、それにsourceが加わります。データベースから読み出しを行うときのFROM句に指定されるビューやテーブルの名前は、viewキーがあればその値、なければnameキーの値が使われます。CREATE/UPDATE/DELETコマンドの対象は、tableキーの値で、それがなければnameキーが使わます。例えば、nameキーにテーブル名があれば、そのテーブル名を指定しての読み書きはviewとtableキーの指定はしなくてもできます。しかしながら、表示は何かのビューを使い、更新はそのビューではない特定のテーブルを指定したいような場合に対処できるように、viewとtableキーを用意しました。

しかし、これだけでは問題があります。あるテーブルのあるレコードのあるフィールドが、ページ上の複数のリンクノードに展開された場合、それが例えば両方ともテキストフィールドであれば、一方の値を変えると、フィールド更新のためのサーバー通信が行われるとともに、クライアントサイドで同一フィールドの別リンクノードの値を更新できるようになっています。その時、何を手掛かりにして反映させるコンテキストを探しているかといえば、「viewないしはnameキーの値が同じだが異なるコンテキスト」です。例えば、住所録的なpeopleテーブルがあり、同一ページに全レコードの一覧と、その中で男性の一覧の両方があったとします。前者のコンテキストは、name=allmembers, view=people、後者のコンテキストはname=malemembers, view=peopleとして適切なqueryキーの条件を与えればいいでしょう。こうすれば、同一テーブルから異なるコンテキストを生成して、内容が異なる一覧を1ページ内で生成できます。

この時、男性の一覧にあるレコードは、必ず全員のレコードの一覧にもあります。どちらもviewキーがpeopleなので、男性のレコードの1つのフィールドを変更すると、対応する全員の一覧にあるレコードのフィールド値も更新されます。以下、この動作は「連動」と呼びます。同一のviewキーの値を持つコンテキスト同士なので、INTER-Mediatorにとっての手がかりがあります。

しかしながら、SQLのレベルで、peopleをもとにしたビューeveryoneとsomeoneがあったとします。それらで表を作るとしたら、nameやviewを使うにしても、everyoneとsomeoneがそれぞれのコンテキストに登場はしますが、コンテキストの情報から各々が同一のpeopleテーブルから導出されていることはわかりません。そうなると、同一のレコードがそれぞれの一覧に見えていて、一方の値を変更したとしても、その変更結果を伝達する手がかりがなく、連動はできません。

このような時には、一方のコンテキストを、name=everyone, source=people、もう一方はname=someone, source=peopleと定義します。ページファイル側は、everyoneあるいはsomeoneをdata-im属性に利用する点は変わりありません。このsourceの設定により、2つのコンテキスト定義から得られるコンテキストは、同一のテーブルから来ているものということがINTER-Mediatorに伝わるので、同一フィールド値がユーザーインタフェース上で連動するようになります。

[IM]リピーター判別の汎用化

INTER-Mediatorに関して、1つ重要な仕様変更を考えています。エンクロージャー、リピーターの識別に関することです。現状は次のようなルールになっています。

(1)ターゲット指定(data-im属性)のあるノードを含む、TR、OPTION、LIタグ要素は自動的にリピーターとなる
(2)自動的にリピーターとなったノードの親ノードが自動的にエンクロージャーとなる
(3)DIV、SPANに限り、data-im-control属性を指定して、リピーターとエンクロージャーを明示的に指定できる

この3つ目のルールを廃止して、次のようなルールにしようかと思っています。ようするに汎用化です。(1)(2)のルールはそのまま継続させます。結果的に、(2)は(3′)に対しても適用されることになります。

(3′)data-im-control属性の値が「repeater」の場合、そのノード及び兄弟ノードのリピーターを集めてレコードの数だけ繰り返す。
(2′)repater群の1つ上位のノードは自動的にエンクロジャーであると識別する。

OL/ULだけでなく、DLなどのタグや、その他、SECTIONタグなど、エンクロージャー/リピーターとして使用したいタグがある点は、以前から指摘されています。SAPN/DIVが対応していれば、「なんとかなる」とは言え、時に不便なこともあります。結果的に、なんでもリピーターにしていいのではないかというのが考えた結論です。そうすれば、DIV内部にINPUTタグのリピートみたいなこともできます。

[IM]INTER-Mediatorはなぜ“会社”にしないのか?

このところ、INTER-Mediatorはなぜ会社にしないのかという主旨の質問を聞かれることが何度かありました。INTER-Mediatorに対して、「なぜ、会社にしないのですか?」「これだったら投資してもらえるのでは?」といった疑問を持たれるようです。ちょうどいい機会なので、新居が考えることを書いておきたいと思います。

まず、INTER-Mediatorもそこそこ出来上がってきており、これを元に仕事ができるレベルになっているのは確かです。新居自身、既にコンスタントにINTER-Mediatorを使った開発案件を抱えるくらいになりつつあります。としたら、一般的に考えられるのは、会社を作ってよりビジネスを広げることでしょう。また、投資が得られたら、ツールの開発などに取り組むことも現実的な作業として考えられるでしょう。しかし、新居自身は「INTER-Mediatorの会社」を作る意思は今の所ありません。

考えられる理想的な状況は、会社を作ってビジネスが軌道に乗るということではなく、次のようなことです。INTER-Mediatorはあくまで素材として存在し、それをベースにビジネスを行う会社が複数共存(連立か?)できるような状況が思想的だと考えます。1社の製品として、1社がコントロールする場合、まず、一定のシェアを確保しないと、ビジネスが存続しません。それは、言い換えれば他者を排除することにもつながります。もちろん、多大なシェアを目指すのも1つの手法ですが、既に数度の飽和をしているIT市場であり、いくつものビッグプレイヤーに1社で臨むのはかなり無理があると考えます。また、そうした一種の賭けをやると、終わりも視野に入ってきます。ビジネスの表舞台に立てるというのは魅力である一方、ダメになった時にはゼロ以下になってしまうということもあります。

INTER-Mediatorが目指すものは、システム開発プロセスの変革であり、その結果、情報システムを広げることでITのメリットをたくさんの人が、より多くの場面で享受できるようにすることです。もちろん、そうした意思はINTER-Mediator特有のものではなく、多くの人たちが様々な手法でチャレンジし続けています。INTER-Mediatorはその流れのほんの小さなインパクトにしか過ぎないものですが、理想を目指して継続させることこそが、何らかの貢献になるのではないかと信じています。そのためには、ビジネスのフィールドに軸足を突っ込むことは、リスクを増やすだけと考えています。

ただし、その一方で、INTER-Mediatorでビジネスをすることには肯定的に捉えています。自分も、業務システムの発注を受けていますし、エミックさんのようにFMPublisherというビジネス展開の一翼をINTER-Mediatorベースに進めていらっしゃる会社もあります。新居自身は、他の皆さんがビジネス展開する上で、必要であれば支援は行います。また、特定の会社だけでなく、原則として複数の会社に対しても支援しています。INTER-Mediatorのコアは「コミュニティ」だと考えています。そのコミュニティで培われたものを持ってビジネスフィールドに進出する人や会社は複数あって、それぞれが独自にアイデアを持ち、協業し、あるいは競合して、世界が広がればいいのではないかと考えています。コミュニティを中心にして、ビジネスフィールドが広がるというイメージです。そして、コミュニティの主体を、新居個人ではなく、Committeeにしたのも、そういう願いの1つの現れでもあります。

こうした手法は、オープンソースを主体にした団体の1つの生きる道だと考えます。例えば、Apache Foundationも、ある意味同様な立ち位置ではないかと考えますが、法人格云々となるといろいろなやり方があるかもしれません。しかしながら、コアコミュニティモデルとも言うべき、コンセプトとそれを実現する素材としてソフトウエア開発をコミュニティで行い、ビジネスを行う主体とは別に存在させることで、多彩な展開が実現することを願っています。したがって、INTER-Mediator株式会社を作る意思は今の所はありません。

[IM]INTER-Mediatorワークショップで作った“イベント参加申し込みサイト”

K-OFのイベントの1つとして、INTER-Mediatorのワークショップを2015年11月7日に開きました。ワークショップとなると、参加者の皆さんに手をうごしてもらって…というのが一般的ですが、その場で参加される方々もいらっしゃったので、手を動かすのはプレゼンターだけでやりました。

今回のワークショップでの開発目標は、イベントの参加申し込みです。別の勉強会での事後アンケートで、「こういうのを作りたい」と希望があり、それを題材にさせていただきました。グループカウンセリングを実施している赤石さんは、現在、フォームズというサービスで参加受付を行い、メールで送られてくる参加依頼の内容を1項目ずつコピー&ペースとしているそうです。当初は「コピペをしないで一発でExcelに入れる」という要望だったのですが、いっそのこと、受付フォームと一覧表示をINTER-Mediatorで作成するのはどうかということで了承していただき、それをワークショップの題材としました。なお、実際の運用サイトでは、以下のコード内にあるパスワードや一部の名称はもちろん変更しています。

12191812_909228155821822_1194109405261025992_n

設計内容とスキーマ

ざっくり書いたユーズケース図は次の通りです。アクターは、赤石さんと、グループカウンセリングに参加される「お客様」の2つです。お客様はブラウザで申し込みをして、その結果をメールで受け取ることになります。その申し込み結果を「申し込みリスト」に蓄積して、それを赤石さんは随時チェックするという流れになります。この時、お客様は認証等なく申し込みを行えるようにしますが、一方で赤石さんしかリストが見れないようにします。ここで、データベースへの新規レコードは認証なし、その他の処理は認証を行わないとできないようにするという基本設計もできてしまいます。また、システムとアクターの接点は2か所あり、それぞれ1ページだけで実現しそうなので、合計2つのWebページの作成で済みそうです。

ユースケース図0

ER図と同じ用途のクラス図は次のようになります。ちょっとシンプルですが、「申し込み」という1つのテーブルで作ります。本来、グループカウンセリングの実施が1つのエンティティになり、カウンセリングのテーブルを作成して、申し込みとイベントが多対1の関係を作ります。現状では1種類のイベントを、シーケンシャルに、つまり同時に複数のイベントの募集をしているといった事情もないので、1テーブルで運用します。言い換えれば、イベントには「開催日時」というフィールドしかないので、別テーブルにする必要性は薄いということです。「義援金支払い許諾」はチェックボックスで、受講条件の1つを許諾するかどうかを明確にするものです。「ユーザー情報」は、INTER-Mediatorで認証を実施するために必要なテーブル群です。

クラス図0

これらを実現するMySQL用スキーマとして、以下の記述を作成しました。実はワークショップでは、これに失敗して悩んでしまったのですが、今後のワークショップはこれを雛形にしましょう(苦笑。ユーザーやデータベースを定義してアクセス権を設定し、applyテーブル(クラス図での「申し込み」)を定義します。その後の、authuser、authgroup、authcor、issuedhashのテーブルは認証で必要とするものです。リスト表示の時に使用するuser1ユーザーのみ定義しています。

SET NAMES 'utf8mb4';
#DROP USER 'web'@'localhost';
CREATE USER 'web'@'localhost' IDENTIFIED BY 'password';
DROP DATABASE IF EXISTS akaishi;
CREATE DATABASE akaishi CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE akaishi;
GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE akaishi.* TO 'web'@'localhost';

CREATE TABLE apply (
  apply_id INT AUTO_INCREMENT,
  name VARCHAR(100),
  yomi VARCHAR(100),
  email VARCHAR(100),
  address VARCHAR(255),
  content TEXT,
  agreement INT,
  startdt DATETIME,
  PRIMARY KEY(apply_id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

CREATE TABLE authuser (
  id INT AUTO_INCREMENT,
  username VARCHAR(48),
  hashedpasswd VARCHAR(48),
  email VARCHAR(100),
  limitdt DATETIME,
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

INSERT authuser SET id=1,username='user1',hashedpasswd='d83eefa0a9bd7190c94e7911688503737a99db0154455354';

CREATE TABLE authgroup (
  id INT AUTO_INCREMENT,
  groupname VARCHAR(48),
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

CREATE TABLE authcor (
  id INT AUTO_INCREMENT,
  user_id INT,
  group_id INT,
  dest_group_id INT,
  privname VARCHAR(48),
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

CREATE TABLE issuedhash (
  id INT AUTO_INCREMENT,
  user_id INT,
  clienthost VARCHAR(48),
  hash VARCHAR(48),
  expired DateTime,
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

申し込みページの作成

まず、お客様がイベントの申し込みを行うページを作ります。コンテキストの定義を含む定義ファイル(apply.php)は以下のようなものです。ポストオンリーモードでのページを作成すればいいので、コンテキストは1つだけです。また、新規レコードは認証なしで、その他のデータベース処理はできないようにしたいので、存在しないユーザー(nobodyknows)に対してのみ読み出し、更新、削除ができるようにして、事実上、これらの3つの処理は行えないようにします。authenticationキーにcreateキーの値がないので、createに関しては認証なく実施できます。オプション指定はなく、データベース指定のところに、dsn、user、passwordのキーで、データベースへの接続についての記述を行っています。

<?php
require_once('../INTER-Mediator/INTER-Mediator.php');

IM_Entry(
  array(
    array(
      'name' => 'apply',
      'table' => 'apply',
      'view' => 'dummy',
      'key' => 'apply_id',
      'post-reconstruct'=>true,
      'authentication' => array(
        'read' => array('user' => 'nobodyknows'),
        'update' => array('user' => 'nobodyknows'),
        'delete' => array('user' => 'nobodyknows'),
      ),
      'send-mail' => array(
        'create' => array(
          'from-constant' => "msyk@msyk.net",
          'cc-constant' => "msyk@msyk.net",
          'subject-constant' => "申し込みを受け付けました",
          'to' => "email",
          'body-template' => "confirm.txt",
          'body-fields' => "name,yomi,email,address,content,startdt",
        ),
      ),
    ),
  ),
  array(),
  array(
    'db-class' => 'PDO',
    'dsn' => 'mysql:host=localhost;dbname=akaishi;charset=utf8mb4',
    'user' => 'web',
    'password' => 'password',
  ),
  false
);

お客様が申し込みを行うページのぺージファイル(apply.html)は次のように定義しました。見出しの文字列は最低限にしていますが、実運用の時には書き直して利用していただくとして、INTER-Mediatorの動作に関わる部分だけを作ってあります。ヘッダ部での定義ファイルの読み込み、BODYタグのonload属性でのフレームワーク呼び出し、そしてボディ部での記述がポイントになります。ボディ部では、TABLEタグによるテーブルが定義されていますが、data-im-control属性により、ポストオンリーモードであることが指定されています。あとは、コンテキストとフィールド名のセットをdata-imに記述します。同意の項目はチェックボックスですが、value=1の指定があり、チェックがあれば1をフィールドに設定します。カウンセリングの日付はstartdtフィールドにしますが、ここの部分はお客様が入力するのではなく、募集するときに日時が決定しているので、その日時を初期値として規定し、INPUTタグ自体はreadonly属性を設定して、表示だけをするようにします。最後のBUTTONもdata-im-control属性を指定して、書き込みボタンにします。なお、定義ファイルのコンテキストにはpost-reconstructキーのみがあり、この場合は書き込みを行うとこのページの再ロードを行うので、そこで入力した結果が消えます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script type="text/javascript" src="apply.php"></script>
</head>
<body onload="INTERMediator.construct();">
  <table>
    <tbody data-im-control="post">
      <tr>
        <th>名前</th>
        <td><input type="text" data-im="apply@name"/></td>
      </tr>
      <tr>
        <th>名前の読み</th>
        <td><input type="text" data-im="apply@yomi"/></td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td><input type="text" data-im="apply@email"/></td>
      </tr>
      <tr>
        <th>住所</th>
        <td><input type="text" data-im="apply@address"/></td>
      </tr>
      <tr>
        <th>相談内容</th>
        <td><textarea data-im="apply@content"></textarea></td>
      </tr>
      <tr>
        <th>同意</th>
        <td><input type="checkbox" value="1" data-im="apply@agreement"/></td>
      </tr>
      <tr>
        <th>日程</th>
        <td><input type="text" data-im="apply@startdt" 
                   value="2015-11-07 13:00:00" readonly/></td>
      </tr>
      <tr>
        <th></th>
        <td><button data-im-control="post">申し込む</button> </td>
      </tr>
    </tbody>
  </table>
</body>
</html>

定義ファイルapply.phpのコンテキストには、レコードを作成した時にメールを送信する設定が組み込まれています。そのためのメールの文面は、confirm.txtファイルに以下のように用意します。メールについては、定義ファイルでの定義通り、差出人、CC、件名は決められたものを設定し、送り先は作成されたレコードのemailフィールドを指定します。本文は、以下のconfirm.txtの中の@@1@@などの部分が新規に作成されたレコードの値に置き換わります。body-fieldsキーの値に列挙されたフィールドとその順番より、@@1@@が名前、@@3@@がメールアドレス、@@5@@が相談内容、@@6@@が開催日時を示します。

@@1@@ 様(メールアドレス:@@3@@)

以下の通り、グループカウンセリングのお申し込みを受け付けました。

開催日時:@@6@@
ご相談内容:
@@5@@

_(などなど必要な情報を追加する)_

作成したページは次のようなものです。

shot3036

申し込みを行うと、入力したメールアドレスに、確認のメールが送られます。

shot3041

一覧ページの作成

一覧ページを構成するために、以下のような定義ファイル(list.php)を作成しました。こちらは、applyテーブルの内容を参照したり、場合によっては変更や削除等があるので、すべてのデータベース操作に対して、user1で認証したユーザーだけが許可されるようにしました。また、開催日の逆順で一覧されることで、最近の開催日に対する参加者がリストの上部にまとまるようにしました。コンテキストには、削除ボタンの追加の指示はありますが、挿入ボタンはありません。挿入ボタンは、必要ならapply.htmlから自分で申し込みを入れてそれを修正すればいいと考えました。

<?php
require_once('../INTER-Mediator/INTER-Mediator.php');

IM_Entry(
  array(
    array(
      'name' => 'apply',
      'table' => 'apply',
      'view' => 'apply',
      'key' => 'apply_id',
      'paging' => true,
      'records' => '20',
      'repeat-control' => 'confirm-delete',
      'sort' => array(
        array('field' => 'startdt', 'direction' => 'desc'),
      ),
      'authentication' => array(
        'all' => array('user' => 'user1'),
      ),
    ),
  ),
  array(),
  array(
    'db-class' => 'PDO',
    'dsn' => 'mysql:host=localhost;dbname=akaishi;charset=utf8mb4',
    'user' => 'web',
    'password' => 'password',
  ),
  false
);

一覧のページファイル(list.html)は以下の通りです。特に変わったところはなく、applyコンテキストの内容をページネーションとともに表示をしています。コンテキスト定義にあるように、20レコードずつ表示をします。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="INTER-Mediator/Samples/sample.css" />
  <title>Title</title>
  <script type="text/javascript" src="list.php"></script>
  <style>
    td{
      border: solid 1px black;
    }
  </style>
</head>
<body onload="INTERMediator.construct();">
  <div id="IM_NAVIGATOR"></div>
  <table>
    <thead>
      <tr><th>開始日時</th><th>名前</th><th>読み</th>
        <th>住所</th><th>相談内容</th><th>同意</th><th></th></tr>
    </thead>
    <tbody>
      <tr>
        <td data-im="apply@startdt"></td>
        <td data-im="apply@name"></td>
        <td data-im="apply@yomi"></td>
        <td data-im="apply@address"></td>
        <td data-im="apply@content"></td>
        <td data-im="apply@agreement"></td>
        <td></td>
      </tr>
    </tbody>
  </table>
</body>
</html>

ページを表示すると、ログインパネルが表示されます。

shot3042

正しいユーザー名とパスワードを入力してログインすれば、リスト形式で、申込者の一覧を見ることができます。

shot3043

ワークショップを終えて

結果的にワークショップでは、入力専用(ポストオンリーモード)のページと、その入力した結果を表示するための2つのページを作成しました。実際のワークショップでは凡ミスに気づかずちょっと時間を無駄しにしてしまって申し訳なかったのですが、通常ではこれくらいであれば1時間程度の作業で行えるということが示せたかと思います。また、典型的なシステム開発の流れも見ていただけるサンプルになったかと思います。

なお、こうして作ったシステムも、作ってから新たなニーズが発生するかと思います。そうした追加の要求や変化する要求についても追跡させてもらって、可能な限り、アフター・ワークショップとしてお伝えできればと考えています。

[IM]JavaScriptのメソッド内関数だけをテストする

INTER-Mediatorだけの話ではなく、JavaScript一般的な話です。メソッドの中に、そのメソッドの中だけで使う関数を書くということが可能なので、INTER-Mediatorではそういう実装をあちらこちらでやっています。処理の多いメソッドでは関数に分離することで見通しが良くなりますが、加えて、メソッド内の関数レベルでのテストをするということも考えました。

以下のプログラムで、変数anObjectはあるオブジェクトを参照しています。オブジェクトはmethod1というメソッドがあり、そのメソッドの中で、内部の関数であるfunc1を呼び出しています。このプログラムにより、コンソールには、”method1″ “func1″と2行の出力が行われます。

var anObject = {
  prop1: null,
  method1: function() {
    console.log("method1");
    func1();
    // method1のその他の処理
    function func1() {
      console.log("func1");
    }
  }
}

anObject.method1();

ここで、func1だけをテスト対象にしたく、かつmethod1の実処理を行いたくないと考えたとします。その場合、次のようにプログラムを変更しました。このプログラムは、こちらのページの記載を参考にしました。

var bObject = {
  prop1: null,
  method1: function(isTesting) {
    console.log("method1: isTesting="+isTesting);
    this.func1 = function() {
      console.log("func1");
    };
    if(isTesting) {
      return;
    }
    this.func1();
    // method1のその他の処理
  }
}

bObject.method1();
(new bObject.method1(true)).func1();

ポイントは、内部関数func1をプロパティにしていることです。そして、テストか通常利用かを示す引数isTestingを追加します。この引数は省略すると、undefinedになりますので、テスト時はtrueを追加するとして、省略時は何も指定しないことにします。テストの時には、プロパティへ内部関数の設定だけを行い、メソッドはそのまま終了します。

最後からの2行目は、普通にmethod2を利用する場合を想定しており、コンソールには、”method1: isTesting=undefined” “func1″と出力されます。この記述は、最初のプログラムの最後の行にある「anObject.method1();」と全く同じで、method1はつまりは通常利用では何も変更せずにそのまま利用できます。

一方、一番最後の行の実行により、コンソールには”method1: isTesting=true” “func1″と表示されます。newがあることでmethod1をコンストラクタとして実行するので、メソッド自体を返し、その結果、func1関数がメソッドのように実行できます。コンソールに”func1″と表示するのは、method1内部のthis.func1()を実行している部分ではありません。isTesting編集がtrueなので、その前にmethod1は終了します。つまり、「(new bObject.method1(true)).func1();」という記述で、func1だけを実行できるので、この関数だけの単体テストができるということです。

ちなみに、この方法は、method1に引数があっても利用できます。

[IM] OAuth2あるいはOpenID対応

Google AppsのOAuth2対応について、Using OAuth 2.0 to Access Google APIsに説明されています。この認証方式をサポートしました。したがって、OAuth2対応およびOpenID対応と言えるかと思います。Google以外のプロバイダについては今後、確認します。まずは、認証できるようになっているので、その使用方法を説明します。

OAuth認証するクライアント

まず最初に、ページをOAuth対応にした場合にどんな感じにページ表示されるのかを示します。認証を必須にすると、INTER-Mediator内蔵のログインパネルが表示されそこに「OAuth認証」というボタンが登場します。ここで、ボタンをクリックします。

shot2712

初めて利用するときには、次のように、認証アカウントから何を取り出すのかを表示するメッセージが出てきます。ここで「許可」をクリックします。なお、複数のアカウントでログインしている場合には、さらにどのアカウントでログインするのかを問い合わせるページもこの前に表示されます。

shot2713

そして、認証されます。おなじみのサンプルのページです。ログインユーザーは、ここでは意図的にプロバイダーによって提供される一意な名前とプロバイダー名を@でつないであります。

shot2714

認証のデータベースを見てみると、そちらに新たにユーザーが追加されています。つまり、外部にあるユーザーの認証ではありますが、いったん、利用しているデータベースのauthuserテーブルに、ユーザーを作り、もっぱらauthuserテーブルのユーザー情報を利用して、認証を行います。一定時間で無効化しますが(今日現在未実装)、それまでは、OAuth認証直後にランダムに生成したパスワードをクライアントに通知して記憶させ、INTER-Mediator自身の認証処理を行うことで、都度都度OAuthサーバーとのやりとりはないようにします。

shot2715

Googleでのアカウント発行

GoogleのOAuth2を利用するためには、アカウント発行が必要です。このアカウントは、アプリケーション、つまり、INTER-Mediatorで作るページに対するアカウントです。ページの利用者はこれらの情報は不要ですし、見えるようにする必要はありません。

まず、Googleデベロッパーのコンソールページに移動します。ここで、「プロジェクトを作成」をクリックします。このページは、もしかすると、2つ先のような画面になっているかもしれません。その場合はページの中にある新規プロジェクトを作成するようなボックスがあるので、それをクリックします。

shot2701

プロジェクトの作成で指定するのはプロジェクト名のみです。もちろん、開発者が把握できる名前を指定して、「作成」ボタンをクリックします。

shot2702

このようなページになります。追加したプロジェクトが1つのボックスになっています。また、左側に見えるように、このページが「ホーム」です。初めての利用であれば、このページに行きますが、以前い何か利用していた場合ではこの限りではありませんので、臨機応変に対処しましょう。ここまでで、ともかく1つのプロジェクトを作成してください。

shot2703

プロジェクトのボックスの右下にある「V」マーク部分をクリックすれば、内容が開きます。ここで、「プロジェクトの詳細」をクリックします。

shot2704

左側のナビゲーションで、APIと認証にある、「認証情報」をクリックします。認証情報というパネルが出るので、「認証情報を追加」をクリックします。

shot2705

ドロップダウンから、「OAuth 2.0クライアントID」を選択します。

shot2706

このような画面になります。「同意画面を設定」をクリックします。ここの同意画面は、最初に認証するときに見える画面です。

shot2707

メールアドレスはGoogleアカウントのメールアドレスが設定されています。サービス名を適当に記述しますが、この名前がタイトルになります。その他は「省略可」であるので、認証の動作には影響しません。入力をして「保存」ボタンをクリックします。

shot2708

アプリケーションの種類は「ウェブアプリケーション」を選択します。そして、「名前」は一覧の中で識別する名前を適当に指定します。そして、承認済みのJavaScript生成元には入力せず、承認済みのリダイレクトURLに、ここではINTER-Mediatorに含まれている/Auth_Support/OAuthCatcher.phpのURLが示されています。実際に利用するときには、このファイルのコピーを作るなどしてもかまいませんが、このまま利用してもかまいません。ログインパネルの「OAuth認証」ボタンをクリックすると、Googleが指定する「トークンリクエスト」のURLに移動し、ユーザーの特定や取得可能な情報のアクセス許可をページ上で求めます。その後に、「承認済みのリダイレクトURL」にリダイレクトするときに、アクセストークンが引数として渡され、INTER-Mediatorはそれを元に、OAuthユーザーをデータベースに作成します。

shot2709

「作成」ボタンを押すと、クライアントIDとクライアントシークレットが表示されます。これらはparams.phpファイルにコピペをします。コピペの方法は後で説明します。(ご安心ください。このIDとシークレットはすでに無効化してあります)

shot2710

OKをクリックすると、発行したアイアウントが一覧されるページになります。ここから、削除や追加、あるいは変更などのアカウントの管理もでき明日し、同意画面の変更などもできます。

shot2711

INTER-Mediatorで作成するファイル

OAuth認証を使いたいページでは、まず、定義ファイルに基本的な設定を行います。例えば、以下のファイルは、Samples/Sample_Auth/MySQL_definitions.phpの記述です。IM_Entryの第2引数に、authenticationキーで連想配列を指定します。とりあえず、クレデンシャルの記録方法(ここではセッションストレージ)とレルム(ここでは「Sample_Auth/MySQL_definitions」)を指定します。レルムは単に同一認証を受け付ける領域名とと考えてください。

IM_Entry(
    array( /* コンテキスト定義 */
    array(
        'authentication' => array(
            'storing' => 'session-storage',
            'realm' => 'Sample_Auth/MySQL_definitions',
        ),
    ),
    array('db-class' => 'PDO'),
    2
);

Googleに関連づける情報は、params.phpファイル側に記述できます。これらは、定義ファイルでは記述できないのが現在の仕様です。それぞれ、以下のように、決められた変数名に値を指定します。最初はプロバイダーで現在は「Google」のみサポートしています。続いて、クライアントIDトクライアントシークレットを、Googleのデベロッパーコンソールのページからコピー&ペーストします。最後のリダイレクトのURLは、「承認済みのリダイレクトURL」と同じURLを指定します。

$oAuthProvider = "Google";
$oAuthClientID = '1084721348801-jv3hvi4shcmr4j7unuhioq8k2mm47n6s.apps.googleusercontent.com';
$oAuthClientSecret = 'hV5TZD8x108K1Zac4RfZopur';
$oAuthRedirect = 'http://localhost:7001/Auth_Support/OAuthCatcher.php';

まだ完成というまでには至っていませんが、ともかく、認証できるところまでを作りました。タイムアウトのタイミングや、カスタマイズポイントなどを作りこみたいと思いますが、ご意見があれば、Facebookのグループでよろしくお願いします。

[IM]追加の検索条件のマッチング

INTER-Mediatorでのページ状態の記憶の仕組みについて、幾つかの変更を行っています。変更前に動作の基本として、ローカルコンテキストは、ローカルストレージ(使えない時はクッキー)に保存をするようにしています。ローカルコンテキストには、ページファイル内にあるdata-im属性が「_@fieldName」のデータの置き場所を確保します。加えて、INTERMediator.addConditionメソッドでクエリーに追加する検索条件も、INTERMediatorオブジェクトのプロパティに見えるものの、実際にはそれをローカルストレージに保存しています。その結果、ページを開いた時にはローカルストレージは復元されるので、追加する検索条件も一度与えると明示的に消さない限りは、そのページに戻ったときに復元され、つまりは検索条件が常に保持されるように見える仕組みが組み込まれています。

ここで、ローカルストレージのキーに、URLを含めていましたが、URLのうちのクエリー部分は排除すべきかと考えました。例えば、ページファイルを開くときに渡すパラメータを与えて「page.html?id=101」などといったURLがある場合、ローカルストレージのキーに「page.html?id=101」までを含めるのか、「page.html」だけを含めるのか、どっちがいいのか(正しいのか?)を考えましたが、後者ではないかと思ったのです。クエリー部分があると、クエリーの内容によって以前の検索条件が消えるというのは、動作として正しくないと考えました。

ところがこうすると、こんな問題が発生します。例えば、page.htmlはクエリー部の情報を検索条件として付与するとします。「page.html?id=101」により、id=101という検索条件が加わり、ローカルコンテキストに保持されて残ります。そして、「page.html?id=55」をアクセスしたとき、どちらもpage.htmlなので、id=101という検索条件が残ったまま、id=55が付与されます。つまり、id=101 AND id=55というように、追加の検索条件が設定されてしまいます。idが主キーなら、検索されるレコードは0になってしまいます。これは意図しない動作です。

この問題には正解はないと思いますが、ロカールストレージのキーにクエリー部を含むURLを入れてしまうと、クエリー部によって、保持されるかどうかの挙動が変わってしまうことになります。一方、クエリー部を含めないでキーにすると、検索条件が追加されてしまって、意図しない動作になります。

そこで考えたのが、追加する検索条件については、マッチングをするということです。ローカルトレージのキーには、クエリー部は含めません。しかし、すでにid=101を検索条件として記憶している時に、addConditionメソッドで条件を追加するときには、条件のオブジェクトのfieldおよびoperatorプロパティが同一のもがあれば、その値のみを置き換えるようにします。id=55を追加すると、id=101とid=55のfieldおよびoperatorは同一なので、id=55の方だけが残るようにします。なお、こうした動作ではなく、従来と同様に単純に追加したい場合は、addConditionの3つ目の引数にtrueを指定することで、マッチングをしないで追加をするようにしました。ここまでをすでに実装しました。

そして、INTERMediator.clearConditionは、ローカルコンテキストの追加検索条件をクリアしていますが、同時に、ローカルストレージもクリアするのが正しいのではないかと考えています。すぐに実装は可能です。