はじめに
東京スタジオのエンジニアの飛田です。
以前、”脱オブジェクト指向初心者! ~「クリーンアーキテクチャ」を読む前に知っておきたい知識~”というタイトルで、この開発者ブログに投稿した記事があります。本稿は、その補遺という位置づけです。
「Rubyには言語仕様としてのインターフェースはないけれども、プログラミングをするときに考えることは、Javaなどとほとんど変わらない」ということをお話したいと思います。
そのことを理解できれば、多くの場合、上述の記事に書いたのと同じ考え方で、プログラミングできるし、クリーンアーキテクチャの考え方をRubyなどで適用する際に、非常に役立つはずです。
インターフェースとは
JavaやC#といった静的型付け言語、あるいは動的型付け言語の中でもPHPなどには、言語仕様としてインターフェースがある。しかし、RubyやPythonにはそれがない。また、世間一般に出回っている、オブジェクト指向プログラミングの参考書の多くは、Javaを例にして書かれている。
そのため、参考書に書かれている技法をRubyやPythonに適用しようしたときに、インターフェースがないからできない、と思ってしまうことがあるかもしれない。必ずしも、そんなことはない。インターフェースの本質は言語仕様ではないからだ。
では、その本質とはなにか。それは、「メソッドのシグネチャのセット」だ。実際に、Javaなどでinterfaceを定義するとき、記述するのは、名前を除けば、メソッドのシグネチャ群であることからも、そのことがわかる。
ここからは、Rubyのコードで例示しながら、その考え方でどのようにプログラミングを行うかを説明していく。
ダックタイピング
動的型付け言語における、インターフェースの考え方に関する言葉で、「ダックタイピング」というのがある。「アヒルのように鳴くものは、アヒルだと考えて良い」という意味合いでつけられている呼称だ。それだけでは、意味がわからないと思うので、コードを例示する。できるだけその言葉に忠実なものにしてみよう。
# アヒルクラス
class Duck
def quack(n)
n.times do
puts "quack!"
end
end
end
# アヒルっぽい鳥クラス
class DuckyBird
def quack(n)
n.times do
puts "QUACK!"
end
end
end
# アヒル小屋クラス
class DuckHouse
def initialize(ducks)
@ducks = ducks
end
# 開けると、中にいたやつがそれぞれ2回鳴いて出ていく
def open
@ducks.each do |duck|
duck.quack(2)
end
@ducks = nil
end
end
duck = Duck.new
ducky = DuckyBird.new
duck_house = DuckHouse.new([duck, ducky])
duck_house.open
こんな感じだろうか。
ここで重要なことは、DuckクラスとDuckyBirdクラスは別物だけれども、DuckHouseにまとめて入れても、問題なくプログラムが動作するということだ。
なぜ、問題がないかと言うと、DuckクラスとDuckyBirdクラスがともに、1つだけ(暗黙的に整数型の)引数を取るquack()メソッドを持っているからである。quack()メソッドの中身は、正しく動作する限りは、それぞれで異なってもよい。
つまり、DuckもDuckyBirdも”アヒルのように鳴く” = “DuckHouseが要求するシグネチャのメソッドを備えている”ので、その範囲内では、同じように扱っていいのだ。
私の頭の中
次は、静的型付け言語と比較したり、クラス図を使ったりして、もう少し整理して説明する。
先ほどのコードを、実際にそこに定義されているクラスだけで、クラス図として書き起こすと、以下のようになる。
しかし、先ほどのコードを書いたプログラマ(つまり私)の頭の中では、そうなってはいない。では、どうなっているか。答えを示す前に、少々まわりくどいが、一度、先ほどのコードをJavaに置き換えてみよう。
public interface IDuck
{
void quack(int n);
}
public class Duck implements IDuck
{
public void quack(int n)
{
for(int i = 0; i < 2; ++i)
{
System.out.println("quack!");
}
}
}
public class DuckyBird implements IDuck
{
public void quack(int n)
{
for(int i = 0; i < 2; ++i)
{
System.out.println("QUACK!");
}
}
}
public class DuckHouse
{
private IDuck[] ducks;
public DuckHouse(IDuck[] ducks)
{
this.ducks = ducks;
}
public void open()
{
for(IDuck duck : ducks)
{
duck.quack(2);
}
ducks = null;
}
}
public class Main
{
public static void main(String[] args) throws Exception
{
IDuck duck = new Duck();
IDuck duckyBird = new DuckyBird();
DuckHouse duckHouse = new DuckHouse(new IDuck[] { duck, duckyBird });
duckHouse.open();
}
}
さて、ここまでくれば、経験あるプログラマは、私の言わんとすることがわかっただろう。
RubyのコードをJavaに置き換えたら、IDuckインターフェースが現れた。Javaで同等のプログラムを書くためには、これはどうしても必要だ。のみならず、Rubyで書いている時点で、私はこのインターフェースを頭の中に思い浮かべて書いていた。
つまり、暗黙的に存在するインターフェースをはっきりと意識して、以下のようなクラス図で表される設計を、頭の中ではしていた。
おそらく、私でなくとも、経験あるRubyプログラマは、このような考え方をしているのではないだろうか。
まとめ
本稿では、Rubyを例に、”言語仕様としてのインターフェースが存在しない動的型付け言語”におけるインターフェースの考え方について説明した。最後に、インターフェースの本質を次のように表現して、まとめとしたい。
言語仕様としてのインターフェースはあくまで補助的な機能に過ぎず、ある場面で、どのようなシグネチャを持つメソッド群が要求されているのかを整理し、まとめ上げた概念こそが、真のインターフェースであり、それはプログラマの頭の中に存在しなければならない。
蛇足
Rubyにはインターフェースがないものの、その存在を意識させるために、以下のようなコードを書けないわけではない。
# インターフェースに近い役割を果たすクラス
class IDuck
def quack(n)
raise NotImplementedError
end
end
class Duck extends IDuck
def quack(n)
#省略
end
end
class DuckyBird extends IDuck
def quack(n)
#省略
end
end
しかしながら、Rubyの設計思想でもっとも重要視されているのは「楽しくプログラミングできる」ことであり、そのために、できるだけ余計なものは書かなくてもいいように設計されている。
そのことを思うと、このようなプログラムを書くのは野暮というものかもしれない。