はじめに
東京スタジオのエンジニアの飛田です。
二十歳過ぎ頃にプログラミングを始め、いつの間にか十数年が経ちました。
オブジェクト指向言語を本格的に使うようになったのは、修士課程を出て、仕事に就いてからです。
実は数年前までは、インターフェースを使う意義がわからなかったし、テストコードも書いていなかったり、ずいぶんと未熟でした。
しかし、実務をこなしながら、余暇に参考書を読んでオブジェクト指向設計や単体テストについて、こつこつと勉強したら、段々と世間で言われているオブジェクト指向設計の原則の意味がわかってきました。今では、かなりオブジェクト指向プログラミングに自信を持っています。
というわけで、今回はなかなかオブジェクト指向初心者から抜け出せない方、すなわち、在りし日の自分に読ませたい、オブジェクト指向プログラミングの秘訣をまとめてみました。
本文は少々カタイ文面になってますが、内容はわかりやすくしたつもりなので、ぜひ読んでください。
関連記事: ダックタイピング、あるいは私の頭の中のインターフェース
キーワードは「依存関係」
オブジェクト指向プログラミングの三大要素「カプセル化」「継承」「ポリモーフィズム」だけでは、オブジェクト指向設計をきちんとするためには足りない。そこに欠けている、あるいは暗黙的に含まれているため、意識されにくい要素、それが「依存関係」だ。実は、依存関係を整理することこそ、オブジェクト指向設計の中核をなす重要な作業といえる。
本稿では、特に「依存関係の逆転」について、Javaのサンプルコードを使って説明する。ちなみに、Javaで例を示す理由は、「事前コンパイルが必要な静的型付け言語の代表である」「クラスの継承とインターフェースの実装を表すキーワードが別になっている」という点で説明がしやすいからだ。
依存とは
主な依存の形態
一般的なオブジェクト指向言語における主な依存関係の例として、以下のものが挙げられる。
- class X extends Y: 継承している
- class X implements Y: 実装している
- class X has Y: メンバ変数やプロパティとして持っている
- class X uses Y: それ以外のところで使っている
Javaのような言語では、XがYに依存していると、以下のような場合にXのコンパイルが通らないことになるので、依存関係を把握することは比較的簡単だ。
- そもそもYが定義されていない
- Yのコンパイルが通っていない
- XからYがアクセス不可能である(Yが他のクラスのprivateな内部クラスであるケースなど)
あるいは、「”Yがコンパイル可能かつXからアクセス可能”でないとXがコンパイルできない」ならば、「XはYに依存している」といってもいい。
クラス図による表現
また、上述の依存関係を表現し、他者と共有するためのツール(設計図)として、クラス図というものがある。このあとの説明に必要となるので、ここでクラス図を導入したいと思う。
クラス図のような設計図は、当事者間で意味が通じることが最重要で、細部の書き方にこだわる必要はない。ここでは、覚えるべきことを最小限にするため、継承、実装を表す線には一般的なUMLで使われる表記を用い、それ以外の依存関係はすべて通常の実線矢印で表すことにする。また、すべての依存関係について、横に日本語で具体的な関係を付記する。なお、クラス図には、クラス内の変数やメソッドを書くことができるが、今回は省略する。
以下に、上記の依存関係となる簡単なコードの例と、それがクラス図でどう表されるかを示す。
継承している
public class X extends Y
{
}
実装している
public class X implements Y
{
}
持っている
public class X
{
private Y y;
}
public class X
{
private Y[] ys;
}
など
使っている
public class X
{
public void someMethod(Y y)
{
}
}
など
依存関係を整理するための秘密道具
Dependency Injecton
Dependency Injection(DI)といえば、実務に放り込まれて、いきなりDIコンテナの勉強をする(させられる)ケースが多いかもしれないが、ここでは、より基礎的で本質的なDIの考え方について述べる。
さて、Dependency Injectionという英語はしばしば「依存性の注入」と訳されるが、それでは注入することで依存性が増えてしまうように感じられてしまい、あまり良い訳とは言えない。Dependency = 「あるクラスやオブジェクトが依存しているオブジェクト」、Injection =「外から与える」という意味と捉えるのが良いだろう。したがって、DIとは、「クラスやオブジェクトが依存している(必要とする)オブジェクトを外から与えること」を意味する。Dependency Injectionを日本語にするならば「依存オブジェクトの注入」といったところだろう。
ここで、一旦、以上のことをコードを例示して説明する。以下のコードは、”X has a Y”の依存関係にあるものを、DIしない場合とDIした場合の例である。
- DIしない場合
public class X
{
private Y y;
public X()
{
this.y = new Y();
}
}
public class Main
{
public static void main(String[] args)
{
X x = new X();
}
}
- DIする場合
public class X
{
private Y y;
public X(Y y)
{
this.y =y;
}
}
public class Main
{
public static void main(String[] args)
{
X x = new X(new Y());
}
}
おわかりいただけるだろうか。後者の場合では、Yのインスタンスをコンストラクタの引数で渡していることがDIに相当する。
この例は、コンストラクタで依存オブジェクトを与えたが、他にもDIの方法があるので、いくつか紹介しよう。
-
コンストラクタ引数による注入
先程の例で示した最も基本的な方法。必要なオブジェクトをコンストラクタを介して引数で与える。X has a Y / Ysの関係になっている場合に用いられる。コードの例はすでに示したので省略する。 -
セッターによる注入
コンストラクタによる初期化とは別に、セッターを定義し、必要なオブジェクトを与える。直接コンストラクタを呼び出せないが、has関係にするのが適切な場合に用いる。ただし、セットし忘れをコンパイルエラーで補足できないので、注意が必要。コンストラクタを呼び出せるならば、コンストラクタ引数による注入を選ぶべきである。
public class X
{
private Y y;
public X()
{
}
public SetY(Y y)
{
this.y = y;
}
}
public class Main
{
public static void main(String[] args)
{
X x = new X();
x.SetY(new Y());
}
}
- メソッド引数による注入
通常のメソッドの引数として、処理に必要なオブジェクトを与える。特定のメソッドがYに依存しているものの、has関係にするのが不適切な場合に用いる。
public class X
{
public X()
{
}
public String methodA(Y y)
{
return this.toString() + y.toString();
}
}
public class Main
{
public static void main(String[] args)
{
X x = new X();
String s = x.methodA(y);
}
}
さて、ここまでDIについて説明してきたが、一体、”何が嬉しいのか”がわからなかったのではないだろうか?実際のところ、DIしたとしても、具象クラスに依存していては、ほとんど意味がないのである。実は、DIとインターフェースを両方組み合わせて、初めて意味をなすのである。というわけで、次項からインターフェースの説明をしよう。
インターフェース
さきほども述べたとおり、依存オブジェクトを外から注入したとしても、具象クラスに依存していては、依存関係はまったく変わっていないので、恩恵は得られない。しかし、DIとインターフェースの両方を導入することで、あるクラスがインターフェースだけに依存し、実装クラスに依存しないようなコードを書くことができる。
public interface IY
{
}
public class Y implements IY
{
}
public class X
{
private IY y;
public X(IY y)
{
this.y = y;
}
}
このようなコードを書いた場合の依存関係をクラス図で表すと以下のようになる。
このとき、XはインターフェースIYのみに依存しており、Yには依存していない。
あるクラスに依存していないことを知るためには、Javaのような言語では、そのクラスが定義されてなくてもコンパイルが可能であることを確認すればよい。試しに、Yの定義を削除すると、次のようなコードになる。これはコンパイル可能である。
public interface IY
{
}
public class X
{
private IY y;
public X(IY y)
{
this.y = y;
}
}
なお、このようにX, Yがともに抽象(ここではインターフェース)に依存するようにすることを、一般に「依存関係の逆転」と呼ぶ。この状態(設計)では、クラス設計を行ったあと、インターフェースだけ定義すれば、X, Yを別々の担当者が実装し、各々がコードをコンパイル可能な状態で、レポジトリにコミットできる。これが”嬉しいこと”の一つである。また、Xが利用するIYを別の実装に差し替えたいときに、Xを変更する必要はない。これも”嬉しいこと”だ。このような種類の”嬉しいこと”は、一般的に「疎結合」という言葉で表現される。
ここまでの説明で、DIとインターフェースの両方を用いることで、依存関係を逆転できることがわかり、”嬉しいこと”がいくつか判明した。次に、単体テストとの関連において、”嬉しいこと”を説明したい。
単体テストとの関係
ここからは、「依存関係の逆転」を行うと、単体テストで”嬉しいこと”があるという説明をするとともに、もう一つ、重要なことを伝えたい。テスト駆動開発は設計の手法であると言われるが、闇雲にテストを書いても、良い設計にはならないということだ。そして、ここまでで述べたことを踏まえて、テストを書くことで、初めて良い設計と結びつくということだ。
ここで例として、プレーヤーには力(power)というパラメータがあり、サイコロを振って、1の目が出たら(力 x 2 )、それ以外の奇数の目が出たら(力 x 1)、それ以外の目が出たら、0のダメージを与えるというゲームを想定する。
まずは、サイコロを表すDieクラス(Diceは複数形で、その単数形がDie)と、Playerクラスを作ったとする。以下のクラス図とコードを見てみよう。
import java.util.Random;
public class Die
{
private Random rand = new Random();
public int roll()
{
return rand.nextInt(6) + 1;
}
}
public class Player
{
private int power;
public Player(int power)
{
this.power = power;
}
public int attack(Die die)
{
int eye = die.roll();
return calculateDamage(eye);
}
private int calculateDamage(int eye)
{
if(eye == 1)
{
return this.power * 2;
}
else if (eye % 2 == 1)
{
return this.power;
}
return 0;
}
}
この例では、Playerクラスのattack()メソッドは、サイコロを振って、出た目に応じた与ダメージ値を返す。さて、このメソッドのテストを書きたいとしよう。しかし、すぐに困ってしまうのではないだろうか。なぜなら、Dieクラスのroll()メソッドが、普通のサイコロと同様に、その都度、出る目が変わるため、テストを実行するたびに期待する答えの状態が変わってしまうからだ。
不完全ではあるものの、試しに、途中までテストを書いてみよう。
import static org.junit.Assert.*;
import org.junit.Test;
public class PlayerTest
{
@Test
public void attackメソッドのテスト() throws Exception
{
Player player = new Player(10);
Die die = new Die();
int actual = player.attack(die);
//実行するたびに値が変わるので、expectedを決められない
//int expected = ;
assertThat(actual, is(expected));
}
}
というわけで、コメントに書いたとおり、実行するたびに返り値が変わるため、期待する答えが定まらない。
ところで、依存関係を逆転させると、以下に示すようなクラス図とコードとなる。
import java.util.Random;
public interface IDie
{
int roll();
}
public class Die implements IDie
{
private Random rand = new Random();
public int roll()
{
return rand.nextInt(6) + 1;
}
}
public class Player
{
private int power;
public Player(int power)
{
this.power = power;
}
public int attack(IDie die)
{
int eye = die.roll();
return calculateDamage(eye);
}
private int calculateDamage(int eye)
{
//省略
}
}
このようにすると、”何が嬉しい”のだろう。それは、テストではDieクラスではなく、IDieを実装したMockクラスに差し替えることができることだ。Mockとは、テストなどで使われるDouble(影武者)の一種で、特に固定の値を返すようなものをいう。では、コンストラクタで与えられた目を必ず返すMockDieクラスを定義して、早速、テストを書いてみよう。
import static org.junit.Assert.*;
import org.junit.Test;
public class PlayerTest
{
private class MockDie implements IDie
{
private int eye;
public MockDie(int eye)
{
this.eye = eye;
}
public int roll()
{
return eye;
}
}
@Test
public void attackメソッドのテスト_1の目が出たとき() throws Exception
{
Player player = new Player(10);
IDie die = new MockDie(1);
int actual = player.attack(die);
int expected = 20;
assertThat(actual, is(expected));
}
//同様に2~6の目が出たときのテストを書ける
}
このように、Mockクラスを使ってテストが書けるようになって"嬉しい"というわけだ。
ところで、仮に10年前の自分が、テストファーストで書いたとして、この設計にたどり着くことができただろうか。答えはNOだ。やはり、ある程度、オブジェクト指向プログラミングの技法を知っている上で、テストを書くことで、このコードを発想できるのだ。
まとめ
そろそろ、まとめに入ろう。
今回は「依存関係の逆転」という原則を中心に据えて、依存関係を整理することで、疎結合かつ単体テスト可能な設計を導くことができることを示した。
その他にも、なにか設計に違和感を感じた場合に、クラス図を書いて依存関係を把握することをおすすめする。そうすれば、おかしな依存関係が可視化されて現れるはずだ。それは「車がタイヤを持つべきところを、タイヤが車を持っている」のような基本的な失敗かもしれないし、今回のように、抽象を一段挟むことで疎結合にできるという種類のものかもしれない。
いずれにしろ、依存関係を把握し、整理することは、設計において非常に重要な役割を果たす。これからは、「依存関係」というキーワードを念頭に置いて、自分の書いたコードを見直してみてはどうだろうか。