LoginSignup
0
0

More than 1 year has passed since last update.

【オブジェクト指向設計実践ガイド】依存関係編

Last updated at Posted at 2023-01-03

はじめに

【オブジェクト指向設計実践ガイド】SRP編の続き。
今回は異なるオブジェクト間の依存関係をいい感じにする方法について考えていきます。

特定のオブジェクトのインスタンスに依存しないこと

特定のクラスのインスタンスに依存するコードの例を出してみましょう。人間を表すHumanクラスの他にTwitterのユーザを表すTwitterUserクラスを追加し、新たにHumanクラスに呟くメソッドを足してみましょう。

+ class TwitterUser
+     def tweet
+         # ... Twitterでツイートする実装
+     end
+ end

class Human
    # これはデータ
    attr_reader :weight, :height
    def initalizer(:weight, :height)
        @weight = weight
        @height = height
    end

    # ツイートするメソッド
+    def tweet
+        TwitterUser.new().tweet
+    end

    # 適正体重を求めるメソッド(= 適正体重を返すという振る舞い)
    def desirable_weight
        (height ^ 2) * 22
    end

    # BMIを求めるメソッド
    def bmi
        weight + (height ^ 2)
    end
end

さて、ツイートするメソッドがTwitterUserクラスにいて良いのかというのは一旦置いておきましょう。今問題なのは、HumanクラスからTwitterUserクラスという特定のクラスのインスタンスへの依存が生じている事です。以下のような依存と懸念があります。

  • tweetメソッドを持つクラスの名前が TwitterUser であることを Human が知っている。
    • もし TwitterUser にクラス名の変更が生じた場合に、呼び出し側も変更しないといけない
    • もし TwitterUser クラスを削除することになったら Human のtweetメソッドも対応を迫られる
  • HumanTwitterUser がtweetメソッドを持っていることを知っている
  • もし TwitterUser クラスがコンストラクタに引数を取るように変更されたら HumanTwitterUser.new() も対応しないといけない。
    • その場合、 HumanTwitterUser がどのような引数をどんな順番でコンストラクタに取るのかも知ることになる
    • TwitterUserの変更がしにくく、Humanクラスが弱くなっている。

そして何よりも 「TwitterUser以外でツイートすることができなくなっている」

...ということで特定のオブジェクトのインスタンスに依存するのはアンチパターンです。じゃあどうすりゃいいのか。

「メッセージ」こそが重要

オブジェクト同士が関わり合って何かする時に重要なのは、 「どのようなメッセージを送るか」 という事です。送り先が何のオブジェクトかということはどうでもいいのです。これを実践し、さっきの質問への答えとなるのが 「依存性注入」 です。やってみましょう。

 class TwitterUser
     def tweet
         # ... Twitterでツイートする実装
     end
 end

class Human
    attr_reader :sns
+    def initalizer(:weight,:height,:sns)
        @weight = weight
        @height = height
+       @sns = sns
    end

    # ツイートするメソッド
    def tweet
+        sns.tweet
    end
end

見やすいようにツイート関連以外のコードは消しました。そして、 Humantweet メソッドは tweet メソッドを持つような sns オブジェクトを呼び出すだけになっています。 Human クラスが依存しているオブジェクト(この場合は TwitterUser ) をコンストラクタで「注入」しているので依存性注入というのですね。僅かな変更ですが嬉しいことがあります。

  • Human はtweetメソッドを持つオブジェクトであれば、どんなオブジェクトを sns にとることもできる。
    • 例えば自前のSNSのツイート機能を Human クラスに組み込むことも今や簡単です。
    • そして Humansns に 「ツイートせよ」というメッセージを送るだけで良くなっており、より本質的なコードになっています。

依存は隔離せよ

さて、依存性注入の威力はわかりましたがいつもこう上手くできるとは限りません。特に、既にスパゲッティコードが大量に乗っているようなコードベースの場合、あるいは注入すべき上手い抽象が見つからない場合などにです。

しかし完璧に依存性注入を適用できなくても、コードをマシなものにすることはできます。 それが 「依存を隔離する」 ということです。
これを実践した例を先ほどの HumanTwitterUser で考えてみましょう。

 class TwitterUser
     def tweet
         # ... Twitterでツイートする実装
     end
 end

class Human
    attr_reader :sns
+    def initalizer(:weight,:height)
        @weight = weight
        @height = height
        # コンストラクタで初期化
+       @sns = TwitterUser.new()
    end

    # ツイートするメソッド
    def tweet
        sns.tweet
    end

    # あるいはこう書くこともできる
    def sns
        @sns ||= TwitterUser.new()
    end
end

Human のコンストラクタ内やメソッドで直接 TwitterUser を初期化しています。当然、 TwitterUser という特定のオブジェクトへの依存がHuman の中に生じることになるので、ベストなコードとは言えません。しかし、この記事の冒頭のコードに比べて改善したこともあります。

  • HumanTwitterUser に依存している問題がコード上で明確になった。
  • 今後リファクタリングできるようになった時には、 Human のコンストラクタだけを修正すれば良い

冒頭のコードではtweetメッセージを外部の TwittterUser に直接送っていましたが、メッセージを自身のインスタンス変数の読み取りメソッドに送るようにしたことによってオブジェクト間の結合が弱まっています!


しかしこれではまだ不十分です。このコードの場合は Human のtweetメソッドが TwitterUser にtweetメソッドがあることを知っています。
よりマシなコードにするのであれば、以下のように TwitterUser オブジェクトにメッセージを送っている部分をラップし、ラッパーを呼び出すようにします。こうすることで

  • TwitterUser のtweetメソッドの名前やシグネチャが変更された場合でも、影響はラッパー(この場合はtwitter_tweetメソッド)の中に収まります。
 class TwitterUser
     def tweet
         # ... Twitterでツイートする実装
     end
 end

class Human
    attr_reader :sns
    def initalizer(:weight,:height)
        @weight = weight
        @height = height
        # コンストラクタで初期化
       @sns = TwitterUser.new()
    end

    # ツイートするメソッド
    def tweet
+        twitter_tweet
    end

+    # TwitterUserをラップする
+    def twitter_tweet
+        sns.tweet
+    end
end

ということで依存性注入できないならせめて 「依存は隔離せよ」

引数やインデックスの順番に依存しないこと

さて、ここでは引数の順番に関する依存について改善していきましょう。引数を持つような関数を呼び出す側は、関数のシグネチャが変更されると常に対応を迫られます。これにはいくつかの対処法があります。

  • 引数をハッシュ(JSやPHPでいう連想配列、Pythonでいう辞書、Goでいうmap)で受け取る
    • あるいは必須の引数だけ普通に受け取り、オプショナルな引数をハッシュで受け取る
  • 引数にデフォルト値を設定しておく
  • 外部ライブラリのオブジェクトを初期化するなら、初期化だけのためのラッパー(いわゆるファクトリ)を実装する
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0