4

クエリで使用できる型付きインターフェイスを指定できるようにしたい。私は最終的に次のようなことができるようになりたいと思っています。

var query = _queryfactory.Create<IActiveUsersQuery>();
var result = query.Execute(new ActiveUsersParameters("a", "b"));
foreach (var user in result)
{
    Console.WriteLine(user.FirstName);
}

簡単そうに見えますね クエリが型付きパラメータと型付き結果を取得したことに注意してください。クエリファクトリを制限してクエリのみを含めることができるようにするには、次のようなものを指定する必要があります。

public interface IQuery<in TParameters, out TResult>
    where TResult : class
    where TParameters : class
{
    TResult Invoke(TParameters parameters);
}

しかし、それは癌のように広がるでしょう:

// this was easy
public interface IActiveUsersQuery : IQuery<ActiveUsersParameters, UserResult>
{

}

//but the factory got to have those restrictions too:
public class QueryFactory
{
    public void Register<TQuery, TParameters, TResult>(Func<TQuery> factory)
        where TQuery : IQuery<TParameters, TResult>
        where TParameters : class
        where TResult : class
    {
    }

    public TQuery Create<TQuery, TParameters, TResult>()
        where TQuery : IQuery<TParameters, TResult>
        where TParameters : class
        where TResult : class
    {
    }
}

これは最終的に次のようなファクトリ呼び出しにつながります。

factory.Create<IActiveUsersQuery, ActiveUsersParameters, UserResult>();

ユーザーがパラメーターの種類と結果の種類を指定する必要があるため、あまり良くありません。

私はそれを制御しすぎていますか?ダミーのインターフェイスを作成する必要があります。

public interface IQuery
{

}

意図を示してから、ユーザーが好きなものを作成できるようにします(ファクトリではなく、ユーザーが正しいクエリを呼び出すため)。

ただし、最後のオプションではクエリをデコレートできないため、あまり便利ではありません(たとえば、キャッシングデコレータを使用して動的にキャッシュします)。

4

3 に答える 3

5

あなたがここで何をしようとしているのか、私は完全に理解しています。SOLID の原則を適用しているため、クエリの実装は、単にメッセージ (DTO) を送信して結果を取得する消費者から離れて抽象化されます。クエリの汎用インターフェースを実装することで、実装をデコレータでラップできます。これにより、トランザクション動作、監査、パフォーマンス監視、キャッシュなど、あらゆる種類の興味深い動作が可能になります。

これを行う方法は、メッセージ (クエリ定義) に対して次のインターフェイスを定義することです。

public interface IQuery<TResult> { }

そして、実装のために次のインターフェースを定義します。

public interface IQueryHandler<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

これIQuery<TResult>は一種のマーカー インターフェースですが、これにより、クエリが返すものを静的に定義できます。たとえば、次のようになります。

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }

    public bool IncludeInactiveUsers { get; set; }
}

実装は次のように定義できます。

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly IUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(IUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        // example
        return (
            from user in this.db.Users
            where user.Name.Contains(query.SearchText)
            where user.IsActive || query.IncludeInactiveUsers
            select user)
            .ToArray();
    }
}

IQueryHandler<TQuery, TResult>コンシューマーは、クエリを実行するために に依存することはできません。

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> handler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> handler)
    {
        this. handler = handler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.handler.Handle(query);

        return this.View(users);
    }
}

これにより、消費者がこれを知ることなく、横断的な懸念事項をクエリ ハンドラーに追加できます。これにより、コンパイル時の完全なサポートが得られます。

ただし、このアプローチ (IMO) の最大の欠点は、(SRP に実際に違反することなく) 複数のクエリを実行する必要があることが多いため、簡単に大きなコンストラクターになってしまうことです。

IQueryHandler<TQuery, TResult>これを解決するために、コンシューマーとインターフェースの間に抽象化を導入できます。

public interface IQueryProcessor
{
    TResult Execute<TResult>(IQuery<TResult> query);
}

複数のIQueryHandler<TQuery, TResult>実装を注入する代わりに、1 つを注入できますIQueryProcessor。これで、コンシューマーは次のようになります。

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Execute(query);

        return this.View(users);
    }
}

実装は次のIQueryProcessorようになります。

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Execute<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

これはコンテナー (コンポジション ルートの一部) に依存し、dynamic入力 (C# 4.0) を使用してクエリを実行します。

これIQueryProcessorは実質的にあなたのQueryFactory.

ただし、このIQueryProcessor抽象化を使用することには欠点があります。たとえば、要求されたIQueryHandler<TQuery, TResult>実装が存在するかどうかを DI コンテナーに検証させる可能性を見逃しています。processor.Executeルート オブジェクトを要求するときに代わりに呼び出すことがすぐにわかります。IQueryHandler<TQuery, TResult>を実装する各クラスに が登録されているかどうかをチェックする追加の統合テストを作成することで、これを解決できますIQuery<TResult>。もう 1 つの欠点は、依存関係があまり明確ではないことです (これIQueryProcessorはある種のアンビエント コンテキストです)。これにより、単体テストが少し難しくなります。たとえば、消費者が新しいタイプのクエリを実行しても、単体テストはコンパイルされます。

この設計の詳細については、次のブログ投稿を参照してください: While… on the query side of my architecture .

于 2012-10-10T08:42:25.963 に答える
2

工場で本当に必要ですか?TQueryあなたはただ使うことができませんでした:

public void Register<TParameters, TResult>
        (Func<IQuery<TParameters, TResult>> factory)
    where TParameters : class
    where TResult : class
{
}

public IQuery<TParameters, TResult> Create<TParameters, TResult>()
    where TParameters : class
    where TResult : class
{
}

その時点で、まだ 2 つの型引数を取得していますが、通常はクエリをフェッチしてすぐに実行したいと仮定すると、型推論を使用して次のようなものを許可できます。

public QueryExecutor<TResult> GetExecutor() where TResult : class
{
}

次に、ジェネリックメソッドがあります。

public IQueryable<TResult> Execute<TParameters>(TParameters parameters)
    where TParameters : class
{
}

したがって、元のコードは次のようになります。

var query = _queryfactory.GetExecutor<UserResult>()
                         .Execute(new ActiveUsersParameters("a", "b"));

それが実際のケースで役立つかどうかはわかりませんが、少なくとも考慮すべきオプションです。

于 2012-10-10T06:07:58.120 に答える
1

私はOTかもしれませんが、私のコメントに従って応答する必要があります。クエリオブジェクトシステム全体を次のように実装します。

システムクラス:

public class Context
{
    // context contains system specific behaviour (connection, session, etc..)
}

public interface IQuery
{
    void SetContext(Context context); 
    void Execute();
}

public abstract class QueryBase : IQuery
{
    private Context _context;

    protected Context Context { get { return _context; } }

    void IQuery.SetContext(Context context)
    {
        _context = context;
    }
    public abstract void Execute();
}

public class QueryExecutor
{
    public void Execute(IQuery query)
    {
        query.SetContext({set system context});
        query.Execute();
    }
}

具体的なクエリクラス:

public interface IActiveUsersQuery : IQuery // can be ommited
{
    int InData1 { get; set; }
    string InData2 { get; set; }

    List<string> OutData1 { get; }
}

public class ActiveUsersQuery : QueryBase, IActiveUsersQuery
{
    public int InData1 { get; set; }
    public string InData2 { get; set; }

    public List<string> OutData1 { get; private set; }

    public override void Execute()
    {
        OutData1 = Context.{do something with InData1 and InData};
    }
}

そして、あなたはそれを次のように使用します:

QueryExecutor executor;

public void Example()
{
    var query = new ActiveUsersQuery { InData1 = 1, InData2 = "text" };
    executor.Execute(query);

    var data = query.OutData1; // use output here;
}

それでも、クエリオブジェクトシステムと同じ利点があります。特定のクエリまたは任意のクエリを装飾する機能は引き続き利用できます(デザインにそれがありません)。また、クエリあたりのオブジェクト数を2に減らし、特定のクエリインターフェイスが必要ない場合は、1つだけに減らすことができます。そして、目に見える厄介なジェネリックはありません。

そして、上記の例の1つの専門分野:

public interface IQuery<TResult> : IQuery
{
    TResult Result { get; }
}

public class QueryExecutor
{
    // ..

    public TResult Execute<TResult>(IQuery<TResult> query)
    {
        Execute((IQuery)query);
        return query.Result;
    }
}

public class ActiveUsersQuery : QueryBase, IQuery<List<string>>
{
    public int InData1 { get; set; }
    public string InData2 { get; set; }

    public List<string> Result { get; private set; }

    public override void Execute()
    {
        //OutData1 = Context.{do something with InData1 and InData};
    }
}

そして、使用量は1行に減ります:

public void Example()
{
    var outData = executor.Execute(new ActiveUsersQuery { InData1 = 1, InData2 = "text" });
}
于 2012-10-10T07:05:34.583 に答える