giftee Tech Blog

ギフティの開発を支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信しています。

Rubyで継承とインターフェースについて考えてみる

こんにちは、 ギフティでエンジニアをしている@aidyak_です。

はじめに

最近読み始めた本に「オブジェクト指向における再利用するためのデザインパターン」があります。

https://m.media-amazon.com/images/I/61zDpaqhh7L._SY522_.jpg

www.amazon.co.jp とても有名な本なので、読んだことがあるという方も多くいらっしゃるのではないかと思います。 この本を読み進めていく中で、しばしば出てくる「インターフェース」について考えてみたいと思うようになりました。 併せて、Rubyの言語仕様の1つである継承についても考えていきたいと思います。

それでは、よろしくお願いします!


インターフェースとは?

早速、インターフェースとはなんなのか、というところから触れていきたいと思います。 辞書や電子の海を漂ってみると、インターフェースという言葉は、「境界面」や「接地面」という意味を持つ、ということがわかりました。この境界面や接地面というのは、より具体的には機器と機器、機器とプログラムといった、2つの異なる物を仲介するための物を指します。

よりプログラマ的に馴染みのある概念だと、例えばバックエンドとフロントエンドという2つの物を仲介するためのAPI(Application Programming Interface)がそれに該当します。

『オブジェクト指向における再利用するためのデザインパターン』では、インターフェースに対してコードを書くことを推奨しています。ならば自分が普段使う言語でインターフェースを理解することはプログラミングを制する第一歩と言えるかもしれません。 早速制してみるか … と思ったのですが、私が普段使っている言語である Rubyでは言語仕様としてのインターフェースがありません。なんなら Matz こと まつもとゆきひろさんもこう仰っています。

実装してみる

上記のようにRubyでは、Javaのようなインターフェースのサポートがありません。そのため、独自にインターフェースを実装してやる必要があります。シンプルな方法としては以下のようなものが考えられます。

class Printable
  def print
    raise NotImplementedError, "You must implement the print method"
  end
end

class Document < Printable
  def initialize(content)
    @content = content
  end

  def print
    puts "Printing Document: #{@content}"
  end
end

ここで、インターフェースがあることによる嬉しいことをまとめてみます。

  • システム全体の一貫性を保つことができる
  • 依存関係を最小限に抑えることができる
  • 具体的な実装に依存しないので、変更や拡張が容易になる
    • テストの効率化や保守性の向上にも繋がる

上記の嬉しいことについて、PrintableインターフェースとDocument2Document3クラスを使って考えてみましょう。

Printableインターフェースを実装していれば、新しいクラスを追加した際でもそのクラスがインターフェースに準拠することが保証されます。 つまり、Document2Document3printメソッドを持つことが保証されます。

使う側のメリットを考えてみます。 使用する側は、Document2クラスの内容を知らずとも、Printableインターフェースを知っていればコードを書くことが出来ます。 Document3クラスを使用したいとなっても、そのクラスがPrintableインターフェースを知っていれば、インスタンスを生成する箇所以外は既存のコードを変更することなく実装することができます。 このようにインターフェースに依存することで、実装の詳細を隠蔽できるので、コード同士を疎結合にすることができます。

Documentクラス側も、Printableインターフェースさえ満たしていれば、内部を自由に修正できます。 また、類似クラスを実装する場合でも、満たすべき仕様が明確になっているという利点もあります。

その結果、既存のコードへの影響が最小限に抑えられ、保守性が向上します。

このように、インターフェースはクラス間の一貫性を保ちつつ、柔軟で拡張性のある設計を実現するための有効な手段と言えます。

実装に戻ってみましょう。 ベースにしたいクラスを定義してあげて、それに対して他のクラスから継承する形です。サクッとかけますが、この実装では多重継承ができないので他のクラスの機能も欲しい、となった時に拡張がしづらくなります。そこでこんな方法を採用してみます。

module Printable
  def print
    raise NotImplementedError, "You must implement the print method"
  end
end

class Document
  include Printable

  def initialize(content)
    @content = content
  end

  def print
    puts "Printing Document: #{@content}"
  end
end

コードをモジュールとして定義しておけば、クラスにそのモジュールをincludeできます。その際、複数のモジュールをincludeすれば、多重継承を実装することができます。 しかし、この実装をそのままインターフェースとして捉えると、効果が発揮されるのが限定的な気がしてきます。 例えば現状のままだと、実装されていないクラスの検知ができません。

なので、もう少し詳しく書いてみます。 先に概要を説明すると、

  • 契約を取りまとめる「インターフェース的なモジュール」を用意する
  • 同様に「インターフェース用モジュール」を用意して、どんなメソッドを実装する必要があるか、という契約を明示する
  • 実装するメソッドはデフォルトではNotImplementedErrorを発生させるが、「インターフェース用モジュール」をincludeしたクラスをオーバーライドする形で回避する

という流れになるようにします。

# 「インターフェース的なモジュール」を用意する
module RequiredMethods
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def requires(*method_names)
      method_names.each do |m|
        define_method(m) do
          raise NotImplementedError, "#{self.class} must implement ##{m}"
        end
      end
    end
  end
end

# 実際のインターフェース用モジュール(ここではPrintableインターフェース)
module Printable
  include RequiredMethods
  requires :print, :print_preview
end

# Printableを実装するクラス
class Document
  include Printable

  def initialize(content)
    @content = content
  end

  def print
    puts "Printing Document: #{@content}"
  end

  def print_preview
    puts "Previewing document..."
  end
end

少し改善されたかなと思います! これによって、宣言したメソッドが実装されていないクラスを検知することができます。

このように、モジュールを上手く使うことで、インターフェースに似たものは十分作れますし、 メタプログラミングを駆使すればさらに近づいたものを実装できるでしょう。

しかし、これも諸刃の剣と言えます。上記の通りRubyらしさは減るでしょうし、何よりコードベースが複雑になってしまうことも懸念されます。可読性を意識したコーディングが必要になってきそうです。この辺りは一種のトレードオフです。周りにとって優しいコードを書いていきたいところですね。

ちなみに

Rubyにインターフェースが導入されていない背景(恩恵に与れない点)の1つに型宣言をしないから、ということが挙げられます。これに関しては、近年の動向からこんな機能が導入されたりしています。例えばRuby3.0からは、rbsという機能が導入されています。

github.com

rbsを使用することで、型定義情報を出力できます。少し逸れますが似たようなものにannotateがあったりもするので、この辺は徐々に充実してきているとも言えます。

github.com

参考資料

Rubyでインターフェースクラスを実現する

RubyでJavaのインターフェースっぽいことをする

【図解で理解】Javaのinterfaceのメリットと使い方について

【C#】インターフェイスの利点が理解できない人は「インターフェイスには3つのタイプがある」ことを理解しよう


ここまでお読みいただきありがとうございました!ギフティでは気持ちの循環に寄り添うあなたのご応募をお待ちしております。気になる方はぜひカジュアル面談でお話しましょう!

giftee.co.jp