简体   繁体   中英

How can I DRY out this F# code? (Fluent Interface)

So this is some of the wettest code I've ever written. But it's useful, which is annoying. The reason for all the repetition is because I want to keep the interface fluent. If I augmented the base class (which happens to be View in this case), it would only give back an instance of View , which would prevent me from doing something like

let label = theme.CreateLabel().WithMargin(new Thickness(5.0)).WithText("Hello")

because the Label.Text property is not implemented by the View base class.

So here is my fluent interface. Get ready. It's ugly, and repetitive. But it also works, and is convenient to use.

Have I missed an obvious way to DRY it out?

module ViewExtensions =
    let private withTwoWayBinding<'TElement, 'TProperty, 'TViewModel, 'TView when 'TView :> IViewFor<'TViewModel>>(viewModel: 'TViewModel, view: 'TView, viewModelProperty: Expr<'TViewModel -> 'TProperty>, viewProperty: Expr<'TView -> 'TProperty>) (element: 'TElement) = 
        view.Bind(viewModel, ExpressionConversion.toLinq viewModelProperty, ExpressionConversion.toLinq viewProperty) |> ignore
        element
    let private withHorizontalOptions<'TElement when 'TElement :> View> options (element: 'TElement) =
        element.HorizontalOptions <- options
        element
    let private withVerticalOptions<'TElement when 'TElement :> View> options (element: 'TElement) =
        element.VerticalOptions <- options
        element
    let private withAlignment<'TElement when 'TElement :> View> horizontalOptions verticalOptions (control: 'TElement) =
        control |> withHorizontalOptions horizontalOptions |> withVerticalOptions verticalOptions
    let private withMargin<'TElement when 'TElement :> View> margin (element: 'TElement) = 
        element.Margin <- margin
        element
    let private withActions<'TElement> (actions: ('TElement -> unit)[]) (element: 'TElement) = 
        for action in actions do action(element)
        element
    type Xamarin.Forms.Entry with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) = withTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: Entry -> unit) = this.With([|action|])
    type Xamarin.Forms.Grid with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: Grid -> unit) = this.With([|action|])
    type Xamarin.Forms.StackLayout with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: StackLayout -> unit) = this.With([|action|])
    type Xamarin.Forms.Button with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.WithText(text) = this.Text <- text; this
        member this.With(actions) = withActions actions this
        member this.With(action: Button -> unit) = this.With([|action|])
    type Xamarin.Forms.Switch with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) = withTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: Switch -> unit) = this.With([|action|])
    type Xamarin.Forms.Label with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.WithText(text) = this.Text <- text; this
        member this.With(actions) = withActions actions this
        member this.With(action: Label -> unit) = this.With([|action|])

UPDATE

So thanks to your help, the answer is yes, I was missing something obvious. As TheQuickBrownFox explained, if I change the fluent interface to something of the form

let label = theme.CreateLabel() |> withMargin(new Thickness(5.0)) |> withContent("Hello")

then the monster you see above can be replaced in its entirety by

module ViewExtensions =
    let withTwoWayBinding<'TElement, 'TProperty, 'TViewModel, 'TView when 'TView :> IViewFor<'TViewModel>>(viewModel: 'TViewModel, view: 'TView, viewModelProperty: Expr<'TViewModel -> 'TProperty>, viewProperty: Expr<'TView -> 'TProperty>) (element: 'TElement) = 
        view.Bind(viewModel, ExpressionConversion.toLinq viewModelProperty, ExpressionConversion.toLinq viewProperty) |> ignore
        element
    let withHorizontalOptions options (element: #View) = element.HorizontalOptions <- options; element
    let withVerticalOptions options (element: #View) = element.VerticalOptions <- options; element
    let withAlignment horizontalOptions verticalOptions element = element |> withHorizontalOptions horizontalOptions |> withVerticalOptions verticalOptions
    let withMargin margin (element: #View) = element.Margin <- margin; element
    let withCaption text (element: #Button) = element.Text <- text; element
    let withText text (element: #Entry) = element.Text <- text; element
    let withContent text (element: #Label) = element.Text <- text; element
    let withSetUpActions<'TElement> (actions: ('TElement -> unit)[]) (element: 'TElement) = (for action in actions do action(element)); element
    let withSetUpAction<'TElement> (action: 'TElement -> unit) = withSetUpActions([|action|])

This code deletion is very pleasing indeed.

The idiomatic F# approach to fluent interfaces is just to use the pipe forward operator |>

module ViewHelpers
    let withMargin margin element = ...
    let withText text element = ...

open ViewHelpers

let label = theme.CreateLabel() |> withMargin (new Thickness(5.0)) |> withText "Hello"

I think you can also shorten your function signatures using flexible types :

let withMargin margin (element: #View) = ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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