Ruby on Rails – 設計

Ruby on Railsが動作するようになったところで、アプリケーションの完成形をイメージしていきます。抽象的な事項(アプリケーションが提供する機能)は先に掲げたとおりですので、その機能を提供するための具体的な方法や手順について考えていきます。これにより、アプリケーションがどのようなウェブページを提供しなければならないのか、そして何を作らなければならないのか、明らかになっていきます。

Anacondaは、情報源を外部に頼ります。そのため、「どのような情報が存在するのか」をシステムに知らせてあげなければなりません。情報はIDによって識別するものとします。Evernoteは各ノートにGUIDを割り当てているので、このGUIDをIDとして使えばいいでしょう。Spiderで考えれば、ネットワークのGUIDと、情報オブジェクトの番号を組み合わせた文字列がIDになります。Anacondaは、まず最初に、このID収集を行う必要があります。IDを収集する際、同時に、どの情報源から取得したのかも記録しておく必要がありますね。

使用する情報のIDが集め終わったら、反復学習を始めることができます。以前にチェックした日時と、そのときのユーザー評価から、表示すべき状態にある情報を選び出し、表示します。そして、ユーザーは表示された情報についてチェックし、もう表示する必要はない、ある程度分かったので次のチェックは先でよい、分かっていなかったので次のチェックは近くに行う、などの評価を与えます。メモを残せるようにしてもよいですが、現時点では見送ることにしましょう。ある情報についてチェックが終わったら、次の情報を表示します。表示すべき状態にある情報が無ければ、その日の分は終了したということになります。

なかなか理解が進まない情報や、全体に対する進捗情報などを確認する、分析的な処理もあると便利ですね。しかし、これも現時点では見送ることにしましょう。いつか追加する予定がある、という程度に捉えておきます。

以上から、Anacondaが提供すべきウェブページは、次のようになります。

  • ID収集・学習などの機能へ分岐するトップページ。
  • ID収集のため、収集元を選択するページ。
  • 表示すべき状態にある情報を表示し、それに対する評価を与えるためのページ。

とりあえずは、トップページから作っていくことにしましょう。

とは言ったものの、何を編集すればよいのか分かりません。Ruby on Railsのガイドを見ながら進めていくことにします。

まずはアプリケーションを作成します。

1
$ rails new Anaconda

詳しい仕組みはまだ分からないのですが、サイト上のルートディレクトリはプロジェクト上のpublic/に相当し、ここにindex.htmlというファイルが存在すれば、これを読み込むようです。静的なウェブページを使うことはないはずなので、削除してしまいます。すると、ルーティングに失敗するというエラーが出るようになります。ルートディレクトリについて表示すべきファイルがないため、エラーが出ているようです。そこで、表示すべきページを作成することにします。これは、Ruby on Railsが提供する機能を使って自動的に行うことができます。

1
$ rails generate controller index home

次に、ルーティングを行うファイルに、ルートディレクトリとして表示すべきものの位置を教えてあげる必要があります。config/routes.rbを開き、次の行を書き加えます。

1
  root :to => "home#index"

ルートとして、"home#index"を指定するという意味です。これは、コントローラhomeindexメソッドを表しており、homeというページのindexというラベルを表すものではありません。ここまで書き加えれば、サーバを起動して、http://localhost:3000にアクセスすることで、homeコントローラのindexメソッドが呼び出されていることを確認できます(表示されているのはビューhomeindex.html.erb)。

ところで、config/routes.rbには、次の行も追加されています(railsコマンドで自動的に追加される)。

1
  get "home/index"

これは、パスhome/indexに対するHTTPのGETリクエストを許可するという意味です。呼び出されるのは、homeコントローラのindexメソッドになります。この行を取り除く前はhttp://localhost:3000/home/indexに対するアクセスが成功し、取り除いた後はhttp://localhost:3000/home/indexに対するアクセスがエラーを返すようになります。home/indexは使わないので、取り除いておきます。

サーバの起動は、次のようにして行います。

1
$ rails server

いきなり「コントローラ」や「ビュー」という言葉が出てきました。これは、MVCモデルのControllerとViewのことです。Ruby on RailsはMVCモデルを採用しています。これによって、論理のコードと表示のコードを分離することができ、開発を局所に集中して行うことができるようになります。なお、Rubyはとても緩い言語ですので、表示に関する処理のみをすべきであるビューに、データ操作に関する処理を入れてしまうことができます(たぶん)。これをやるとMVCモデルを採用している意味が完全に失われてしまうので、自分がいまどの部分について作業しているのか、しっかりと自覚しておく必要があります。まだModelが出てきていませんけれども、じきに登場します。

Ruby on Rails – 下準備

Ruby on Railsはスクリプト言語Rubyで動作するので、まずはRubyをインストールするところから始めます。なお、環境はWindowsです。

Rubyの公式サイトからダウンロードしてもよいのですが、RubyInstallerというパッケージを使うのも楽です。これは、Windows用にRubyをビルドした環境をパッケージにしたものです。実行形式のインストーラーもあるので、楽ではあります。単にアーカイブしただけのパッケージもあるので、変なインストーラーは使いたくないという方は、そちらも使えます。使うRubyのバージョンは、もちろん最新版(執筆時点で1.9.2)。いまさら1.8系を使うのも、時代遅れというものです。幸いなことに、Ruby on Railsも3.0以降はRuby 1.9をベースにしているようです。

次に、Ruby on Railsをインストールします。

Rubyを使える状態にして(環境変数PATHにRubyの実行ファイルが格納されたディレクトリを追加する)、Rubyのパッケージマネージャgemを使えるようにしておきます(いまは標準添付されている?)。ここまでできたら、gem install railsするだけです。

1
$ gem install rails

自分の環境では、マニュアルをコンパイルしているところでエラーが発生しました。しかし、全体としては問題なく動作しているので、無視しています。何も指定しなければ、安定した最新版がインストールされることになるでしょう(執筆時点で3.0.9)。

次に、SQLite3をインストールします。これは必須ではないのですが、開発段階からMySQLなどフルセットのデータベースを使うのは設定が面倒ですから、それらの面倒を省くことのできるSQLiteを導入しておいた方が楽だと思います。

まず、gemで必要なRubyライブラリをインストールします。

1
$ gem install sqlite3

次に、SQLiteのホームページからsqlite3.dllをダウンロードします。ダウンロードしたら、適当なディレクトリにDLLを展開し、環境変数PATHを通します。ここまで終わったら、正常に動作するか、チェックしておきましょう。

1
$ ruby -e 'require "sqlite3"'

さて、ここで、rake 0.9以降を使っている場合(?)、rakeがうまく動作しないという問題があります。理由はよく分からないのですが、こちらで紹介されている解決方法を実行すると、確かにエラーが出なくなりました。どうやら、rakeのバージョンによって挙動が違っているようで、それが悪さをしているようです。この対処により正しく動作しているような感じですが、まだちょっと分かりません(のちに検証して追記するなどします)。

まだ方法は紹介していませんが、作成したプロジェクトにあるGemfileに次の行を書き加えます。

1
gem 'rake', '0.8.7'

そして、次のコマンドを実行します。

1
2
$ bundle unlock
$ bundle update

こちらの情報によれば、Gemfile修正後、次のコマンドを実行するのでもいいようです。

1
$ bundle update rake

ちなみに、bundleとはBundlerというプロジェクトが提供するプログラムで、アプリケーション相互の依存関係を管理するために作られたものだそうです。

Ruby on Railsは素晴らしいフレームワークですが(そうなのだそうだ)、さまざまな技術を利用しているので、「果たしてこの機能は何が提供しているんだろうか?」と疑問に思ってしまうことがしばしばです。なにやら、Active Supportという、Rubyの標準ライブラリに(ウェブアプリ開発に)便利な機能を多数追加するという芸当をやっているそうで、これも「果たしてこの機能はRubyが標準で提供しているものなのだろうか?」と思ってしまうことが多いそうです。おいおい分かってくると思いますが、「どこまでがRailsなのか」をしっかり把握しておくようにしたいと思います。まあ、そのうち、そんなことを考えなくてもいい時代が到来するのかもしれませんが。

Project Anaconda

Evernoteの発見により、Spiderは瀕死の重傷を負ってしまいました。

細かい点を見れば存在価値も見出せるので、息の根を止められたというわけではないのですが、はたしてそれらのために作り続けるだけの価値があるのかというと難しいところがあるので、再起不能の重傷を負ってしまったといえるでしょう。

一方、Spiderを利用して作ろうとしていたアプリケーションは、Evernote単体では実現できない機能を提供するので、少なからぬ動揺を受けてはいるものの、未だ健在です。

何を作ろうとしていたかといえば、反復学習のためのアプリケーションです。学習は、新たに取得した記憶を自身に定着することで進んでいく側面があります。これは、同じことを繰り返し行うことで実現されるのですが、自分で意識的に行おうとするとなかなか難しいところがあります。効果的な反復学習を行うには、一定の時間を空けることが必要といわれています。どの情報を、いつ取り組んだのか、すべて自分で管理するのは、情報が増えれば増えるほど難しいものです。そのため、アプリケーションによる学習履歴の管理が必要となるのであり、類似のソフトウェアも存在するようですが、わたしが望む機能を備えているものは無いようです。

このようなソフトウェアを、まるで知識に巻き付いて取り込んでいくものだということで、Anacondaと名付けました。

最初はSpiderのネットワークを利用してアプリケーションを構築しようと考えていたのですが、先述のとおり、Spiderは再起不能となってしまったので、依って立つ土台を大きく変える必要があります。

どうやら、Evernoteは、付属するアプリケーションや、Web APIを利用することで、そのデータベースにアクセスすることができるとのことです。だとすれば、ウェブアプリケーションとの親和性が強いということになります。ウェブアプリといえば真っ先に思い浮かぶのはPHPですが、個人的には、あまり好きではありません。使えるものならば、Rubyを使いたい。そういえば、Rubyにも優秀なフレームワークとしてRuby on Railsというものがあります。噂には聞くのですが、実際に使ったことはありません。

ということで、Ruby on Railsを使って、Anacondaを実装することに決定しました。

とはいえ、Ruby on Railsに関しては素人なので、「流儀」を学びながらの開発となります。その過程については、このブログに書き込んでいく予定です。

さて。まずはAnacondaが備えるべき機能を明らかにしておきましょう。いわば、これから歩む道の到達点です。

  • ある情報について、チェックした日時の履歴を管理することができる。
  • チェックした日時の情報をもとに、現在においてチェックすべき情報を選び出すことができる。
  • もととなる情報は、独自形式を用いず、(ある程度)一般的な形式による。すなわち、他のアプリケーションでも利用できるものとする。

ひとまずは、こんなところです。すでに作成に取りかかっていることもあるので、これは枝葉を取り除いた後の姿になっています。最初はいろいろと別の機能も搭載することを考えていたのですが、そうすると「これは一体いつ完成するのか?」ということになってしまうので、できるだけシンプルな機能だけを搭載することにしました。その代わり、外部とのインターフェイスには最大限の柔軟性を持たせ、多様な外部アプリケーションとの連携を前提としています。

SpiderとEvernote

最近流行のEvernoteを使ってみました。

第一印象としては、「緩い」個人用データベースです。

これはSpiderとほぼ同じ方向性です。パクられた!(笑)

というわけで、困りました。Spiderを作り上げても、その到達地点には、Evernoteという同目的かつ高品質な製品が待ち構えています。はたして、Spiderを作り上げる意味はあるのでしょうか?

もしEvernoteの保有する情報を他のアプリケーションから使用できないのであれば、Spiderにも存在意義があります。しかし、Evernoteは、Evernote APIという形で、データベースへのアクセス方法を提供しているのでした。ちゃんちゃん。

これにて、Spiderは息の根を止められました。

今後は、いかにEvernoteのデータベースを外部アプリケーションから操作するか、という観点で研究を進めていきたいと思います…。

Spiderの実装 (2)

Spiderを実装するうえで頭を悩ませたのは、細かな違いを吸収し、統一された方法でデータベースにアクセスするためのDatabaseインターフェイスです。

インターフェイスのメソッドを実行するたびにSQLをコンパイルしていたのでは動作が重くなって仕方ありませんので、Connection#prepareStatement()でプリコンパイルしておかなければなりません。このタイミングとして、(1) Databaseの実装オブジェクトを構築するときにプリコンパイルする、(2) 各メソッドを実行するときにプリコンパイルする、という方法が考えられます。効率の面を考えれば、(2)ではメソッドを実行するたびにPreparedStatementのフィールドが初期化されているかを調べなければならないので、(1)の方が優れています。(1)は初期化に時間を要しますが、すべてのメソッドが使われるのならば、総計では変わりありません。nullチェックが入る分、(2)の方が(若干ですが)重くなります。

ただ、効率よりも考えるべきなのは、コードの見やすさです。(1)はコンストラクタ、または初期化メソッドにすべてのSQLを書き込まなければならず、そのSQLを使う部分とは離れてしまうことになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Database1 implements Database
{
    public Database1 ()
    {
        this.prepared = connection.prepareStatement("SELECT ...");
        // 初期化するSQLが増えていけば、ここがどんどん伸びていく
    }
    public int getData (long id)
    {
        this.prepared.setLong(1, id);
        ResultSet rs = this.prepared.executeQuery();
        // ...
    }
}

もっとも、エディタの上下分割機能を使えば解決するという問題でもあります。しかし、個人的には、そのような機能を使わずとも、SQLと、そのSQLを利用するコードは近くに配置したいという気持ちです。

一方、(2)の方法を採るのならば、SQLと、そのSQLを利用するコードは近くに配置することができるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Database2 implements Database
{
    private PreparedQuery prepared;
    public int getData (long id)
    {
        if (this.prepared == null) {
            this.prepared = connection.prepareQuery("SELECT ...");
        }
        this.prepared.setLong(1, id);
        ResultSet rs = this.prepared.executeQuery();
        // ...
    }
}

SQLが遠く離れていると、何番目のパラメータに何のデータを入れればよいのか分かりにくくなってしまいますが、このようなコーディング方法であれば必ず近くに配置されるので分かりやすさが向上します。しかし、この方法では、メソッドが実際に呼ばれるまでSQLがコンパイルされません。そのため、SQLに文法エラーがあるとき、発見が遅れることになります(先にコンパイルしておけばオブジェクト構築時にエラーが検出される)。もっとも、すべてのメソッドについてテストするようにしておけば、これは問題になりません。開発スタイルとしても、ちゃんとテストは行うべきです。次に、毎回nullチェックが入るため、動作速度が(きわめてわずかですが)遅くなります。フィールドは最初に初期化された以降nullになることがないので、このチェックは最初の一回以外すべて意味を持ちません。個人的には、このような無駄が気になって仕方ありません。

以上から、(1)のように、あらかじめSQLをプリコンパイルするようにしつつ、(2)のように、SQLと、そのSQLを使うコードを付近に配置するという方針を考えることにします。

Spiderの実装 (1)

インターフェイスだけ定義してもプログラムは動作しませんので、実装をしなければなりません。

Spiderシステムは情報を蓄積して扱いやすいインターフェイスを提供するためのものですから、情報を蓄積するストレージが必要になります。フリースタイル(自前でディスクへの保存などの機能を実装する)でもいいのですが、効率の良いものを構築する自信はありません。また、かなりの手間がかかるので、完成がいつになるのか分かりません。そこで、リレーショナルデータベースをストレージシステムとして利用することにします。幸いなことに、JavaにはJDBCという仕組みが用意されているので、データベースの操作は比較的に楽です。また、著名なデータベースのJDBCドライバも公開されています。

どのデータベースを利用するか悩むところですが、まだシステムが成熟しておらず、テスト段階であることを考えれば、手軽に使うことのできるものが望ましいといえます。とはいえ、いつまでも簡易なデータベースに頼っているようでは、システムやデータの規模が大きくなってきたときに対応できなくなるおそれがあります。そのため、特定のデータベースに依存しない設計を目指すことにし、最初は扱いやすいデータベース(SQLiteなど)で開発し、徐々に本格的なデータベース(MySQLなど)へ移行していくことにします。具体的には、NetworkやConceptはデータベースを抽象化したオブジェクトを介してデータベースと情報をやり取りします。そして、そのデータベースごとに仲介オブジェクトの実体を実装していきます。

簡略的に記述すれば、次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package jp.blacksoft.spider_jdbc;
public interface Database
{
    /**
     * @param id
     *            調べるオブジェクトの概念ID
     * @return オブジェクトの作成日時, オブジェクトが存在しなければ{@code null}
     */
    public Date getConceptCreateDate (long id);
    // etc...
}
public class JdbcNetwork implements Network
{
    private final Database objDatabase;
    // etc...
}
public abstract class JdbcConcept implements Concept
{
    // ...
    @Override
    public Date getCreateDate ()
    {
        return this.getDatabase().getConceptCreateDate(this.numConceptID);
    }
    // ...
}

各データベース用の実装クラスでは、Databaseで定義されたインターフェイスに合わせた情報を返すように、各データベースに応じたSQLを記述します。

Spiderの基本構造 (2)

Networkは、そのネットワークに属するオブジェクト(KnowledgeとLink)について、Java標準のコレクションであるSetによるアクセス方法を提供します。しかし、場面によっては特定の条件を満たすオブジェクトのみを取得したい場合もあるので(例えば特定のプロパティを有するオブジェクトのみを取得するなど)、常にSetを用いてアクセスするのでは、データすべてを読み出したうえでフィルタリングしなければならず、非効率的です。

そのため、NetworkとSpiderは、特定の条件(SearchOption)を指定して検索できるようなメソッドを提供します。SearchOptionは用途に応じたサブクラスを提供しており、NetworkやSpiderは、指定された検索条件が実装するインターフェイスを調べることによって検索方法を判別します。これにより、Networkが現実に使用するデータベースの機能を用いるなどすることができ、アクセスの効率化が期待されます。

もっとも、現段階ではどのような検索条件が必要になるか明確ではないので、今後、具体的なアプリケーションを構築していくうえで問題が発生すれば、この仕様は変更されるかもしれません。

当初は、それぞれのNetworkが使用するデータベースとの接続オブジェクト(Connection)にはアクセスできないようにし、Network経由で接続を閉じるようなこともできないようにしようと考えていました。Networkがデータベース接続をラッピングするような役割を果たしているからです。Networkから実装側にアクセスできてしまうと、間違って接続を切断してしまうことも生じるのではないか、という不安もありました。

しかし、そのような方針では、アプリケーション側でConnectionを管理しなければなりません。すると、SpiderはNetworkを管理し、アプリケーションはConnectionを管理し、それらが整合するように調整する必要があります。これは手間ですし、バグを作り込む要因になってしまうので、Networkから実装側にアクセスする手段を提供することにしました。

Networkはインターフェイスですから、インスタンスを生成するためには実装クラスのコンストラクタを使う必要があります。しかし、それではアプリケーションとクラスライブラリが実装レベルで結びつくことになってしまい、仕様変更に強い設計になりません。そこで、Networkを構築するための情報として、NetworkSourceというインターフェイスと、NetworkFactoryというファクトリクラスを導入することにしました。

NetworkSourceはSerializableを拡張しており、シリアライズで永続化(保存)できることを想定しています。NetworkSourceのファクトリクラスも作れば完全に実装クラスとの結びつきを切断できるのですが、現段階ではファクトリのインターフェイスが定まっていないので、保留してあります。Networkのストレージとなるデータベースによって初期化に用いるパラメータが変わってくるので(例えばSQLiteではファイル名のみ、MySQLなどではデータベースのURLとユーザー名など)、そう簡単にはインターフェイス化できないのではないかと思っています。

Spiderの基本構造 (1)

作成中の知識整理用システムであるSpiderの構造について、書いていきたいと思います。

ドキュメント類はJavadocしかない状態なので、これがマニュアルに準じたものになればいいなあ、と思っています。たぶん、明日には、この目標を忘れているんでしょうけど。

Spiderシステムは次のモジュールから構成されます。

  • Spider : システムの中核
  • Network : 知識ネットワーク
  • Concept : 知識ネットワークを構成するオブジェクト
    • Knowledge : 文字列を表すオブジェクト
    • Link : オブジェクトとオブジェクトを関連づけるオブジェクト

情報は、多数のKnowledgeと、それらを結びつけるLinkによって構成されます。これはひとつのネットワークを形作り、それがNetworkに対応します。ネットワークを束ねるのがSpiderです。

Networkはひとつのデータベースに対応することが予定されています。つまり、Spiderは複数のデータベースに散らばった情報を統合するものでもあるのです(もちろん単独のNetwork、すなわちデータベースしか使わないこともできます)。

なぜこのような構造になっているのかを説明します。

まず、人の知識というものが、どのように表現できるか、というところが出発点です。ただ情報が散在しているだけでしょうか。それとも、階層構造になっているのでしょうか。どちらでもありません。たしかに、情報は断片として記憶されています。しかし、それぞれの情報は単独で意味をなすものではなく、他の情報と関連して意味をなします(例えば、「蜘蛛は縦糸を歩く」という知識はそれ単体で有用なものではなく「蜘蛛の横糸は粘着性があり縦糸にはない」という知識と関連することで意味の通るものとなる)。だからといって、あらゆる情報がなんらかの情報に包含されるという関係があるわけでもありません(仮にあるとすれば最も上にある概念は何になる?)。このような思考を経てたどり着いたのが、知識と知識が関連しあい、そこに上下関係などは必ずしも観念されないという、ネットワーク構造です。

Spiderは複数のNetworkを束ねるという役割を果たします。これによって、複数のNetworkをあたかも一つの大きなNetworkであるかのように扱うことができるようになります。しかし、べつに複数のNetworkを作らなくても、すべての要素をひとつのNetworkとして作り上げてしまえば済むようにも思われます。おそらく、そちらのほうが動作効率としてもいいでしょう。

Spiderが考えているのは、動的なNetworkと静的なNetworkの存在です。たとえば、学校で使う教科書を挙げてみます。教科書に書かれた内容は、すべての学生にとって同じものです。しかし、各人の教科書への書き込みや、細かな内容の取扱いは別々です。日本史が苦手な人は、日本の年表に「要チェック」のラベル付けをするでしょう。一方で、得意な人は「チェック不要」のラベル付けをするかもしれません。日本の年表は「静的なNetwork」、個々人の評価は「動的なNetwork」なのです。実際の運用では、教科書のデータを書き込み不可なデータベースに配置し、個々人のデータは読み書き可能なデータベースに配置することになると思われます。Spiderは、そのような事態に対応できるようにするため、複数のNetworkを束ねます。いまのところ、複雑な構造をサポートできるようにする必要性が感じられないので、フラットに束ねることしか考えていません。必要が出てくれば、さらに複雑な束ね方を検討することも考えられます。

Spiderに登場するモジュールは、これだけです。これらを複雑に組み合わせれば、難解な知識も表現できるものと期待しています。

知識整理アプリケーション

以前から、知識整理のためのアプリケーションの作成を進めています。

なかなか納得できるものを作り上げることができず、作っては壊してを繰り返していたのですが、ようやく納得できるものになろうとしています。まだデータベース部分しか作っておらず、エンジンは完成形に近づいているものの、タイヤやハンドルなど車の部分が出来上がっていないという状況なので、今後タイヤ部分などを作っていく最中に気に入らなくなってしまう可能性も残っているのですが。

明確なコンセプトは定めていないのですが、データベース部分については「なるべく制限を設けない」「シンプルなインターフェイス」という方針を採っています。ちなみに使用言語はJavaです。

データベース部分のコードネームは「Spider」。蜘蛛の網のような知識ネットワークの構築を目指して、このような名前を付けました。蜘蛛は興味深い生物で気に入っているという理由もあります。

現在作成中の実装はSQLiteをストレージとして使っているものですが、近い将来にはJavaDB(Derby)を使っていきたいと思っています。いつかは、Hibernateを使ってさまざまなデータベースに対応していきたいのですが…なかなか難しそうです。