简体   繁体   English

通用接口的通用工厂方法

[英]Generic factory method for a generic interface

I want to be able to specify a typed interface which can be used with queries. 我希望能够指定一个可以与查询一起使用的类型化接口。 I ultimately want to be able to do something like: 我最终希望能够做到这样的事情:

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

Looks simple enough, ehh? 看起来很简单,嗯? Notice that the query got typed parameters and a typed result. 请注意,查询获得了类型化参数和类型化结果。 To be able to restrict the query factory to only contain queries we'll have to specify something like: 为了能够将查询工厂限制为仅包含查询,我们必须指定以下内容:

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

But that's going to spread like cancer: 但这会像癌症一样蔓延:

// 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
    {
    }
}

Which ultimately leads to a factory invocation like: 这最终导致工厂调用,如:

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

Not very nice since the user have to specify the parameter type and the result type. 由于用户必须指定参数类型和结果类型,因此不是很好。

Am I trying to control it too much? 我试图控制它太多了吗? Should I just create a dummy interface: 我应该创建一个虚拟接口:

public interface IQuery
{

}

to show the intent and then let the users create whatever they like (since they, and not the factory, will invoke the correct query). 显示意图,然后让用户创建他们喜欢的任何东西(因为他们,而不是工厂,将调用正确的查询)。

However, the last option isn't very nice since it won't let me decorate queries (for instance dynamically cache them by using a caching decorator). 但是,最后一个选项不是很好,因为它不会让我装饰查询(例如通过使用缓存装饰器动态缓存它们)。

I completely understand what you are trying to do here. 我完全理解你在这里要做的事情。 You are applying the SOLID principles, so query implementations are abstracted away from the consumer, who merely sends a message (DTO) and gets a result. 您正在应用SOLID原则,因此查询实现是从消费者那里抽象出来的,消费者只是发送消息(DTO)并获得结果。 By implementing a generic interface for the query, we can wrap implementations with a decorator, which allows all sorts of interesting behavior, such as transactional behavior, auditing, performance monitoring, caching, etc, etc. 通过为查询实现通用接口,我们可以使用装饰器包装实现,装饰器允许各种有趣的行为,例如事务行为,审计,性能监视,缓存等。

The way to do this is to define the following interface for a messsage (the query definition): 执行此操作的方法是为messsage定义以下接口(查询定义):

public interface IQuery<TResult> { }

And define the following interface for the implemenation: 并为实现定义以下接口:

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

The IQuery<TResult> is some sort of marker interface, but this allows us to define statically what a query returns, for instance: IQuery<TResult>是某种标记接口,但这允许我们静态定义查询返回的内容,例如:

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

    public bool IncludeInactiveUsers { get; set; }
}

An implementation can be defined as follows: 实现可以定义如下:

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();
    }
}

Consumers can no take a dependency on the IQueryHandler<TQuery, TResult> to execute queries: 消费者不能依赖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);
    }
}

This allows you to add cross-cutting concerns to query handlers, without consumers to know this and this gives you complete compile time support. 这允许您向查询处理程序添加横切关注点,而无需消费者知道这一点,这为您提供了完整的编译时支持。

The biggest downside of this approach (IMO) however, is that you'll easily end up with big constructors, since you'll often need to execute multiple queries, (without really violating the SRP). 然而,这种方法(IMO)的最大缺点是你很容易得到大型构造函数,因为你经常需要执行多个查询,(而不是真正违反SRP)。

To solve this, you can introduce an abstraction between the consumers and the IQueryHandler<TQuery, TResult> interface: 要解决这个问题,您可以在使用者和IQueryHandler<TQuery, TResult>界面之间引入抽象:

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

Instread of injecting multiple IQueryHandler<TQuery, TResult> implementations, you can inject one IQueryProcessor . 注入多个IQueryHandler<TQuery, TResult>实现的IQueryHandler<TQuery, TResult> ,您可以注入一个IQueryProcessor Now the consumer will look like this: 现在,消费者将看起来像这样:

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);
    }
}

The IQueryProcessor implementation can look like this: 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);
    }
}

It depends on the container (it is part of the composition root) and uses dynamic typing (C# 4.0) to execute the queries. 它取决于容器(它是组合根的一部分)并使用dynamic类型(C#4.0)来执行查询。

This IQueryProcessor is effectively your QueryFactory . 这个IQueryProcessor实际上是你的QueryFactory

There are downsides of using this IQueryProcessor abstraction though. 但是,使用这个IQueryProcessor抽象有一些缺点。 For instance, you miss the possibility to let your DI container verify whether a requested IQueryHandler<TQuery, TResult> implementation exists. 例如,您错过了让DI容器验证是否存在请求的IQueryHandler<TQuery, TResult>实现的可能性。 You'll find out at the moment that you call processor.Execute instead when requesting a root object. 您现在可以找到调用processor.Execute而不是在请求根对象时。 You can solve this by writing an extra integration test that checks if an IQueryHandler<TQuery, TResult> is registered for each class that implements IQuery<TResult> . 您可以通过编写额外的集成测试来解决这个问题,该测试检查是否为实现IQuery<TResult>每个类注册了IQueryHandler<TQuery, TResult> IQuery<TResult> Another downside is that the dependencies are less clear (the IQueryProcessor is some sort of ambient context) and this makes unit testing a bit harder. 另一个缺点是依赖关系不太清楚( IQueryProcessor是某种环境上下文),这使单元测试更加困难。 For instance, your unit tests will still compile when a consumer runs a new type of query. 例如,当使用者运行新类型的查询时,您的单元测试仍将进行编译。

You can find more information about this design in this blog post: Meanwhile… on the query side of my architecture . 您可以在此博客文章中找到有关此设计的更多信息: 同时...在我的架构的查询方面

Do you really need TQuery in your factory? 真的需要在你的工厂使用TQuery吗? Could you not just use: 你能不能只使用:

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
{
}

At that point, you've still got two type arguments, but assuming you'd usually want to fetch the query and immediately execute it, you could use type inference to allow something like: 此时,您仍然有两个类型参数,但假设您通常想要获取查询并立即执行它,您可以使用类型推断来允许以下内容:

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

which would then have a generic method: 那么这将有一个通用的方法:

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

So your original code would just become: 所以你的原始代码将成为:

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

I don't know whether that will help in your actual case, but it's at least an option to consider. 我不知道这对你的实际情况是否有帮助,但它至少是一个可以考虑的选择。

I might be OT, but I need to respond according to my comments. 我可能是OT,但我需要根据我的评论做出回应。 I would implemented the whole query object system like this: 我会像这样实现整个查询对象系统:

System classes: 系统类:

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();
    }
}

Concrete query classes: 具体查询类:

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};
    }
}

And you use it like this: 你这样使用它:

QueryExecutor executor;

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

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

It still has same advantages of query-object system. 它仍然具有查询对象系统的相同优点。 You still get ability to decorate either specific queries or any query (you are missing that in your design). 您仍然可以装饰特定查询或任何查询(您在设计中缺少这些查询)。 It also reduces objects per query to 2 and you can reduce it to only 1 if you don't need the specific query interface. 它还将每个查询的对象减少到2,如果不需要特定的查询接口,则可以将其减少到只有1。 And there are no nasty generics in sight. 而且看不到令人讨厌的仿制品。

And one specialization of above example: 以上例子的一个专业化:

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};
    }
}

And then use is reduced to single line : 然后使用减少到单行:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM