実用的なGroovy: MOPとミニ言語について

GroovyのおかげでMeta Object Protocolが研究室から皆さんのアプリケーションの中に

耳を地につけ、耳を澄ませてください。MOPが動き出しています。Meta Object Protocolを学びましょう。MOPは、アプリケーションや言語を構築するための、そしてアプリケーションを「言語として」構築するための古くて新しい手法なのです。

Andrew Glover, CTO, Vanward Technologies

Andrew GloverAndrew Gloverは合衆国ワシントン特別区にある、Vanward TechnologiesのCTO(最高技術責任者)です。Vanward Technologiesは自動化テスト・フレームワークの構築を専門としており、ソフトウェアのバグ発生数や統合時間やテスト時間の減少、また全体的なコード安定性改善に貢献しています。



2005年 9月 20日

Groovyプロジェクトのマネージャー、Guillaume Laforgeは最近のインタビューの中で、彼の好きなGroovyの機能は、MOPつまり『Meta Object Protocol』を実装していることだ、と言っています。このプロトコルを利用すると、実行時に、オブジェクトにメソッド(つまり『メッセージ』)が渡された場合、そのオブジェクトが、自分自身の状態や振る舞いに影響する特定な選択を行うようにできるのです。プロジェクト、PARC Software Design Areaのホームページ(参考文献)を引用すると、

メタオブジェクト・プロトコルの手法は、ユーザーが自分の特定な要求に合わせて設計や実装を調整できるように、言語を、開いたものにできる、開いたものにすべきである、という考えを元にしています。つまり、ユーザーが言語設計プロセスに参加することが奨励されているのです。

この心の広い提案は明らかに、より利口なアプリケーション、さらには『言語』まで構築できるという、素晴らしい可能性を示しています。今月のコラムでは、GroovyがMOPをどのように実装しているかを説明します。そして実例を使いながら、非常に実際的なアプリケーション、ミニ言語として機能する辞書アプリケーションを紹介します。

この辞書アプリケーション例のソースコードは、ダウンロード・セクションにあります。この記事で使用している例を追うためには、DbUnit(参考文献)をダウンロードする必要があります。

このシリーズについて

どのようなツールであれ、開発作業の中に採り入れるためには、どういう場合に使うべきか、または使うべきではないかをよく知る必要があります。スクリプト言語は非常に強力なツールですが、その強力さは、適切なシナリオで適切な使い方をした場合にのみ発揮されます。『実用的なGroovy』シリーズはそうした点を念頭に置き、Groovyの実用的な使い方に焦点を絞って、どういう場合に、どのように使うのかを解説して行きます。

このシリーズについて

どのようなツールであれ、開発作業の中に採り入れるためには、どういう場合に使うべきか、または使うべきではないかをよく知る必要があります。スクリプト言語は非常に強力なツールですが、その強力さは、適切なシナリオで適切な使い方をした場合にのみ発揮されます。『実用的なGroovy』シリーズはそうした点を念頭に置き、Groovyの実用的な使い方に焦点を絞って、どういう場合に、どのように使うのかを解説して行きます。

魔法のMOP

Meta Object ProtocolはGroovy独特のものではなく、Groovyを作った人達が発明したものでもありません。実際、元をたどってみると、LISPに、そしてAOPの背後にいる人達の一部につながっていることが分かります。そのつながりを知れば、Groovyを作った世界主義者達がMOPを称える理由も納得できるでしょう。

GoovyではMOPの実装が可能です。これは、『Groovyの地』では、すべてのオブジェクトが暗黙的にgroovy.lang.GroovyObjectを実装しており、これがinvokeMethod()とgetProperty()という、2つのメソッドを定義しているためです。実行時に、あるオブジェクト(クラスやその階層構造の中でプロパティーやメソッドとして存在していないもの)にメッセージが渡されると、getProperty()またはinvokeMethod()が呼ばれます。

リスト1では、Groovyクラス、MOPHandlerを定義しています。これはinvokeMethod()とgetProperty()を実装しています。いったんMOPHandlerのインスタンスを作れば、任意の数のメソッドまたはプロパティーを呼ぶことができ、このインスタンスが何を呼び出したかを説明するメッセージが出力されるのを見ることができます。

リスト1.  MOPが呼び出しに手を貸す
class MOPHandler {	
 
  def invokeMethod(String method, Object params) { 	
    println "MOPHandler was asked to invoke ${method}"
    if(params != null){
	 params.each{ println "\twith parameter ${it}" }
    }
  }
  
  def getProperty(String property){
     println "MOPHandler was asked for property ${property}"
  }  
}
def hndler = new MOPHandler()
hndler.helloWorld()
hndler.createUser("Joe", 18, new Date())
hndler.name

リスト1のコードを実行すると、リスト2に示すような出力が得られます。

リスト2.  私の言うことが信じられませんでしたか?
aglover@glove-ubutu:~/projects/groovy-mop$ groovy 
  ./src/groovy/com/vanward/groovy/MOPHandler1.groovy
MOPHandler was asked to invoke helloWorld
MOPHandler was asked to invoke createUser
        with parameter Joe
        with parameter 18
        with parameter Sun Sep 04 10:32:22 EDT 2005
MOPHandler was asked for property name

なかなかカッコ良い、と思いませんか。MOPはメッセージを渡すエラーを捉えるための安全策として非常にうまく動作しますが、それが最も興味深い機能なわけではありません。MOPの素晴らしさは、MOPを使って利口なオブジェクトを作る時に発揮されます。つまりMOPを使うと、渡される任意のメッセージを『汎用的な方法で』インテリジェントに応答するようなオブジェクトが作れるのです。


ミニ言語を作る

MOPを使ってできる面白いことの一つに、その領域専用の擬似的な言語、つまりミニ言語が作れることが挙げられます。これらのミニ言語は、特定な問題を解決することを目標とした、専用言語です。一般的に普及しているJavaやC#、あるいはGroovyなどの言語は、汎用言語として、どんな問題の解決にも使用できますが、ミニ言語は隙間が対象なのです。例が必要であれば、Unixの、古めかしいBashのようなシェルを考えてみてください。

ちょっと遊んでみると、実はMOPがよく分かると思います。このコラムのこれから先では、辞書アプリケーションを作ります。これは、実は自分自身のミニ言語なのです。このアプリケーションでは、辞書をクエリーするためのインターフェースを提供します。ユーザーはこれを利用して、新しい単語を作ることができ、与えられた単語の定義を得ることができ、与えられた単語の同意語を得ることができ、単語の品詞をクエリーすることができ、そして単語を削除することができます。表1は、このミニ言語を要約したものです。

表1. 辞書ミニ言語の意味構造

この辞書アプリケーション言語は、次のような意味構造を持っています(最も特徴的なものから一般的なものの順にリストアップしています)。

1. 品詞
単語の品詞をクエリーするには、メッセージをisで始め、その後に単語、その後にaまたはan、そして品詞を続けます。
2. 同義語
単語の同義語リストをクエリーするには、メッセージをsynonymsOfで始め、その後に単語を続けます。
3. 単語の削除
辞書から単語を削除するには、メッセージをremoveまたはdeleteで始め、その後に単語を続けます。
4. 単語を作る
単語の定義をクエリーするには、その単語をプロパティーまたはメソッドとして渡します。
5. 定義を検索する
辞書の中で新しい単語を作るには、その単語をメソッドとして渡し、そして単語の定義や品詞、オプションとしての同義語リストなどをパラメーターとして渡します。

辞書アプリケーション

この辞書アプリケーションは、図1に示す構造を持つテーブルを持ったデータベースの上に置かれます。いつもこのシリーズを読んでいる読者であれば、このテーブル構造は以前のコラム、「Groovy Builderでマークアップする」に出てきたことを覚えているでしょう。

図1.  辞書用の単純なデータベース・モデル
図1.  辞書用の単純なデータベース・モデル

注意: 定義の中の『EXAMPLE_SENTENCE』カラムは無視することにします。

ここでは、ユーザーとデータベースとの間のインターフェースとして動作する、単純なファサード(facade)を作ります。このファサードは、invokeMethodとgetPropertyの実装を提供することによって,GroovyのMOP機能を利用しています。invokeMethodメソッドはコマンドを判断し、対応する内部privateメソッドに責任を委任します。

先に進むために必要な事項

このアプリケーションはデータベースに依存しているので、これから先に進む前に、GroovyによるJDBCプログラミングを思い出す必要があるかも知れません。またこの記事では、ファサードに渡されるメッセージを判断するために、Groovyの正規表現(Feeling Groovyで紹介しました)も使っています。

このファサードを書きながら、Groovyを使ってファサードをテストします。ですから皆さんは、Groovyによるユニット・テスト機能を思い出す必要があるかも知れません。(これについてはGroovyを使って、より高速にJavaコードをユニット・テストするで取り上げました)

最後に、データベースの状態を管理するために、テストでは一貫してDbUnitを使っています。このシリーズではDbUnitについて書いたことはないので、先に進む前に、GroovyでDbUnitを使うための簡単な説明をしておきましょう。


DbUnitがGroovyと出会う

DbUnitはJUnitの拡張として、テスト実行の間、データベースを既知の状態に置きます。DbUnitにはテストケース内での委任を可能とするAPIがあるため、GroovyでDbUnitを使うのは驚くほど簡単です。DbUnitを使うためには、DbUnitにデータベース接続と、データベースのシード(seed)となるデータを含んだファイルを提供する必要があります。これらをJUnitのフィクスチャー機構(つまりsetUp())の中にプラグインすれば、準備完了です。リスト3は、辞書プログラム・アプリケーションの最初のテスト・クラスです。

リスト3.  辞書アプリケーションの開始テスト・クラス
package test.com.vanward.groovy

import com.vanward.groovy.SimpleDictionary
import groovy.util.GroovyTestCase
import java.io.File
import java.sql.Connection
import java.sql.DriverManager
import org.dbunit.database.DatabaseConnection
import org.dbunit.database.IDatabaseConnection
import org.dbunit.dataset.IDataSet
import org.dbunit.dataset.xml.FlatXmlDataSet
import org.dbunit.operation.DatabaseOperation

class DictionaryTest extends GroovyTestCase{  
   def dictionary
   
   void setUp() {        
      this.handleSetUpOperation()  
      dictionary = new SimpleDictionary()    
   } 
  
   def handleSetUpOperation() {
      def conn = this.getConnection()
      def data = this.getDataSet()       
      try{
          DatabaseOperation.CLEAN_INSERT.execute(conn, data)
      }finally{
          conn.close()
      }
    }
    
    def getDataSet()  {
      return new FlatXmlDataSet(new File("test/conf/words-seed.xml"))
    }
    
    def getConnection()  {
       Class.forName("org.gjt.mm.mysql.Driver")
       def jdbcConnection = DriverManager.
        	getConnection("jdbc:mysql://localhost/words", 
       	         "words", "words")             
       return new DatabaseConnection(jdbcConnection)
    }  
}

リスト3では、私が辞書アプリケーションを書く間のテスト・クラスとして動作する、クラス・シェルを作っています。テストが呼び出されると、JUnitはsetUp()を呼び、setUp()が今度はDbUnitのAPIを呼びます。DbUnitは、test/conf/words-seed. xmlというファイルの中にあるデータをデータベースに挿入します。test/conf/words-seed.xmlというファイルの内容をリスト4に示します。

リスト4.  サンプルのシード・ファイル
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
 <word WORD_ID="1" SPELLING="pugnacious" PART_OF_SPEECH="Adjective"/>   
 <definition DEFINITION_ID="10" 
             DEFINITION="Combative in nature; belligerent." 
             WORD_ID="1" />
 <synonym SYNONYM_ID="20" WORD_ID="1" SPELLING="belligerent"/>
 <synonym SYNONYM_ID="21" WORD_ID="1" SPELLING="aggressive"/>  
 <word WORD_ID="2" SPELLING="glib" PART_OF_SPEECH="Adjective"/>
 <definition DEFINITION_ID="11" 
     	       DEFINITION="Performed with a natural, offhand ease" 
             WORD_ID="2" />             
 <definition DEFINITION_ID="12" 
  	      DEFINITION="Marked by ease and fluency of speech or 
  	      writing that often suggests or stems from insincerity, 
  	      superficiality, or deceitfulness" 
         WORD_ID="2" />              
 <synonym SYNONYM_ID="30" WORD_ID="2" SPELLING="artful"/>
 <synonym SYNONYM_ID="31" WORD_ID="2" SPELLING="suave"/>  
 <synonym SYNONYM_ID="32" WORD_ID="2" SPELLING="insincere"/>  
 <synonym SYNONYM_ID="33" WORD_ID="2" SPELLING="urbane"/>  
 
 <word WORD_ID="3" SPELLING="rowel" PART_OF_SPEECH="Verb"/>  
 <definition DEFINITION_ID="50" 
             DEFINITION="to vec, trouble" 
             WORD_ID="13" />
</dataset>

このシード・ファイルのXML要素(リスト4の中に示されています)は、テーブル名と一致します。この要素の属性は、対応するテーブル・カラムと一致します。


ミニ言語を作る

テスト・クラスを定義したので、アプリケーションの開発(そしてテスト)を始めることができます。表1でリストアップした機能を、1つずつ攻めることにしましょう。

正規表現グループ

この辞書の例では、正規表現グループが大きな役割を果たします。Groovyでは、=~ syntax構文を使って、通常のJava Matcherインスタンスを作ることができます。Matcherインスタンスを使うと、group()メソッドを呼び出すことによって、String断片を得ることができます。正規表現内でグループを作るには、括弧を使います。例えば正規表現、(synonymsOf)(.*)では、2つのグループが作られます。1つのグループはString『synonymsOf』そのものに一致し、他方は『synonymsOf』に続く任意の文字に一致します。ただし、グループ値を得ようとする前に、まずMatcherのmatches()メソッドを呼び出す必要があることに注意してください。

1. 品詞

もしユーザーが、ある単語に対する品詞をクエリーしようとする場合には、そのユーザーは、isRowelAVerbやisEstivalAnAdjectiveのようなメッセージを渡します。(注意: ここではラクダ記法(camel-case semantics)に従っており、aとanの両方を許すことによって、適切な英語に従おうとしています。)質問に答えるためのインテリジェント・ロジックとしては、単に適切な単語を判断するだけのことです。最初の場合であれば『Rowel』であり、2番目の場合であれば『Estival』です。品詞も判断する必要があります。この例ではそれぞれ、『動詞』と『形容詞』となります。

正規表現を使えば、ロジックは些細なものになります。パターンは、is(.*)(An|A)(Verb|Adjective|Adverb|Noun)となります。私の関心は、最初と3番目のグループ(単語と品詞)だけです。このことから、品詞を検索して質問と答えを比較する、単純なデータベース・クエリーを書くことができます。これをリスト5に示します。

リスト5.  品詞を判断する
private determinePartOfSpeech(question){
  def matcher = question =~ 'is(.*)(An|A)(Verb|Adjective|Adverb|Noun)'
  matcher.matches()
  def word = matcher.group(1)
  def partOfSpeech = matcher.group(3)
  def row = sql.firstRow("select part_of_speech from word 
  	where spelling=?", [word])	
  return row[0] == partOfSpeech
}

リスト5は非常に単純に見えますが、これに対して幾つかのテストケースを書くとしたらどうなるかを見てみましょう。

リスト6.  単語の品詞判断をテストする
void testPartOfSpeechFalse() {
  def val = dictionary.isPugnaciousAVerb()
  assertFalse("pugnacious is not a verb", val)
}	
void testPartOfSpeechTrue() {
  def val = dictionary.isPugnaciousAnAdjective()
  assertTrue("pugnacious is an Adjective", val)
}

リスト4のXMLを再度見てください。どのテストも実行されないうちにデータベースを既知の状態にするためにDbUnitを利用しているので、単語『pugnacious』は、その品詞が『形容詞』に設定されており有効である、と考えることができます。リスト6では、2つの単純なテストをDictionaryTest(リスト3を見てください)に追加し、ロジックが正しく動作することを確認しています。

2. 同意語

同義語をクエリーするための意味構造パターンは、synonymsOfBloviateです。ここでは、同義語を求めるための単語(Bloviate)がsynonymsOfの後に続いています。正規表現では、さらに簡単に、(synonymsOf)(.*)です。必要な単語が見つかると、ロジックはデータベース・クエリーを行い、wordテーブルとsynonymテーブルを結合します。返される同義語はListに追加され、返されます。これをリスト7に示します。

リスト7.  getSynonymsの実装
private getSynonyms(question){
  def matcher = question =~ '(synonymsOf)(.*)'
  matcher.matches()
  def word = matcher.group(2).toLowerCase()
  def syns = []	
  sql.eachRow("select synonym.spelling from synonym, word " +
      "where synonym.word_id = word.word_id and " +
      "word.spelling = ?", [word]){ arow ->
          syns << arow.spelling
       }	   
  return syns		
}

リスト8に示すテストは、同義語を求めるための単語が正しく同義語を返すことを検証し、同義語のない単語(Rowel)は空のListを返すことを検証します。

リスト8.  そのメソッドをテストすることを忘れずに!
void testSynonymsForWord() {
  def val = dictionary.synonymsOfPugnacious()
  def expect = ["belligerent","aggressive"]
  assertEquals("should be: " + expect, expect, val)
}
void testNoSynonymsForWord() {
  def val = dictionary.synonymsOfRowel()
  def expect = []
  assertEquals("should be: " + expect, expect, val)
}

3. 単語の削除

リスト9の削除ロジックは、removeかdeleteのどちらかのコマンドの後に単語が続けば良いようにしているので、柔軟です。例えば、removeGlibもdeleteGlibも、どちらも有効なコマンドです。従って正規表現は(remove|delete)(. *)となり、ロジックはSQLのdeleteです。

リスト9.  単語をremoveする、またはdeleteする
private removeWord(word){
  def matcher = word =~ '(remove|delete)(.*)'
  matcher.matches()
  def wordToRemove = matcher.group(2).toLowerCase()	
  sql.execute("delete from word where spelling=?" , [wordToRemove])	
}

当然ですが、柔軟であるということは、単語削除のために少なくとも2つのテストケースを書く必要があるということを意味します。これをリスト10に示します。本当に単語が削除されたかどうかを検証するために、それらの単語の定義を検索し、何も返ってこないのを確認しています(5. 単語の定義を検索するを見てください)。

リスト10.  両方のケースをテストする
void testDeleteWord() {
  dictionary.deleteGlib()
  def val =  dictionary.glib()
  def expect = []
  assertEquals("should be: " + expect, expect, val)
}
void testRemoveWord() {
  dictionary.removePugnacious()
  def val =  dictionary.pugnacious()
  def expect = []
  assertEquals("should be: " + expect, expect, val)
}

4. 単語を作る

新しい単語を作るための意味構造は、どのクエリー・パターンにも一致しないメッセージであり、またパラメーター・リストを含んだメッセージです。例えば、echelon("Noun", ["a level within an organization"])のようなメッセージが、このパターンに一致します。単語は『echelon』であり、最初のパラメーターは品詞です。その後に、定義と、オプションとしての3番目のパラメーター(同義語のList)を含む、Listが続いています。

リスト11で示したように、データベースの中に新しい単語を作るのは、単に単語と定義を適当なテーブル(wordとdefinition)に挿入するだけのことです。また、もし同義語のListがある場合は、それぞれの同義語が追加されます。

リスト11.  単語を作る
private createWord(word, defsAndSyms){
  def wordId = id++
  def definitionId = wordId + 10
  sql.execute("insert into word 
  	(word_id, part_of_speech, spelling) values (?, ?, ?)" , 
    [wordId, defsAndSyms[0], word])
  for(definition in defsAndSyms[1]){	
    sql.execute("insert into definition 
    	(definition_id, definition, word_id) " +
      "values (?, ?, ?)" , [definitionId, definition, wordId])
    }
  //has a list of synonyms has been passed in
  if(defsAndSyms.length > 2){
    def synonyms = defsAndSyms[2]
    synonyms.each{ syn ->
      sql.execute("insert into synonym 
      	(synonym_id, word_id, spelling) values (?, ?, ?)" , 
        [id++, wordId, syn])
      }
    }
}

リスト11では、プライマリー・キー・ロジックは危険なほど単純です。ただし、幾つかのテストによって、これに自信を持つことができます。

リスト12.  単語作成ロジック用のテストケース
void testCreateWord() {	
  dictionary.bloviate("Verb", 
  	["To discourse at length in a pompous or boastful manner"], 
    ["orate", "gabble", "lecture"])
  def val = dictionary.bloviate()
  def expect = "To discourse at length in a pompous or boastful manner"
  assertEquals("should be: " + expect, expect, val[0])
}
void testCreateWordNoSynonyms() {	
  dictionary.echelon("Noun", ["a level within an organization"])
  def val = dictionary.echelon()
  def expect = "a level within an organization"
  assertEquals("should be: " + expect, expect, val[0])
}

今までと同じように、リスト12でも幾つかのテストケースを書いて、想定したように動作することを確認しています。単語を作った後、その単語に対するdictionaryインスタンスをクエリーし、その単語がデータベースの中に入ったことを確認します。

5. 単語の定義を検索する

辞書アプリケーションに渡されるメッセージのうち、これまでのクエリー・タイプ(品詞と同義語)のどれとも一致しないもの、また、パラメーターを何も含まないものは、すべて定義クエリーとみなされます。例えば.glibや.glib()のようなメッセージは、glibの定義を返します。

リスト13.  単語の定義を見つけるのは難しくありません!
private getDefinitions(word){	
  def definitions = []			   	  
  sql.eachRow("select definition.definition from definition, word " +
    "where definition.word_Id = word.word_id and " +
    "word.spelling = ?", [word]){ arow ->
      definitions << arow.definition
    }	   
  return definitions	
}

ここでは、メソッド・コールとプロパティー・コールのどちらでも定義クエリーとして動作するように許しているので、リスト14では少なくとも2つのテストを書く必要があります。リスト6の場合と同じく、『pugnacious』は既にデータベースの中にあると想定します。DbUnitは便利だと思いませんか。

リスト14.  メソッド・コールとプロパティー・コールの両方のケースをテストする
void testFindWord() {
  def val = dictionary.pugnacious()
  def expect = "Combative in nature; belligerent."
  assertEquals("should be: " + expect, expect, val[0])
}
void testFindWordAsProperty() {
  def val = dictionary.pugnacious
  def expect = "Combative in nature; belligerent."
  assertEquals("should be: " + expect, expect, val[0])
}

辞書アプリケーションの意味構造に関しては、これで終わりです。今度は、このアプリケーションの下にあるロジックを詳しく見てみましょう。


MOPのロジック

辞書アプリケーションの骨格部分は、invokeMethodとgetPropertyの実装の中にあります。これらのメソッドの中では、アプリケーションにメッセージが渡された時にどのように処理するかに関して、インテリジェントな判断を下す必要があります。どのような判断をしているかは、メッセージ・タイプを判断するためにStringオペレーションを実行する、一連の条件ステートメントの中に見ることができます。

リスト15の中で、唯一面倒な条件文は、最初の条件文です。これは、メッセージが本当に定義クエリーであること、そして他のもの(is. ..やremove...、delete...、またはsynonymOf...など)の組み合わせではないことを検証しています。

リスト15.  MOPのロジックの心臓部分
def invokeMethod(String methodName, Object params) { 	
  if(isGetDefinitions(methodName, params)){
    return getDefinitions(methodName)
  }else if (params.length > 0){	 
    createWord(methodName, params)
  }else if(methodName[0..1] == 'is'){
    return determinePartOfSpeech(methodName)
  }else if
  (methodName[0..5] == 'remove' || methodName[0..5] == 'delete'){	
  	 removeWord(methodName)
  }else if (methodName[0..9] == 'synonymsOf'){
    return getSynonyms(methodName)
  }
}
 
private isGetDefinitions(methodName, params){
  return !(params.length > 0) && 
    ( (methodName.length() <= 5 
    && methodName[0..1] != 'is' ) || 
    (methodName.length() <= 10 
    && isRemoveDeleteIs(methodName) ) || 
    (methodName.length() >= 10 
    && methodName[0..9] != 'synonymsOf' 
    && isRemoveDeleteIs(methodName)))	
}
 
private isRemoveDeleteIs(methodName){
	return (methodName[0..5] != 'remove' 
      && methodName[0..5] != 'delete' 
      && methodName[0..1] != 'is')
  }

isGetDefinitionsメソッドは、なかなかの仕事をしますが、一部はisRemoveDeleteIsに頼っています。いったんメッセージが定義クエリーには当てはまらないと判断されると、ロジックはずっと単純になります。

リスト16のgetPropertyメソッドは単純です。これは、ユーザーはプロパティーでしか定義をクエリーできない、と規定しておいたためです。ですから、getPropertyが呼び出された時には、getDefinitionsメソッドを呼びます。

リスト16.  おかげさまで、プロパティーは簡単です!
def getProperty(String property){
  return getDefinitions(property)
}

そして、本当にこれが全てです。リスト5からリスト16に示したロジックによって、ユーザーにとって非常に柔軟性があり、複雑さもそれほどではないアプリケーションができます。もちろん、先に触れたように、このアプリケーションでのプライマリー・キー・ロジックは完璧ではありません。これを改善するためには、データベース・シーケンスからHibernate(非常に優雅にIDを処理します)のようなフレームワークまで、様々なステップを選ぶことができます。

百聞は一見にしかず、です。リスト17は、辞書アプリケーションの言語が完全動作しているところです。(私がSAT(大学進学適性全国テスト)を受ける前に、こんな風なものがあれば良かったのですが!)

リスト17.  辞書の展示会
import com.vanward.groovy.SimpleDictionary

def dict = new SimpleDictionary()  

dict.vanward("Adjective", ["Being on, or towards the front"], 
	["Advanced"])
dict.pulchritude("Noun", ["Great physical beauty and appeal"])

dict.vanward             //prints "Being on, or towards the front"
dict.pulchritude()       //prints "Great physical beauty and appeal"

dict.synonymsOfVanward() //prints "Advanced"

dict.isVanwardANoun()    //prints "false"

dict.removeVanward()
dict.deletePulchritude()

MOCのポイント

このあたりで皆さんは、『何がポイントなのか?』と思っているかも知れません。確かに、MOCを使わなくても、privateメソッドを公開し、幾つかを調整し直し、通常の振る舞いをするアプリケーションを提供する(つまりgetDefinitionメソッドやcreateWordメソッドなどを公開する)ことも簡単にできたわけです。

この主張を検証してみましょう。例えば全く同じ辞書アプリケーションを、GroovyのMOC実装に頼らずに作るとしたらどうでしょう。そうすると、createWord()メソッド用のパラメーター・リストを再定義して、def createWord(word, partOfSpeech, defs, syns=[]){}のように見えるようにしなければなりません。

そこで、ご想像の通り、最初のステップとしてはprivate修飾子をやめてdefで置き換えますが、これはこのメソッドをpublicとして宣言するのと同じです。次に、パラメーターを定義し、最後のパラメーターをオプション(syns=[])とします。

また、deleteメソッドも定義する必要があります。このメソッドは、removeとdeleteの両方をサポートするために、少なくともremoveメソッドを呼ぶ必要があります。品詞クエリーも変更する必要があります。もう一つのパラメーターを追加するので、determinePartOfSpeech(word, partOfSpeech)のようなものになります。

つまり上のステップによって、辞書アプリケーションの概念的な複雑さを、MOP実装から静的APIに移すことができたわけです。しかしこれは、同じ疑問を提起します。つまり、そうすることのポイントは一体何なでしょう? MOPを実装すれば、アプリケーションのユーザーに対して比類ないほどの柔軟性を提供できます。そしてできあがるAPIは、静的に定義された一連のメソッドではなく、『言語』そのものと同じくらい柔軟なのです!


まとめ

皆さんの頭の中はグルグル回っているかも知れません。少し考えてみてください。もしユーザーが、自在にクエリーを作ることのできる辞書アプリケーションを作れるとしたら、どうでしょう。例えばfindAllWordsLikeGlibやfindAllWordsWithSynonymGlibなどといった意味構造が、MOP側では単純なテキスト構文解析にすぎなくなり、ユーザー側からはクエリーの宝庫となるのです。

さらに進めて考えてみてください。皆さんが、ビジネス的にもう少し使い道のある、別のミニ言語を作れるとしたらどうでしょう(例えば株取引を対象にしたミニ言語など)。さらに、このアプリケーションをコンソールで使いやすいように作るとしたらどうでしょう。そうするとユーザーは、スクリプトを書く代わりに、Groovyのシェルを使えるようになるのです。(私がBashに触れたことを思い出してください。)

もし皆さんが、この種の錬金術ユーティリティーはLISPの変人達がEmacsエクステンションの戯言を言うためのもの、と思っているのでしたら、ぜひ考え直してください。最近Ruby on Railsの導師達が、そのプラットフォームがなぜ素晴らしいかを騒々しく語り合っていますが、その下を覗くとMOPが使われているのです。そのおかげで、SQLに入り込まなくても命名パターン(naming pattern)を使いさえすれば、素敵なクエリーを作れるようになっているのです。さあ、戯言を言っているのは誰でしょう?


ダウンロード

内容ファイル名サイズ
Sample codej-groovy-mop.tar.gz5KB

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218884
ArticleTitle=実用的なGroovy: MOPとミニ言語について
publish-date=09202005