質問: this.mapper.Map(dataOfA); context.Bs.Add(dataForB); } // 4 context.As.Delete(); // 5 context.SaveChanges(); transaction.Commit(); } catch (Exception) { // 1' transaction.Rollback(); throw new AToBFailureException(); } } } } class EntityFrameworkOrdinaryContext : DbContext { public EntityFrameworkOrdinaryContext(ISetting setting) : base(null) { } public DbSet As { get; } public DbSet Bs { get; } } interface ISetting { } interface IMapper { TMapped Map(TRaw raw); } class A { } class B { } class AToBFailureException : Exception { //... } ``` しかし、このコードに改善の余地があるような気がしています。 以下が気になっているところです。 1.コンテキストクラスを直接生成している 2.Bからデータを削除し、BからAにデータを移し、Aのデータを削除するという具体的な一連の処理を直接やっている。 3.さらにトランザクション管理までしている。 4.データベースコンテキストクラスや、EntityFrameworkに直接依存している(DbContext,DbSetなど) AからBでなくBからA、もっと言えばあらゆるエンティティからエンティティに対して、さらに言えば機能を分割してトランザクションがないパターンにも対応でき、あらゆるデータのストレージ間に対して汎用的に適用できるようなインターフェースと実装を書けたりしないものかと思い質問しました。必要に駆られているので教えてほしいといった旨ではございません。目的はコードをよくすることと、良いコードをかけるようになるための経験を積むことです。このコードは改善可能か、どういった観点から改善の方針を定めるのか、逆にこのままで十分なのかなど、皆様の意見をなんでも頂ければと思います。 よろしくお願いいたします。
回答: こんにちは。 いい感じに抽象化の闇に飲まれてますね。昔自分も同じようにハマったりしました。質問がふわっとしているため、深読みしつつ回答してみます。 コードを柔軟にする、即ち汎用化、一般化していくことは、基本的には良いことと言われるものですが、実際に着手する際にはまず「どこまで一般化するのか?」の基準をはっきりと定める必要があります。 例えば、差し替えられる箇所を増やすほどに、そのコードを応用できる場所も増えるわけですが、それに伴って「(そのクラスを使用する)ユーザが書かなければならないコード」の量も増えていきます。そして、究極の汎用化は「何も書かない」ことになります。ただのC#まで行ってしまえば、ユーザはなんでも作れるわけですからね。 質問でやりたい目的は、「EntityFramework」そのものが既に達成しているため、この上に抽象レイヤをかぶせるのは蛇足であると考えます。質問のコードの時点で「やりたいことだけを書く」という状態までたどり着いていると判断できるからです。 ただし、特定の用途に対して繰り返し使いたい汎用的な処理を作成するという方針自体は悪くないです。これは「EntityFrameworkに乗っかるカタチのユーティリティライブラリを作りたい」と言い換えることができます。 というわけで、今回は「良いライブラリを書く指針」について書きます。 ライブラリというものは、 「ユーザが表層に露出したインターフェースを使用して、何らかの目的を達成出来るようにするもの」 「ユーザが内部のコードに触ることができないため、内部のコードに触らなくても良いように書く」 「ユーザがやりたいことを記述できる汎用性を持たせる」 「ユーザは思いもよらないようなコードを書くので、出来る限り多くの状況をカバーした安全性を持たせる」 というような要素を満たすように設計されたもので、根本的にユーザコードとは書き方が異なってきます。 では、ライブラリとして機能を提供する際のたったひとつの注意点です。 * ユーザが出来ることを、ひとつの機能につきたったひとつに制限する 「汎用性」と矛盾してると思いますか?そうではないです。クラスは「名前に準ずる機能を提供するもの」であり、「名前が機能そのものを表す」ものではありません。思いつく限りの汎用性のあるクラスはやはり、名前に準じた機能を提供しています。そして、その機能に「関係のないコンテキスト」を排除することで、「あらゆるものに」そのクラスの機能を提供することができているのです。今回は、機能を提供する際に「何を提供したかったのか」をはっきりさせていなかったため、どこをどう汎用化すれば良いのかがわからなくなっているのが悩みの原因だと推測します。クラス自体を「機能」と見なしてしまったため、その本質を正しく認識できなくなっているのです。 例として、今回は「EntityFrameworkでアクセス可能なDBで、テーブルからテーブルにデータを移動する」というたったひとつの機能を提供したいと決めるとします。それなら、例えば以下のようなAPIが存在すれば、どうでしょうか? ``` public static void MoveData( this TDbContext context, Func> from, Func> to, Func converter) where TDbContext : DbContext ; ``` APIだけでも、何を書けばどう動くのか、ある程度見えるのではないでしょうか。 「テーブルからテーブルにデータを移動したい」というたったひとつの目的にのみ使える代わりに、その目的に合致する操作であれば、どんな対象にでも適用できるように書けるはずです。(EntityFrameworkの細かい扱いに自信がないので、もっと良い設計があるかもしれません......) そして、これ以外の機能を実装したいなら、それに合わせた「名前」を新たに決めて、そのたったひとつの目的を達成する機能(メソッド)を作ってしまえばいいのです。 その他ライブラリを作る--機能を提供する--時に気にすることというと、 * 何かのフレームワークに乗っかるつもりならべったり乗っかる * 標準のインターフェース、依存するフレームワークのインターフェースをなるべく使用する * ユーザがコードを書く時に「面倒だ」と感じないようにする * ユーザにどう使ってほしいのか、ユーザがどのように使いたいと考えるかを常に意識する * シグネチャから何をするのか読み取れないメソッドは設計不足 * public、private以外のaccessibilityも必要に応じて使っていく * ユーザはぶっ飛んだコードを書くと思って、出来る限りの状況を想定したコードを書く * ユーザが発生させる例外は決して握りつぶさない(rethrowで透過させてもいいし、何らかの失敗を通知する形があればそれでも良い) みたいな感じですかね。 あんまり長々と書いても仕方ないので、サクッと締めます。 まずは「提供したい機能とその名前(シグネチャ)をはっきりと定める」ことから始めると良いと思います。