LoginSignup
321
252

More than 1 year has passed since last update.

「DI使うとインタフェース地獄に陥るらしいから使いたくない」と言っていたA氏がインタフェースを使わずにDIで幸せになるまで

Last updated at Posted at 2021-10-08

DIはインタフェース定義しなくても十分実用的だし、むしろそっちの方が本質だよ、という話をします。C#や.NETを使っていますが、それに限らず普遍的な内容です。

インタフェースと実装に分けるとか無理。DIなど不要!

中堅社員のA氏は、「DIっていちいち実装とインタフェース分けないとダメなんでしょ?。さすがにやってられんわ」と言って頑なにDIを導入しようとしません。

DIはテスタビリティと併せて語られることが多かった為か、A氏は「注入するクラスは基本的にインタフェース定義しましょう」という記事ばかりを読んでいたのです。

インタフェースと実装を分けるとは、例えば次のような事です。

services.AddScoped<IMessageStore, MessageStore>();

public interface IMessageStore 
{
    string GetMessage(string id);
}

public class MessageStore : IMessageStore 
{
    public string GetMessage(string id) => id switch
    {
        "M001" => "message 1.",
        "M002" => "message 2.",
        "M003" => "message 3.",
        _ => "default message",
    }
}

こうすることで、注入される側が依存オブジェクトの実装に依存しなくなり、例えばテスト時には別のインスタンスを用いてテストが行える、というようなメリットが語られます。

class MyController : Controller
{
    IMessageStore _messageStore;

    public MyController(IMessageStore messageStore)
    {
        _messageStore = messageStore;
    }

    public IActionResult GetMessage(id)
    {
        return View(_messageStore.GetMessage(id));
    }
}
本番用
services.AddScoped<IMessageStore, MessageStore>();
テスト用
services.AddScoped<IMessageStore, TestMessageStore>();

しかし、A氏はこう反論します。

「実際にはいちいちこんなモックを使ってテストを行うのは稀。このためにDIを導入するなんて過剰仕様だ」

A氏はDIのない世界を選んだ

A氏は、

「今回インタフェース定義もしていないし、DIしたとしても結局そのクラスは依存オブジェクトの実装クラスに依存したまま。だったら内部で直接newしても同じだろう」

と考え、DIなど使わなくてよいと判断しました。実は単にDIを使ったことがなく、面倒にしか感じていなかっただけなのですが…。

DIしていないMyControllerのコンストラクタ
    public MyController()
    {
        // MessageStoreなんてここでnewしてやるぜ!
        _messageStore = new MessageStore();
    }

コンストラクタの中で直接MessageStoreをnewしています。

A氏は「これで全然問題がない」と思いました。

立ち込める暗雲

ここでMessageStoreが、ハードコーディングの文字列ではなくDBからのデータ取得に変更になってしまいました。結果として、MessageStoreのコンストラクタにMyDbContextを渡すよう、仕様変更が行われます。

変更前
var store = new MessageStore();
変更後
var store = new MessageStore(dbcontext);

MessageStoreをnewしている箇所は全て、書き変えなくてはならなくなりました。
それどころか、各所でMyDbContextをどうにかして用意しなくてはならなくなったのです。

幸いにも、MyDbContextはいつでもどこでも自由にnewして大丈夫なオブジェクトでした。

A氏は淡々と、MessageStoreをnewしている箇所を変更していきます。

DIする前のMyControllerのコンストラクタ
    public MyController()
    {
        // MessageStore用のMyDbContextをここでnewしてやるぜ!
        var dbcontext = new MyDbContext();

        // MessageStoreなんてここでnewしてやるぜ!
        _messageStore = new MessageStore(dbcontext);
    }

ところが、MyDbContextの下に赤い破線が出てコンパイルが通りません。
コンパイラが「こんな型知らないんですけど」と言っています。

それもそのはず、このシステムはN層システムであり、MyControllerを定義しているController層からは、MyDbContextが定義してあるDB層には直接触れないようになっていたのです。

A氏は以下の提案をすることになりました。

(a) MessageStoreコンストラクタの中でMyDbContextを生成して使う

そうすれば、MessageStoreのコンストラクタからMyDbContextは消え、元のシンプルな状態に戻ります。

public class MessageStore
{
    private MyDbContext _dbcontext;
    public MessageStore()
    {
        _dbcontext = new MyDbContext();
    }

    ...
}

しかしこれはMessageStoreの開発責任者であるM氏に却下されました。
MessageStoreの内部でMyDbContextを生成してしまうと、その破棄責任を負う事になるから困る、というのです。

M「じゃあMessageStoreをIDisposableにしてDispose()の中で_dbcontext.Dispose()をするようにするから、MessageStoreの利用者もちゃんとMessageStoreをDispose()してくれるか?」

M氏にそういわれたA氏は、「なんかそれは面倒だな」と思い諦めました。

(b) MessageStoreのGetMessageの中で毎回MyDbContextを生成して使う

それではと、A氏はM氏に

「MessageStoreのコンストラクタでMyDbContextを生成するのではなく、GetMessageの度に都度、新しいMyDbContextを生成してすぐ破棄すればいいのではないか」

と提案してみました。

public class MessageStore
{
    public string GetMessage(string id)
    {
        var dbcontext = new MyDbContext();
        return (
            from msg in dbcontext.Messages
            where msg.Id == id
            select Message
        ).FirstOrDefault();
    }
}

しかしこれも、「メッセージを1つ取得する為だけに毎回MyDbContextを取得するなんて」とM氏に怒られました。

A氏は「すぐ使い終わるんだし、どうせ裏側では接続プール使いまわされてるんだからいいのに…」と思いましたが、確かに嫌がる気持ちもわかるなと思い黙っていました。

(c) Controller層から直接DB層を参照するよう変更する

こうなったら最後の手段です。
Controllerから直接MyDbContextを参照できるようにパッケージ間の依存関係を変えてしまうのです。

それなら、これができるようになります。

DIする前のMyControllerのコンストラクタ
    public MyController()
    {
        // MessageStore用のMyDbContextをここでnewしてやるぜ!
        var dbcontext = new MyDbContext();

        // MessageStoreなんてここでnewしてやるぜ!
        _messageStore = new MessageStore(dbcontext);
    }

結局これしか残されておらず、A氏はチームリーダーに「もうこれしかありません」と直談判し、そのようにしました。

数日後...

A氏の横で新人君がチームリーダーに怒られていました。

「なんでControllerでDBのテーブルに直接アクセスするようなコードを書くんだ。なんのためのN層システムかわからないのか」

新人君は「すいません、なんかやってみたらできたので…」と言い訳しています。

そう、A氏がController層からMyDbContextを直接参照できるように変更してしまったので、それが可能になってしまったのです。

それを横目に見つつ、A氏は新たな仕様変更の報告を受けていました。

「MyDbContextを直接生成するのをやめてください。MyDbContextを生成する為のファクトリメソッドMyDbFactory.CreateDb()を用意したのでこちらを使うようにしてください。」

キレるA氏

A氏はキレました。

つい数日前に、MessageStoreの仕様変更でひと悶着あったばかりです。
今度はMyDbContextだと!? そもそもController担当の自分には関係ない話じゃないか!

修正箇所は何十か所にも及びます。やってられません。

MessageStore担当のM氏に「あんたがMyDbContextをコンストラクタで要求するような変更をしたからこんなことになった。どうしてくれるんだ」とキレちらかします。

M氏はそしらぬ顔です。

D氏登場!

そこへD氏が登場して言いました。

「DI使えば一発ですよ」

D氏はControllerのコンストラクタを次のように修正しました。

    public MyController(MessageStore messageStore)
    {
        _messageStore = messageStore;
    }
Startup.cs
services.AddScoped(typeof(MyDbContext), p => MyDbFactory.CreateDb());
services.AddScoped<MessageStore, MessageStore>();

D「できました」

A「え、何、どういうこと? MyDbContextどこいった?」

D「DIしたので、MyControllerはもらったMessageStoreを使うだけですよ」

A「それは分かるけど、MessageStoreの生成にMyDbContext必要だろ?」

D「だからStartup.csで、MyDbContextの生成方法も伝えてますよ。このファクトリメソッドを使えばいいんですよね?」

Startup.cs
services.AddScoped(typeof(MyDbContext), p => MyDbFactory.CreateDb());

A「いや、そうなんだけど…えっ? どういうこと?」

D「DIコンテナは、MessageStoreを生成しようとして、コンストラクタ引数にMyDbContextがあるので、それも生成しようとするんですよ。幸いにもDIコンテナにMyDbContextの生成方法も伝えてあるので、それを生成して渡してくれるんです」

A「え、DIコンテナってそこまでやってくれるの?」

D「そうなんです。それがDIコンテナのいいところなんですよ」

A「マジか…。え、IMessageStoreとか定義していないけどいいの?」

D「なくても全然かまいませんよ。MessageStore型を要求されたらMessageStoreを生成して返すように設定すればOKです。どうせMessageStoreを入れ替えてテストとかしないですよね?」

A「うん‥そうだね…」

A氏は思いました。

「(インタフェースと実装の分離とか、テスタビリティとかの話はなんだったんだ…そんなことしなくてもDIめちゃくちゃ便利じゃないか。生成に関する仕様を外出しできるってのは、こういうことだったんだな…)」

D「DIの紹介記事にも、生成の仕様を一か所にまとめる、とか書かれてませんでした?」

A「心を読むな。でもようやくわかったよ。DIはテストの事なんて考えなくても便利だな。今後はどんどん使っていくことにしよう」

D「もちろん、インタフェースと実装を分けて登録できるなら、そうした方がパッケージも分割しやすくなって便利なんですけどね」

A「まあそれはおいおい考えるよ」

インスタンスの生存期間の話

A「ところで、生成されたMyDbContextはいつDispose()されるんだ?」

D「AddScopedで登録しているので、システムが決めたスコープ範囲が終了すればDIコンテナが自動でDispose()してくれますよ。ASP.NET MVCだと、スコープの範囲はリクエスト単位ですね」

A「それはつまり、リクエスト毎にMyDbContextが1つだけ生成されて、リクエスト中は全ての処理で同じインスタンスが使いまわされた後、リクエスト終了時に自動的にDispose()されるってことか」

D「その通りです。単一リクエスト中の処理は基本的にシングルスレッドですから、マルチスレッド問題も起こりません」

A「(なんだそりゃ、完璧じゃないか…)」

D「そうなんですよ」

A「だから心を読むな。ちなみに、リクエスト単位とかじゃなく、その都度インスタンスを生成したい場合はどうするんだ?」

D「その場合は、AddTransientを使ってサービス登録しておけばいいですよ。」

A「なるほど、簡単そうだな」

後日談

その後、DB層の担当者にまでDI導入のメリットを説くA氏の姿がありました。

もうしばらくすると、「サービス登録のコードが膨れ上がって大変なことに!」と騒ぐA氏の姿が見られるのですが、それはまた別の機会に…。

D「まぁその時は、アセンブリスキャンして自動的にサービス登録するやり方を紹介しますよ」

A「未来を読むな」

321
252
15

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
321
252