BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Rubyのオープンクラス:猿のようにパッチを当てない方法

Rubyのオープンクラス:猿のようにパッチを当てない方法

ブックマーク

最近リリースされたRuby 1.8.7のプレビューリリースをウオッチしていたRails開発者はすぐに1.8.7プレビュー1に関してあることに気がつきました。それは、1.8.7プレビュー1がRailsを破壊してしまうということです。Railsが破壊されてしまう理由は、Symbol#to_procメソッドを追加したからです。それは、Ruby 1.9からバックポートされたものです。このメソッドを追加したことにより、よりコンパクトな方法でコードを書くことが出来ます。(Symbol#to_procの詳細(参考記事)を確認)

結果として何が起きたか?Railsはto_procメソッドをSymbolクラスに追加ずみでしたが、Ruby 1.8.7 Preview 1が追加したSymbol#to_procメソッドは、Railsが追加したSymbol#to_procと比較して若干異なる動きをします。
幸いなことに、Railsユーザは沢山いるので、問題はすぐに報告され、Ruby 1.8.7のファイナルバージョンでは、正しく動作するSymbol#to_procに修正されました。

問題

Rubyのオープンクラスは便利な機能で、ロード済みのクラスに対して簡単にメソッドを追加することが可能です。以下はロード済みのStringクラスに対して、fooメソッドを追加した例です。この例の様に簡単にメソッドを追加することが出来ます。

class String
 def foo
 "foo"
 end
end 
puts "".foo # prints "foo"


オープンクラスの問題は明白です。我々はソフトウェア設計の古くて議論の余地のない原則であるモジュラリティを使うことが可能です。長年に渡って、ものすごい数の概念が開発されてきました。それらの概念はいずれも増え続けるコードベースを考慮したモジュラリティを得るために開発されたものです。ローカル変数対グローバル変数、レキシカルスコープ対動的スコープ、多数の名前空間システムなどなど。これはまだ進行中のプロセスです。コンポーネント指向を行うという発展中の考えと、物理的なコンポーネントを組み立てて製品を作るのと同様に、ソフトウェアはコンポーネント化可能だという考えを考慮してください。モジュラリティは、我々が理解しているように、大切なソフトウェア資産です。

従って、オープンクラスとオープンClassesと自由の効くMonkeypatching(特にPythonコミュニティで、この機能が知られているように)に対して反対意見を述べているのがモジュラリティなのです。ライブラリ開発者であれば誰でも既存のクラスに手を加えるが、彼らは次の質問に答える必要がある。その質問とは、追加したこのメソッドはこのクラスに絶対必要なものであり、追加したことによってモジュラリティを壊してしまで追加する必要があるということを。この問題を繰り返し扱ってみましょう。

  • サードパーティ製のライブラリとの名前衝突とインタラクション
    クライアントプログラムは大抵、一つのライブラリのみに依存していません。追加されたライブラリは、他のライブラリが既に修正されたクラスに対して何かを追加し、名前の衝突を起こすという可能性を高めます。たとえそれが何かの理由で適当だとしても、Ruby標準ライブラリの基本的なクラスをオープンすると、間違いなく問題が起きます。解決策の中には、Objectをオープンする必要があります。その場合、全てのサブクラスは追加したメソッドを持つことになります。これは他のMonkey Patchingされたライブラリと衝突してしまうことよりも大きい問題になります。なぜか?理由はシステムの全てのクラスは、Objectクラスを継承しているからです。従って追加したメソッドは、全クラスの全名前空間に存在してしまいます。従って、追加したメソッドにSHA-1のようなハッシュ値を生成するしくみがないと、他のメソッドと衝突してしまうことがあります。
  • 将来のRubyバージョンとstdlibsとの互換性
    最近注目を浴びた例は、Symbol#to_procメソッドです。ある特定の操作に対して特別簡潔な構文を考慮するために、ライブラリはSymbol#to_procメソッドを追加しました。Ruby 1.9では、これは標準的なRuby stdlibのSymbolクラスに追加されました。このことは、他のソースで名前が衝突する可能性があることを示唆します。もし名前が十分一般的なら、将来リリースされるRubyで、その名前のメソッドが含まれる可能性があります。そのメソッドがまったく同じ振る舞いをするのであれば問題はないが、そうでないとしたら問題です。メソッドが同じ振る舞いをしない場合メソッドの再定義を行うと、システム即ち標準的なRubyライブラリの振る舞いに依存しているRubyのstdlibとそれを利用している全ての箇所が壊れる可能性があります。

設計でオープンクラスを避ける方法

クラスをオープンする理由の一つは、クラスサポート、プロトコル、またはインタフェースのオブジェクトを作ることです。例えば、messages/methosのセットです。コンテクスト内の用語プロトコルに関する長い説明(参考記事・英語)を読んでください。これを実現する代わりとなる解決策があります。

Adapters

Adapterパターンを使うと、与えられたあるオブジェクトXは、オブジェクトXの代わりとして振る舞うプロトコルをサポートしているオブジェクトをルックアップ出来ます。

Adapterパターンを取り入れた広く知れたアプリケーションの例を、Eclipseの中に見つけることが可能です。Adapterパターンは、Eclipseプラットフォームを拡張可能で且つモジュール化するのに役立っています。Adapterパターンの使用例:以下はエディタ用のアウトラインGUIを取得する例です。

OutlinePage p = editor.getAdapter(OutlinePage.class); 

editorオブジェクトのクラスは、エディタのコンテンツに対して、アウトラインをどのように表示すべきかを知っているOutlinePageオブジェクトを直接返すことが出来ます。この特定のeditorがアウトライン機能を実装していないならば、getAdapterメソッドのプロトコルは、中心にあるルックアップシステムにコールをフォワードするように提案します。それは我々に拡張性とモジュール性をもたらしてくれます。たとえベンダーの開発者がアウトラインGUIを提供してくれなくても、他のEclipseプラグインが、それを提供することが出来ます。アダプターパターンの長所:機能を追加するのにドメインクラスの修正が必要ないところがアダプターパターンの長所です。アダプターのロジックは望んだインタフェースから元のオブジェクトへ適応するロジックを全て含みます。インタフェースに対して直交する変更を行えます。大きな変更は必要ないです。アダプターパターンを取り入れたEclipseのバージョンに関してより多くの情報を知りたい場合は、Alex Blewitt氏による「IAdapterとは何か?」(リンク)を読んでください。

動的言語におけるアダプターパターンの使用例はZOPEに由来します(編集部注:Zopeは、Pythonで書かれた、オープンソースのWebアプリケーション・サーバです)。「アヒルのようにアルクためにGrokを使う」(リンク)という彼のプレゼンテーションの中で、Brandon Craig Rhodes氏は何年にも渡ってZOPEを構築した経験を説明しています。アヒルではないオブジェクトをアヒルのように振る舞わせる方法に対する異なるアプローチに関して長所と短所を調査しています。この解決策は、アダプターを定義して提供する幾つかの方法を説明しています。

これらのアダプターの実装は、小さなアプリケーションではやり過ぎのように思えるかもしれませんが。アダプターによってアプリケーションはモジュール化が保たれます。オープンクラスとは異なります。なぜならリターンされたアダプターは、適応されたオブジェクトと必ずしも同じオブジェクトではないからです。オープンクラスまたはシングルトンクラス(シングルトンクラスに関しては次の節を参照)を使うことにより、特定の型の全てのオブジェクトに対して振る舞いを追加することが可能です。それが不可欠な機能かどうかは、あなたのアプリケーション次第です。

シングルトンクラス

Rubyでは、シングルトンクラスという新しいクラスをオブジェクトの元のクラスから作ることによって、ある一つの特定のオブジェクトが所属するクラスを修正することが可能です。以下がその方法です。

a = "Hello"
def a.foo
 "foo"
end
a.foo  # returns "foo"

これらの変更を行ったことにより影響を受けるのは変更を行ったオブジェクトのみであり、他のクラスやオブジェクトは影響を受けません。どのような場面でシングルトンクラスを使用すれば良いかに関して、より多くの情報と使用例を知りたい場合は、「オブジェクト・メタデータにシングルトンクラスを使う」(参考記事)というInfoQの記事を読んでください。

安全にオープンクラスを使う方法

もしあなたが本当にオープンクラスを使う必要があるなら、リスクを減らすコツがあります。Jay Fields氏はメソッドをクラスに追加する異なる方法(リンク)をリストアップしています。その解決方法とは以下の通りです。

  • alias を使う。
  • alias_method_chain を使う。
  • アンバウンドメソッドをまとめる。
  • メソッドを再定義し、superを使用するモジュールを拡張する。

それぞれの解決法の詳細に関しては、Jay氏の記事を読んでください(リンク)

最終的にクラス拡張を一つの場所に集めておいてください。、例えばそれら全てをextensions.rbというファイルに置いておくなど。この規約を守ることにより、メソッドがどこで定義されているかを教えてくれる特別なIDEやクラスブラウザが無くても、コードを読んでいる誰に対しても、全ての拡張がすぐに見えるようになります。それは同様にどんなクラスが影響を受けるかというドキュメントにもなります。

Rubyや他言語においてオープンクラスへの安全なアプローチ

既存のクラスを拡張するアイデアは、Ruby固有のものではありません。他の言語も似たような機能のサポートを行っており、それらは、グローバルな名前空間を汚さないような解決策を見つけています。

一つのコンセプトは、Classboxes(リンク)と呼ばれます。実装は、Squeak Smalltalk、Javaまたは.NETで利用することが可能です(リンク)。基本的な考え方は以下の通りです。

古典的なモジュールシステムは、アプリケーションのモジュール開発をサポートしており、モジュールに定義されていないクラスにメソッドを追加したり置き換えたりする能力を欠いています。しかしメソッドの追加や置き換えをサポートしている言語は、アプリケーションのモジュール化された視点を影響しません。それらの変更はグローバルなインパクトを持っています。結果は、オブジェクト指向言語のモジュールシステムとメソッドの追加や置き換え機能という望まれた機能の間にあるギャップです。

我々が公開したclassboxesの問題を解決するために、オブジェクト指向言語のモジュールシステムは、メソッドの追加や置き換えが出来ます。さらに、classboxによって生成された変更は、そのclassboxに見えるというだけです。(インポートしているclassboxには見えない)我々はローカルでリバンディングしています。


C#の拡張メソッド(リンク)は、この問題に対して異なるアプローチを提供しています。Rubyのオープンクラスとは異なり、拡張したメソッドは実際のクラスに影響を与えません。代わりに、それらは、拡張メソッドを定義しているソースに対してのみ可視的であり、コンパイラにとってそれは全て実装されています。リンクした記事からの例を見てみましょう。

public static int WordCount(this String str) 

上の例の様に、このメソッドはオブジェクトへのポインタを取得して、動作しています。拡張を可視的にするために以下の1行を追加します。

using ExtensionMethods; 

そしてあなたは新しいメソッドを使うことが可能です。

string s = "Hello Extension Methods";
int i = s.WordCount(); 

このアプローチの利点:拡張したメソッドは、はっきりとインポートしたコードの中でのみ可視的です。

Monkey Patchingやオープンクラスに関して議論を行ったことにより、回避方法を試みるようになりました。coderrによる回避方法(リンク)は、拡張をローカル内に保つコンテクストでコードをラップすることです。この解決方法に対する一つの問題は、Rubyのネイティブ拡張を使う必要があり、それを使ってRubyインタプリタへ接続することです。それはRubyInline(リンク)を使用しています。動作するCのコードを見つけるためにinlineメソッドのコールを探してください。

異なるアプローチは、Reginald Braithwaite氏(リンク)のgemのRewrite(リンク)です。それはParseTreeを使って、RubyのコードのためにASTを使用し、これを使って追加されたメソッドを可視化しています。InfoQは以前Rewrite gemに関して議論していました(参考記事)。Rewriteのgemは同様にネイティブな拡張に依存しています。(この場合はParseTreeを使うなど) 

結論

注意深くオープンクラスを使わないと、オープンクラスの機能がいろいろと問題を引き起こし得ることを我々は見てきました。オープンクラスは、for-loop、ダイナミックメモリーアロケーションそして多くの言語の機能と運命を共有しています。もちろん強調しているところもあります。名前衝突のような問題が本当に実世界で起こるのかということを見てきました。このことを考慮して、我々はアダプターパターンを使用して既存のクラスを修正する代わりとなる解決策を見てきました。そしてもしそれを選択しない場合は、出来るだけ安全にオープンクラスを使う方法を見てきました。最後にオープンクラスに対して煩わしくない解決策が、Rubyの次期バージョンでは考慮されています。それらがどのようになるかを予想するために、我々は他の言語、例えばClassboxes、C#の拡張メソッドなどの解決策を見てきました。

 

原文はこちらです:http://www.infoq.com/articles/ruby-open-classes-monkeypatching
(このArticleは2008年7月11日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT