今回は、SOLIDの最後の依存性逆転の原則です。
これまた難しそうな名前ですね。
前回からの続きです。
https://qiita.com/F_Yoko/items/90b2b5766158c0736b46
依存性逆転の原則
これは「高レイヤのモジュールは低レイヤのモジュールに依存してはならず、高レイヤのモジュールも低レイヤのモジュールも抽象に依存すべきである」あるいは「抽象は実装である具象には依存してはならず、実装である具象は抽象に依存すべきである」というものです。
.
.
.
はい、意味不明です。
「困難は分割せよ」ということで分割してみましょう。
今回の説明で使われている言葉は以下が中心です。
- 上位モジュール/クラス:ツールを使って動作を実行するクラス
- 下位モジュール/クラス:動作を実行するために必要なツール
- 抽象化:2つのクラスをつなぐ仲介役
これをさらに分かりやすく(?)ドラえもんで説明すると、以下の関係となります。
上位レベルのモジュール=>ドラえもん
下位レベルのモジュール=>ドラえもんの部品
つまり、使う側(ドラえもん)と使われる側(部品)の関係です。
で、ここからが重要なんですが、この使う側であるドラえもんは部品と直接やり取りしちゃいけないそうです。
そのやり取りには、必ず 抽象化された方法 が必要なんだそうです。つまり、仲介役を置くわけです。
なんか昔の貴族の手紙のやり取りでいう従者みたいですね。(←)
なんでこんな面倒なことをするのかというと、
低レベルのモジュールが変更された場合でも、高レベルのモジュールに影響を与えず、抽象化に基づいてプログラムが構築されているため、柔軟性と保守性が向上するからです。
これは具体的にコードで見て見た方が早そうです。
依存性逆転の原則の原則に従っていない例
以下のコードは依存性逆転の原則に反しているコードです。この状況だと動くので、一見問題はなさそうに見えますが、今後の柔軟性を考えるとよろしくないコードとなっています。
class Doraemon
def initialize
@time_machine = TimeMachine.new
end
def use_gadget
@time_machine.use
end
end
class TimeMachine
def use
puts "Traveling through time"
end
end
doraemon = Doraemon.new
doraemon.use_gadget # "Traveling through time" を出力
このコードではDoraemonのコンストラクター内でTimeMachineをインスタンス化していますが、もし別のガジェットを使いたい場合は、Doraemonの記述を変更する必要があります。
例えば、DokodemoDoor
を追加したくなったとしましょう。
そうした場合に、Doraemonのコンストラクター内のインスタンスの記述を変更する必要が出てきます。
class Doraemon
def initialize
#以下の記述のせいで、新しいガジェットを追加するためには記述を変更しないといけない!面倒!
@time_machine = TimeMachine.new
@dokodemo_door = DokodemoDoor.new
end
また、TimeMachineに追加機能を追加したい場合も、Doraemonに依存しているため、Doraemonのコードを変更する必要があります。
要は、1つのクラスの中に具体的なクラスを混ぜてしまうとコードの柔軟性が失われてしまうわけです。
では、どうしたら良いのかというと次の依存性逆転の原則に従ったコードのように書けば良いです。
依存性逆転の原則に従っている例
このコードでは、高レベルの Doraemon クラスは低レベルの AnyGadget および TimeMachine クラスに直接依存することなく、Gadget という共通のインターフェースに依存しています。
このような設計により、より柔軟性の高いアプリケーションを構築できます。
具体的には、新しい Gadget の種類を簡単に追加できます。
例えば、RobotやJetPackなどの新しいガジェットを作成して、Gadgetインターフェースを実装するだけで、Doraemonクラスで使用することができます。これにより、アプリケーション全体を変更することなく、新しい機能を追加できます。
class Doraemon
def initialize(gadget)
@gadget = gadget
end
def use_gadget
@gadget.use
end
end
class AnyGadget
def use
puts "Using a gadget"
end
end
class TimeMachine
def use
puts "Traveling through time"
end
end
any_gadget = AnyGadget.new
doraemon = Doraemon.new(any_gadget)
doraemon.use_gadget # "Using a gadget" を出力
time_machine = TimeMachine.new
doraemon = Doraemon.new(time_machine)
doraemon.use_gadget # "Traveling through time" を出力
この場合、以下のようにコンストラクターでインスタンスを引数として取ることで、柔軟性のあるコードとなっています。このように書くことで、新しい機能を追加したくなった場合に、上位クラスを変更することなく追加できるようになります。
また、他のクラスにおいてuseメソッド
を共通化させているのも大きなポイントですね。
class Doraemon
def initialize(gadget)
@gadget = gadget
end
.
.
.
依存性逆転の原則で大切なのは、仲介役となる抽象化です。
このコードでその抽象化を可能にしているのは、以下の部分です。
-
Doraemonクラスのinitializeメソッドで、Gadgetオブジェクトを受け取り、それを@gadgetというインスタンス変数に格納しています。ここで、Gadgetオブジェクトは実際にはどんな種類のオブジェクトでも受け取ることができるため、抽象化されたインターフェースを提供しています。
-
GadgetクラスとそのサブクラスであるAnyGadgetクラスとTimeMachineクラスが、共通のインターフェースを持っている点も抽象化された部分です。具体的には、Gadgetクラスとそのサブクラスでuseメソッドが実装されていることが前提となっているため、このメソッドが存在することが共通のインターフェースとなっています。
まとめ
要は、役割ごとに分割して、それを外側で生成してからメイン部分に渡しましょうということです。そうすればコードの改修が非常に楽になります。