DI(依存性の注入)のパート2です。
依存性が強いために引き起こす問題の解決方法について、みてみたいと思います。
C#やJavaなどのオブジェクト指向の言語には、インターフェースという、メソッド名だけを定義して処理コードは記述しない、抽象的なクラス(のようなもの)が用意されています。
インターフェースのメソッドは、それを継承したクラスが処理を実装しなければなりません。
これからの理解でとても重要な点は、インターフェースを継承して実装したクラスのインスタンスは、インターフェース型の変数に代入できるということです。
インターフェース型の変数の中味(処理コード)は、実装したクラスのインスタンスになります。
このことは、インターフェース型の変数には、異なる実装をしたクラスのインスタンスも代入できるということを、意味します。
同じインターフェース型変数名であっても、その変数に代入した実装クラスのインスタンスが異なれば、それぞれの実装したインスタンスの挙動をしてくれる、訳です。
まず、前回紹介したHondaEngeneクラスのメソッド(startEngineメソッド、stopEngineメソッド)をインターフェースとして記述します。
インターフェース名をEngineと名付けました。
Engine.java public interface Engine { public void startEngine(); public void stopEngine(); } |
次に、HondaEngineクラスをEngineインターフェースを実装する記述に変更します。
(青字の部分を追加します。)
HondaEngine.java public class HondaEngine implements Engine { public void startEngine() { System.out.println(“ホンダスタート”); } public void stopEngine() { System.out.println(“ホンダストップ”); } } |
HondaEngineのメソッドを利用するCarクラスは、以下のように書き換えます。
(青字の部分を書き換えます。)
Car.java public class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void go() { engine.startEngine(); engine.stopEngine(); } } |
ここで注目してほしいのは、型宣言をインターフェース型のEngineにしていることです。
こうすることで、CarクラスにHondaEngineクラスの記述がなくなりました。
CarクラスはHondaEngineクラスが存在することが前提のクラスだったことを思い出してください。
インターフェース型のEngine型にすることで、仮にHondaEngineクラスが作成中であったりした場合でも、それを実装するクラスはテスト用のクラス、例えばTestEnjineなどとしておけばよいのです。
Carクラスは、これでHondaEngineクラスの依存から解放できました。
このように依存が弱まった関係を疎結合といいます。
続いて、Mainクラスを以下のように書き換えます。
(青字の部分を書き換えます。)
Main.java public class Main { public static void main(String[] args) { Engine engine1 = new HondaEngine(); Engine engine2 = new HondaEngine(); Engine engine3 = new HondaEngine(); Car car1 = new Car(engine1); Car car2 = new Car(engine2); Car car3 = new Car(engine3); car1.go(); car2.go(); car3.go(); } } |
Engine型(インターフェース型)に、HondaEngineクラスのインスタンスを代入しています。
このようにインターフェース型の変数に、インターフェースを実装したクラスのインスタンスを代入することを、注入といいます。
コード例では、HondaEngineクラスのインスタンスをEngine型の件数に代入(注入)しているので、あまり有難みがありませんが、次のようにして別々の実装クラスを代入(注入)することも可能です。
Engine engine1 = new HondaEngine();
Engine engine2 = new NissanEngine();
Engine engine3 = new ToyotaEngine();
なお、コード例では、HondaEngineクラスを指定してNewしているので、ここにも依存関係があります。
この依存関係の解決方法は、今までの方法と異なりますので、後述します。
いままでのことを、まとめると、
● インターフェース を作成し、メソッド名定義だけを行う。
● インターフェース を実装したクラスを作成する。
● 実装クラスのメソッドを利用するクラスは、実装クラスのインスタンスを インターフェース型 で定義する。
これにより、依存関係が 疎結合 になる。
● 利用するクラスは、インターフェース型の変数 に実装したインスタンスを代入する。
インターフェース型の変数 に実装したインスタンスを代入することを、注入 という。
● 注入した インターフェース型の変数 でメソッドを呼び出して、使用する。
● 同じ インターフェース 型の変数 に、実装が異なるクラスのインスタンスを 注入 することができる。
● これにより、同じ イ ンターフェース型の変数 でも、注入したインスタンスごとの異なる挙動を持たせることができる。
といったことかな、と思います。
要するに、依存性の注入とは、インターフェースを実装した依存性の低い “疎結合” のクラスを作成し、そのクラスを利用するときは、そのクラスのインスタンスをインターフェース型変数に注入(代入)して使うこと、と理解しました。
最後に、Mainクラスの以下のコードについてです。
Engine engine1 = new HondaEngine();
Engine engine2 = new HondaEngine();
Engine engine3 = new HondaEngine();
new HondaEngine();となっているということは、Mainクラスが動くためにはHondaEngineクラスが完成していることが前提となっています。
ここでも、依存関係が密結合になっています。
これを解決するためにFactoryクラスという、インスタンスを作成する専門のクラスを用意します。
以下がFactoryクラスのコード例です。
Factory.java public class Factory { public static Engine createHondaEngine() { return new HondaEngine(); } } |
注目したいのは、
public static Engine createHondaEngine()
の部分の青字の部分です。
Staticにすることで、FactoryクラスをNewしてインスタンス化しなくても、createHondaEngineメソッドを使用できます。
Engineとすることで、返り値をEngine型で返すことができます。
そして、Mainクラスの部分を次のように書き換えます。
Engine engine1 = Factory.createHondaEngine();
Engine engine2 = Factory.createHondaEngine();
Engine engine3 = Factory.createHondaEngine();
このようにすることで、MainクラスのHondaEngineクラスへの依存がなくなります。
Factoryクラスで、
return new HondaEngine();
としていますが、仮にHondaEngineクラスが開発中で使えないとすれば
return new TestEngine();
としておけばよいのです。
以上、DI(依存性の注入)について、理解したことをまとめてみました。
この理解を踏まえて、次回はDIコンテナについて掲載したいと思います。
徒然に思うこと
私がシステムに携わっていた20年以上まえは、モジュール化といって、機能別にプログラムを作成し、それを使い回すという時代でした。
その頃は、まだまだ、プログラム間の依存関係が高く、特にプログラム修正作業は大変でした。
今回、オブジェクト指向を学んでいくことを通して、「抽象化」について考えさせられました。
人の思考のプロセスも、実は、抽象化していくプロセスではないだろうか、と思ったりします。
しかし、いきなり抽象化したものを目の前に提示されても、理解できません。
具体的な事柄を数多く経験していく過程で、それらから共通する特徴などをつかみ取って、抽象化していくのではないでしょうか。
私のような凡人には、DI(依存性の注入)を理解するのに、たくさんの試行錯誤が必要でした。