2012-10-10 66 views
3

我希望能夠指定可用於查詢的類型化接口。我最終希望能夠做到這樣的事情:通用接口的通用工廠方法

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 
{ 

} 

表明意圖,然後讓用戶創建他們喜歡(因爲他們,而不是工廠,將調用正確的查詢)。

但是,最後一個選項不是很好,因爲它不會讓我裝飾查詢(例如通過使用緩存裝飾器動態緩存它們)。

+0

你真的需要泛型IQuery嗎?用作「方法說明符」而不是「常用方法」的IMO接口是不好的做法,可能會導致這種問題。虛擬接口要好得多。 – Euphoric

+0

@Euphoric:如果可能,我確實需要通用查詢。能夠裝飾查詢將產生相當強大的功能。 – jgauffin

+0

你是什麼意思「裝飾查詢」?另請注意,因此,您需要爲輸入參數至少創建一個類。這不會是問題,如果你讓具體查詢做它自己的執行的執行。 – Euphoric

回答

4

我完全理解你正在試圖在這裏做。您正在應用SOLID原則,因此查詢實現從消費者中抽象出來,消費者僅發送消息(DTO)並獲得結果。通過爲查詢實現一個通用接口,我們可以用一個裝飾器來包裝實現,它允許各種有趣的行爲,比如事務行爲,審計,性能監視,緩存等等。

這樣做的方法是定義爲一個消息話題下面接口(查詢定義):

public interface IQuery<TResult> { } 

並定義以下接口,用於實行:

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

IQuery<TResult>是某種毫安的rker接口,但是這使我們能夠靜態地定義什麼是查詢返回,例如:

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

這允許您添加橫切關注點查詢處理程序,沒有消費者知道這一點,這給你完整的組合物1樂時間支持。但是,這種方法(IMO)最大的缺點是,你會很容易得到大的構造函數,因爲你經常需要執行多個查詢(沒有真正違反SRP)。

爲了解決這種情況,可以引入消費者和IQueryHandler<TQuery, TResult>接口之間的抽象:

注入多個 IQueryHandler<TQuery, TResult>實現
public interface IQueryProcessor 
{ 
    TResult Execute<TResult>(IQuery<TResult> query); 
} 

Instread,可以注入一個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抽象有缺點。例如,您錯過了讓您的DI容器驗證是否存在請求的IQueryHandler<TQuery, TResult>實現的可能性。在請求根對象時,您會發現此時您調用processor.Execute。您可以通過編寫一個額外的集成測試來解決這個問題,該測試檢查是否爲實現IQuery<TResult>的每個類註冊了IQueryHandler<TQuery, TResult>。另一個缺點是依賴關係不太清晰(IQueryProcessor是某種環境上下文),這使得單元測試有點困難。例如,當消費者運行新類型的查詢時,您的單元測試仍然會編譯。

您可以在此博客文章中找到有關此設計的更多信息:Meanwhile… on the query side of my architecture

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

在這一點上,你仍然有兩個類型參數,但假設你通常會想讀取查詢並立即執行,你可以使用類型推斷允許的東西像:

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

我不知道這是否有助於您的實際情況,但它至少是一個選項來考慮。

+0

這看起來不錯。讓我玩一下。 – jgauffin

+0

拼寫錯誤如何傳播真是太棒了......你看,你們都留下了一個開放的pa句。或者我不知道一些神祕的C#語法? – Theraot

+0

@Theraot:固定。 –

0

我可能是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。並且沒有令人討厭的仿製藥在眼前。

而且上面的例子中的一個專業化:

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

,然後使用減少到單行:

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

該模式的全部目的是從規範中抽象出實現(並使其可擴展而無需修改現有代碼)。您將使用情況與特定實施相關聯。而且,通過在接口中不指定結果,在從工廠返回之前不可能裝飾實現。 – jgauffin

+0

我如何將它與特定的實現聯繫起來?爲什麼它不能在工廠裝飾?你認爲在工廠裏隨處可以裝飾仿製藥嗎? – Euphoric

+0

'新的ActiveUsersQuery' =自己創建實現。隨意更改執行程序並顯示如何實現適用於所有查詢的緩存裝飾器。 – jgauffin