こんにちは、 ギフティでエンジニアをしている@aidyak_です。
はじめに
最近読み始めた本に「オブジェクト指向における再利用するためのデザインパターン」があります。
www.amazon.co.jp とても有名な本なので、読んだことがあるという方も多くいらっしゃるのではないかと思います。 この本を読み進めていく中で、しばしば出てくる「インターフェース」について考えてみたいと思うようになりました。 併せて、Rubyの言語仕様の1つである継承についても考えていきたいと思います。
それでは、よろしくお願いします!
インターフェースとは?
早速、インターフェースとはなんなのか、というところから触れていきたいと思います。 辞書や電子の海を漂ってみると、インターフェースという言葉は、「境界面」や「接地面」という意味を持つ、ということがわかりました。この境界面や接地面というのは、より具体的には機器と機器、機器とプログラムといった、2つの異なる物を仲介するための物を指します。
よりプログラマ的に馴染みのある概念だと、例えばバックエンドとフロントエンドという2つの物を仲介するためのAPI(Application Programming Interface)がそれに該当します。
『オブジェクト指向における再利用するためのデザインパターン』では、インターフェースに対してコードを書くことを推奨しています。ならば自分が普段使う言語でインターフェースを理解することはプログラミングを制する第一歩と言えるかもしれません。 早速制してみるか … と思ったのですが、私が普段使っている言語である Rubyでは言語仕様としてのインターフェースがありません。なんなら Matz こと まつもとゆきひろさんもこう仰っています。
Rubyでは、型やインターフェースはあなたの心の中にあるのです。
— Yukihiro Matz (@yukihiro_matz) November 26, 2018
まあ、抽象クラスやインターフェースの代わりにmoduleが使えますが、使いすぎるとRubyっぽくなくなるので、ほどほどに。
実装してみる
上記のように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
インターフェースとDocument2
、Document3
クラスを使って考えてみましょう。
Printable
インターフェースを実装していれば、新しいクラスを追加した際でもそのクラスがインターフェースに準拠することが保証されます。
つまり、Document2
、Document3
はprint
メソッドを持つことが保証されます。
使う側のメリットを考えてみます。
使用する側は、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という機能が導入されています。
rbsを使用することで、型定義情報を出力できます。少し逸れますが似たようなものにannotateがあったりもするので、この辺は徐々に充実してきているとも言えます。
参考資料
【図解で理解】Javaのinterfaceのメリットと使い方について
【C#】インターフェイスの利点が理解できない人は「インターフェイスには3つのタイプがある」ことを理解しよう
ここまでお読みいただきありがとうございました!ギフティでは気持ちの循環に寄り添うあなたのご応募をお待ちしております。気になる方はぜひカジュアル面談でお話しましょう!