14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyでインターフェイスの実装を強制させたい

Last updated at Posted at 2019-02-27

Rubyでインターフェイスの実装を強制させる方法を考えたいと思います。

Rubyは動的型付け言語なので、少なくともポリモーフィズムを実現するのに、Javaのようなインターフェイスを定義する必要はありません。しかし、「ソフトウェアのモジュールを疎結合に保つためには、モジュールは他のモジュールの実装ではなくインターフェイスに依存すべきである」という原則があります。そのような観点からは、インターフェイスを明示でき、それを利用するクラスがインターフェイスの規約をみたしているかチェックできると便利そうです(本当か?)。

以下に紹介する方法では一部メタプログラミングを用いていますが、一般的にメタプログラミングはプログラムの保守性や可読性を下げるため、多用すべきではありません。また、本稿の内容によって生じた問題に関して、筆者は責任を負いかねます。

とりあえず、メソッド呼び出し時に例外にしてみる

もっとも簡単な方法は、実装されていないメソッドが呼び出されたときに、例外とする方法でしょう。

sample.rb
module SomeInterface
  def some_method
    raise NotImplementedError, "Method \"#{__method__}\" is not implemented in class \"#{self.class.name}\"."
  end
end

class SomeClass
  include SomeInterface
end

obj = SomeClass.new
obj.some_method # => Method "some_method" is not implemented in class "SomeClass". (NotImplementedError)

この例では、SomeClassでsome_methodがオーバーライドされていなければ、親モジュールのsome_methodが呼び出されて、NotImplementedErrorが投げられます。

ただ、これだと実際にsome_methodが呼び出されないと、メソッドの未実装が検知できないので微妙です(そもそもこんなことしなくても、定義されていないメソッドを呼び出せばNoMethodErrorが投げられますし)。やはり、クラス定義時にメソッドの未実装を検知できるようにしたいものです。

メタプログラミングでなんとかしてみる

というわけで目標としては、attr_accessorやincludeのように、モジュール定義内で以下のように書くことで、メソッド未定義検知に必要な処理をするようにしたいです。

module SomeInterface
  interface :some_method
end

class SomeClass
  implements SomeInterface
end
# => Method "some_method" is not implemented in class "SomeClass". (NotImplementedError)

以下、これを実装するために必要な事項をかんたんに復習します。

モンキーパッチ

Rubyでは、実行時にクラスやモジュールの定義を、変更したり拡張したりできます。
たとえば、以下の例では組み込みのクラスHashに、Structへ変換する処理を追加しています。

hash_to_struct.rb
class Hash
  # 注意: キーと値が同一でも==による比較はfalseになる
  def to_struct
    new_keys = self.keys.map(&:to_sym)
    new_values = self.values
    Struct.new(*new_keys).new(*new_values)
  end
end

h = {'name': 'Taro', 'age': 22}
s = h.to_struct # => #<struct name="Taro", age=22>

なお、クラスやモジュールの上書きは、グローバルに影響するので注意が必要です。今回は簡単のためにこの方法で説明しますが、特別な理由がなければRefinementsを用いるのが好ましいです。Refinementsを使った場合、上書きの影響範囲はusingを用いたスコープ内に限定されます。

クラスもモジュールもオブジェクト

Rubyistには今さらな話ですが、Rubyではクラスやモジュールもオブジェクトです。たとえば、クラスは「Classクラス」のインスタンスであり、クラスFooの宣言は「定数FooにClassのインスタンスを代入している」に過ぎません。なので、以下の2つのコードは同じことをやっています。

class Foo
end
f = Foo.new
Foo = Class.new
f = Foo.new

attr_accessorやincludeはメソッド

これもRubyistには今さらな話ですが、attr_accessorやincludeなどはいかにも言語の予約語みたいな見た目ですが、単なるメソッドです。たとえば、attr_accessorはModuleクラスのインスタンスメソッドであり、引数で受け取ったシンボルと同名のインスタンス変数を返すメソッドを動的に定義します。

いざ、実装

上のコードを動作させるには、attr_accessorやincludeと同じくModuleクラスのインスタンスメソッドとして、「interface(*syms)」と「implements(*consts)」を実装すればいいようです。
あとは、実装させるべきメソッド名と、実際に実装されているメソッド名が取得できれば、目的のものが作れそうです。

実装させるべきメソッド名の一覧(上述の例におけるsome_method)は、具体的なモジュール(上述の例におけるSomeInterface)が有します。
具体的なモジュールは、Moduleクラスから見ると自身のインスタンスなので、実装されるべきメソッド名は、インスタンス変数または特異メソッドから取得できそうです。
インスタンス変数は外部からは直接参照できないので、今回はinterfaceメソッド呼び出し時に、実装されるべきメソッド一覧を返す特異メソッドinterface_methodsを動的に定義するようにします。

implementsの方は、実装すべきメソッド名がすべて、実際に実装されているメソッド名一覧に含まれているかチェックします。その後、includeを呼び出して、クラスの継承関係にインターフェイスモジュールを追加します。

というわけで、できたコードが以下です。

interface.rb
class Module
  def interface(*syms)
    define_singleton_method(:interface_methods) { syms }
  end

  def implements(*consts)
    methods_should_be_implemented = consts.map(&:interface_methods).reduce(:+).uniq
    methods_actually_implemented = self.instance_methods
    methods_should_be_implemented.each do |method|
      unless methods_actually_implemented.include?(method)
        error_str = "Method \"#{method}\" is not implemented in class \"#{self.name}\"."
        raise NotImplementedError, error_str
      end
    end
    include *consts
  end
end

実際に実行してみると、どうやら意図どおりに動くようです。

sample3.rb
require './interface.rb'

module Interface1
  interface :method_a, :method_b
end

module Interface2
  interface :method_c
end

class SomeClass
  def method_a
  end
  def method_c
  end

  implements Interface1, Interface2 # 仕様上どうしても、メソッド定義のあとに書かないといけない。
end
# => NoImplementedError: Method "method_b" is not implemented in class "SomeClass".

以上です。
今回は、最低限の機能だけ果たしそうなものを書いてみましたが、いろいろ懸念点や改善点がありそうです。たとえば、

  • 他のライブラリと名前が衝突しないか
  • パフォーマンスが悪化しないか
  • 引数の個数まで含めてチェックしたい場合はどうするか
  • implementsはメソッド定義の前に書きたい

等々。

ご意見・ご感想等ございましたら、コメントいただけると幸いです。

参考文献

有名すぎて今さらですが、非常に良い本です。「メタプログラミング」という語から、何やらマニアックなテクニックばかり載っているように思われるかも知れませんが、Rubyのオブジェクトモデルやブロックの説明などは、多くのRubyプログラマーの役に立つと思います。

14
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?