質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

2回答

2317閲覧

コードを柔軟にしたい

syogakusya

総合スコア67

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

2グッド

5クリップ

投稿2017/01/26 17:04

編集2017/01/26 17:06

#質問
ある要件を満たすコードをより柔軟にしたいです。
要件は以下です。

あるデータベースのテーブルAのデータをBに移し、Bを空にしたい。このとき、以下の順に処理を行う。

1 トランザクションを開始
2 Bのレコードをすべて削除
3 Aからデータを抽出し、Bに移す(データは似通っていて、データの変換は単純)
4 Aのレコードをすべて削除
5 トランザクションを終了

1'途中で例外的状況に陥ったらロールバックしてログを出力して異常終了

以下、捕捉となります。

データベース操作はEntity Frameworkによって行う

このとき、以下の実装を行いました。
設定値などはところどころ省略しています。注目していただきたいのはDataAToBクラスです。

class DataAToB { private readonly ISetting setting; private readonly IMapper<A, B> mapper; public DataAToB(ISetting setting, IMapper<A, B> mapper) { this.setting = setting; this.mapper = mapper; } // データを移す処理はこれ public void Do() { // 1 using (var context = new EntityFrameworkOrdinaryContext(setting)) using (var subContext = new EntityFrameworkOrdinaryContext(setting)) using (var transaction = context.Database.BeginTransaction()) { try { // 2 context.Bs.Delete(); // 3 foreach (var dataOfA in subContext.As) { // 捕捉:マッピングは絶対成功する var dataForB = 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<A> As { get; } public DbSet<B> Bs { get; } } interface ISetting { } interface IMapper<TRaw, TMapped> { 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、もっと言えばあらゆるエンティティからエンティティに対して、さらに言えば機能を分割してトランザクションがないパターンにも対応でき、あらゆるデータのストレージ間に対して汎用的に適用できるようなインターフェースと実装を書けたりしないものかと思い質問しました。必要に駆られているので教えてほしいといった旨ではございません。目的はコードをよくすることと、良いコードをかけるようになるための経験を積むことです。このコードは改善可能か、どういった観点から改善の方針を定めるのか、逆にこのままで十分なのかなど、皆様の意見をなんでも頂ければと思います。
よろしくお願いいたします。

ozwk, Tak1wa👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

こんにちは。
いい感じに抽象化の闇に飲まれてますね。昔自分も同じようにハマったりしました。質問がふわっとしているため、深読みしつつ回答してみます。


コードを柔軟にする、即ち汎用化、一般化していくことは、基本的には良いことと言われるものですが、実際に着手する際にはまず「どこまで一般化するのか?」の基準をはっきりと定める必要があります。
例えば、差し替えられる箇所を増やすほどに、そのコードを応用できる場所も増えるわけですが、それに伴って「(そのクラスを使用する)ユーザが書かなければならないコード」の量も増えていきます。そして、究極の汎用化は「何も書かない」ことになります。ただのC#まで行ってしまえば、ユーザはなんでも作れるわけですからね。
質問でやりたい目的は、「EntityFramework」そのものが既に達成しているため、この上に抽象レイヤをかぶせるのは蛇足であると考えます。質問のコードの時点で「やりたいことだけを書く」という状態までたどり着いていると判断できるからです。

ただし、特定の用途に対して繰り返し使いたい汎用的な処理を作成するという方針自体は悪くないです。これは「EntityFrameworkに乗っかるカタチのユーティリティライブラリを作りたい」と言い換えることができます。

というわけで、今回は「良いライブラリを書く指針」について書きます。

ライブラリというものは、
「ユーザが表層に露出したインターフェースを使用して、何らかの目的を達成出来るようにするもの」
「ユーザが内部のコードに触ることができないため、内部のコードに触らなくても良いように書く」
「ユーザがやりたいことを記述できる汎用性を持たせる」
「ユーザは思いもよらないようなコードを書くので、出来る限り多くの状況をカバーした安全性を持たせる」
というような要素を満たすように設計されたもので、根本的にユーザコードとは書き方が異なってきます。

では、ライブラリとして機能を提供する際のたったひとつの注意点です。

  • ユーザが出来ることを、ひとつの機能につきたったひとつに制限する

「汎用性」と矛盾してると思いますか?そうではないです。クラスは「名前に準ずる機能を提供するもの」であり、「名前が機能そのものを表す」ものではありません。思いつく限りの汎用性のあるクラスはやはり、名前に準じた機能を提供しています。そして、その機能に「関係のないコンテキスト」を排除することで、「あらゆるものに」そのクラスの機能を提供することができているのです。今回は、機能を提供する際に「何を提供したかったのか」をはっきりさせていなかったため、どこをどう汎用化すれば良いのかがわからなくなっているのが悩みの原因だと推測します。クラス自体を「機能」と見なしてしまったため、その本質を正しく認識できなくなっているのです。

例として、今回は「EntityFrameworkでアクセス可能なDBで、テーブルからテーブルにデータを移動する」というたったひとつの機能を提供したいと決めるとします。それなら、例えば以下のようなAPIが存在すれば、どうでしょうか?

csharp

1public static void MoveData<TDbContext, TA, TB>( 2 this TDbContext context, 3 Func<TDbContext, DbSet<TA>> from, 4 Func<TDbContext, DbSet<TB>> to, 5 Func<TA, TB> converter) 6 where TDbContext : DbContext 7 ;

APIだけでも、何を書けばどう動くのか、ある程度見えるのではないでしょうか。
「テーブルからテーブルにデータを移動したい」というたったひとつの目的にのみ使える代わりに、その目的に合致する操作であれば、どんな対象にでも適用できるように書けるはずです。(EntityFrameworkの細かい扱いに自信がないので、もっと良い設計があるかもしれません……)
そして、これ以外の機能を実装したいなら、それに合わせた「名前」を新たに決めて、そのたったひとつの目的を達成する機能(メソッド)を作ってしまえばいいのです。


その他ライブラリを作る--機能を提供する--時に気にすることというと、

  • 何かのフレームワークに乗っかるつもりならべったり乗っかる
  • 標準のインターフェース、依存するフレームワークのインターフェースをなるべく使用する
  • ユーザがコードを書く時に「面倒だ」と感じないようにする
  • ユーザにどう使ってほしいのか、ユーザがどのように使いたいと考えるかを常に意識する
  • シグネチャから何をするのか読み取れないメソッドは設計不足
  • public、private以外のaccessibilityも必要に応じて使っていく
  • ユーザはぶっ飛んだコードを書くと思って、出来る限りの状況を想定したコードを書く
  • ユーザが発生させる例外は決して握りつぶさない(rethrowで透過させてもいいし、何らかの失敗を通知する形があればそれでも良い)

みたいな感じですかね。


あんまり長々と書いても仕方ないので、サクッと締めます。
まずは「提供したい機能とその名前(シグネチャ)をはっきりと定める」ことから始めると良いと思います。

投稿2017/01/30 10:46

tamoto

総合スコア4103

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

あらゆるデータのストレージ間に対して汎用的に適用できるようなインターフェースと実装を書けたりしないものか

それってある程度System.Dataにまとまってませんか
お話に出ているEntityFramework自体、ある程度特化した環境で生産性をあげようと動くもの(作られてきたもの)なので、それを汎化したいといってもムリがあるのでは無いでしょうか?

最近teratailで見かける気がする「コードをよくすること」とはなんぞやというのが個人的な感想です
これこそ完全にコンテキストに依存するので回答しにくいような気がします
例えば私なら「データのストレージ間に対して汎用的に適用できるような...」の課題にそもそも「EntityFramework」は採用しませんし

投稿2017/01/28 21:45

dojikko

総合スコア3939

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問