2010-02-15 17 views
4

變化狀態位於構建器模式的中心。有沒有一種慣用的方式來實現F#中這樣一個類的內部實現,它將減少/消除可變狀態,同時保留通常的接口(該類將主要用於其他.NET語言)?在F#中實現構建器模式(a System.Text.StringBuilder)

這裏有一個天真的實現:

type QueryBuilder<'T>() =        //' 
    let where = ref None 
    let orderBy = ref None 
    let groupBy = ref None 
    member x.Where(cond) = 
     match !where with 
     | None -> where := Some(cond) 
     | _ -> invalidOp "Multiple WHERE clauses are not permitted" 
    // members OrderBy and GroupBy implemented similarly 

一個想法是創建一個記錄類型來存儲的內部結構,並使用複製和更新表達式。

type private QueryBuilderSpec<'T> =      //' 
    { Where : ('T -> bool) option;      //' 
     OrderBy : (('T -> obj) * bool) list;    //' 
     GroupBy : ('T -> obj) list }      //' 

type QueryBuilder<'T>() =        //' 
    let spec = ref None 
    member x.Where(cond) = 
     match !spec with 
     | None -> 
      spec := Some({ Where = Some(cond); OrderBy = []; GroupBy = [] }) 
     | Some({ Where = None; OrderBy = _; GroupBy = _} as s) -> 
      spec := Some({ s with Where = Some(cond) }) 
     | _ -> invalidOp "Multiple WHERE clauses are not permitted" 
    // members OrderBy and GroupBy implemented similarly 

這一切似乎有點笨重,也許應該努力實現F#勢在必行模式時,可以預期的。有沒有更好的方法來做到這一點,再次,爲了命令式語言而保留常用的構建器界面?

+1

我並沒有真正看到你的建設者之間有太大的區別。任何一個都可以從外部變化,而不管第一種還是第二種方式都沒有實現。 –

+0

沒錯。我沒有看到消除可變狀態的任何方法。不同之處在於第一個實現可以包含任意數量的可變變量。第二個實現有一個。是的,也許這是一個微不足道的區別。我討厭這兩個,但這是我需要建立的。我更喜歡F#,但也許我需要回到C#來做到這一點。只是以爲在放棄F#之前我會得到更多的意見。 – Daniel

回答

7

我認爲這取決於在你的用例中,你可能會用一個不可變的實現更好。下面的例子 將靜態執行,任何製造商都有,其中,順序和正在修建前一次設置完成組屬性, 雖然他們可以按任意順序設置:

type QueryBuilder<'t,'w,'o,'g> = 
    internal { where : 'w; order : 'o; group : 'g } with 

let emptyBuilder = { where =(); order =(); group =() } 

let addGroup (g:'t -> obj) (q:QueryBuilder<'t,_,_,unit>) : QueryBuilder<'t,_,_,_> = 
    { where = q.where; order = q.order; group = g } 

let addOrder (o:'t -> obj * bool) (q:QueryBuilder<'t,_,unit,_>) : QueryBuilder<'t,_,_,_> = 
    { where = q.where; order = o; group = q.group } 

let addWhere (w:'t -> bool) (q:QueryBuilder<'t,unit,_,_>) : QueryBuilder<'t,_,_,_> = 
    { where = w; order = q.order; group = q.group } 

let build (q:QueryBuilder<'t,'t->bool,'t->obj,'t->obj*bool>) = 
    // build query from builder here, knowing that all components have been set 

顯然,你可能需要調整這個針對您的特定約束,並將其展示給其他語言,您可能希望使用其他類上的成員和委託,而不是讓出界限的函數和F#函數類型,但您可以獲得該圖片。

UPDATE

也許是值得推廣的是什麼我多一點的描述做了 - 這個代碼是有點密。使用記錄類型沒有什麼特別之處;一個正常的不可變類會一樣好 - 代碼會稍微簡潔一些,但與其他語言交互可能會更好。我的實現基本上有兩個重要特性:

  1. 每個用於添加的方法都會返回一個代表當前狀態的新構建器。這很簡單,雖然它與Builder模式通常實現的方式明顯不同。
  2. 通過使用其他泛型類型參數,您可以實施非平凡的不變量,例如在使用Builder之前要求指定幾個不同的屬性中的每一個。這對於某些應用程序來說可能是過度的,並且有點棘手。它只能用一個不可變的生成器,因爲我們可能需要在操作後返回一個具有不同的類型參數的生成器。

在上面的例子中,這樣的操作順序將由型系統被允許:

let query = 
    emtpyBuilder 
    |> addGroup ... 
    |> addOrder ... 
    |> addWhere ... 
    |> build 

而這一個不會,因爲它從未設置順序爲:

let query = 
    emptyBuilder 
    |> addGroup ... 
    |> addWhere ... 
    |> build 

正如我所說的,這可能是對你的應用程序的矯枉過正,但它是唯一可能的,因爲我們正在使用不可變的構建器。

+0

謝謝你讓我感到蠢。 :)而不是每個成員返回「這個」,它應該返回一個新的不變的QueryBuilder。當然!謝謝。這正是我正在尋找的。 – Daniel

+0

順便說一句 - 我不完全理解你的代碼,但我認爲我可以放棄記錄類型並使類類型不可變(從每個「構建器」方法返回一個新實例),這將很有用。 – Daniel

+0

@丹尼爾 - 查看我的更新,以進一步解釋我試圖展示的內容。你完全正確,這與非記錄不可變類型一樣好。 – kvb

2

從內部消除可變性看起來並不像它對我有多大的意義......你可以通過設計使它變得可變 - 在這一點上的任何技巧都不會真正改變任何東西。

至於簡潔 - let mutable可能是因爲它得到好(這樣你就不需要使用!取消引用):

type QueryBuilder<'T>() = 
    let mutable where = None 
    let mutable orderBy = None 
    let mutable groupBy = None 
    member x.Where(cond) = 
     match where with 
     | None -> where <- Some(cond) 
     | _ -> invalidOp "Multiple WHERE clauses are not permitted" 
    // members OrderBy and GroupBy implemented similarly 
+1

使用記錄以及複製和更新表達式,或其他功能技術與可變變量(減少可變變量的數量除外)有什麼好處? – Daniel

+0

我在這裏看不到。 –

1

一個備選是隻使用F#記錄類型,與這裏的一切是默認值無/空:

type QueryBuilderSpec<'T> = 
    { Where : ('T -> bool) option; 
     OrderBy : (('T -> obj) * bool) list; 
     GroupBy : ('T -> obj) list } 

let Default = { Where = None; OrderBy = None; GroupBy = [] } 

這允許客戶端代碼,以使用「與」語法新副本:

let myVal = { Default with Where = fun _ -> true } 

然後,您可以使用「與」做「設爲myVal」的更多副本,如果你願意的話,因而「打造」了更多的屬性,同時保持原有不變:

let myVal' = { myVal with GroupBy = [fun x -> x.Whatever] } 
+0

這正是我要做的,如果這將主要用於F#。不幸的是,它需要看起來像一個典型的.NET類型。雖然,這是我用於內部維護狀態的方法。 – Daniel

+0

在這種情況下,你可以添加方法到你的記錄類型來創建它的新副本?語法是「... with x.AddWhere whereFunc = ... // create copy」。這些看起來像其他.NET語言的普通.NET方法,記錄的行爲類似於字符串類。 – Robert

+0

如果我理解你,這是我在OP中的第二次實現中所做的。我非常喜歡kvb的想法,讓builder類不可變。然後每個方法都可以返回一個傳入更新狀態的新實例。 – Daniel