Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9700

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

$
0
0
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氏は、 「インタフェースを使わなかったら、結局そのクラスは依存オブジェクトの実装に依存したままじゃないか。だったら内部で直接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して大丈夫なオブジェクトでした。 あなたは淡々と、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) MessageStoreコンストラクタの中でMyDbContextを生成して使う そうすれば、MessageStoreのコンストラクタからMyDbContextは消え、元のシンプルな状態に戻ります。 public class MessageStore { private MyDbContext _dbcontext; public MessageStore() { _dbcontext = new MyDbContext(); } ... } しかしこれはMessageStoreの開発責任者に却下されました。 MessageStoreの内部でMyDbContextを生成してしまうと、その破棄責任を負う事になるから困る、というのです。 「じゃあMessageStoreをIDisposableにしてDispose()の中で_dbcontext.Dispose()をするようにするから、MessageStoreの利用者もちゃんとMessageStoreをDispose()してくれるか?」 そういわれ、「なんかそれは面倒だな」と思い諦めました。 (b) MessageStoreのGetMessageの中で毎回MyDbContextを生成して使う それでは、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を取得するなんて」と怒られました。 あなたは「すぐ使い終わるんだし、どうせ裏側では接続プール使いまわされてるんだからいいのに…」と思いましたが、確かに嫌がる気持ちもわかるなと思い黙っていました。 (c) Controller層から直接DB層を参照するよう変更する こうなったら最後の手段です。 Controllerから直接MyDbContextを参照できるようにパッケージ間の依存関係を変えてしまうのです。 それなら、これができるようになります。 DIする前のMyControllerのコンストラクタ public MyController() { // MessageStore用のMyDbContextをここでnewしてやるぜ! var dbcontext = new MyDbContext(); // MessageStoreなんてここでnewしてやるぜ! _messageStore = new MessageStore(dbcontext); } 結局これしか残されておらず、あなたはチームリーダーに「もうこれしかありません」と直談判し、そのようにしました。 数日後... 横で新人君がチームリーダーに怒られていました。 「なんでControllerでDBのテーブルに直接アクセスするようなコードを書くんだ。なんのためのN層システムかわからないのか」 新人君は「すいません、なんかやってみたらできたので…」と言い訳しています。 そう、あなたがController層からMyDbContextを直接参照できるように変更してしまったので、それが可能になってしまったのです。 それを横目に見つつ、あなたは新たな仕様変更の報告を受けていました。 「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「まあそれはおいおい考えるよ」 後日談 その後、DB層の担当者にまでDI導入のメリットを説くA氏の姿がありました。 もうしばらくすると、「サービス登録のコードが膨れ上がって大変なことに!」と騒ぐA氏の姿が見られるのですが、それはまた別の機会に…。 D「まぁその時は、アセンブリスキャンして自動的にサービス登録するやり方を紹介しますよ」 A「未来を読むな」

Viewing all articles
Browse latest Browse all 9700

Trending Articles