F#中的策略模式

时间:2014-04-08 14:20:58

标签: f# functional-programming strategy-pattern

在C#中,我有以下代码:

public class SomeKindaWorker
{
    public double Work(Strategy strat)
    {
        int i = 4;
        // some code ...
        var s = strat.Step1(i);
        // some more code ...
        var d = strat.Step2(s);
        // yet more code ...
        return d;
    }
}

这是一段代码,可以通过使用提供的策略对象来填充部分实现来完成某种工作。注意:一般来说,策略对象不包含状态;它们只是多态地提供了各个步骤的实现。

策略类如下所示:

public abstract class Strategy
{
    public abstract string Step1(int i);
    public abstract double Step2(string s);
}

public class StrategyA : Strategy
{
    public override string Step1(int i) { return "whatever"; }
    public override double Step2(string s) { return 0.0; }
}

public class StrategyB : Strategy
{
    public override string Step1(int i) { return "something else"; }
    public override double Step2(string s) { return 4.5; }
}

观察:通过使用lambdas(并完全摆脱策略对象)可以在C#中实现相同的效果,但是这个实现的好处是扩展类有它们的Step1和Step2一起实现。

问题:F#中这个想法的惯用实现是什么?

思想:

我可以将单个步骤函数注入到Work函数中,类似于观察中的想法。

我还可以创建一个收集两个函数的类型,并通过以下方式传递该类型的

type Strategy = { Step1: int -> string; Step2: string -> double }
let strategyA = { Step1 = (fun i -> "whatever"); Step2 = fun s -> 0.0 }
let strategyB = { Step1 = (fun i -> "something else"); Step2 = fun s -> 4.5 }

这似乎是我想要实现的最接近的匹配:它使实现步骤保持紧密,以便可以将它们作为一堆进行检查。但是这个想法(创建一个仅包含函数值的类型)在功能范例中是惯用的吗?还有其他想法吗?

4 个答案:

答案 0 :(得分:9)

您应该在{}使用F# object expressions

type IStrategy =
    abstract Step1: int -> string
    abstract Step2: string -> double

let strategyA =
    { new IStrategy with
        member x.Step1 _ = "whatever"
        member x.Step2 _ = 0.0 }

let strategyB =
    { new IStrategy with
        member x.Step1 _ = "something else"
        member x.Step2 _ = 4.5 }

您将获得两全其美:继承的灵活性和功能的轻量级语法。

使用功能记录的方法很好但不是最惯用的方法。以下是F# Component Design Guidelines(第9页)建议的内容:

  

在F#中,有许多方法可以表示操作字典,例如使用函数元组或函数记录。通常,我们建议您使用接口类型   目的。

修改

使用with记录更新非常好,但当记录字段是函数时,intellisense并不能很好地工作。使用接口,您可以通过在对象表达式中传递参数来进一步自定义,例如

let createStrategy label f =
    { new IStrategy with 
        member x.Step1 _ = label
        member x.Step2 s =  f s }

或在需要更多可扩展性时使用interface IStrategy with(它与C#方法相同)的接口实现。

答案 1 :(得分:6)

你提到在C#中简单使用lambdas的可能性。对于步骤很少的策略,这通常是惯用的。它真的很方便:

let f step1 step2 = 
    let i = 4
    // ...
    let s = step1 i
    // ...
    let d = step2 s
    //  ...
    d

不需要接口定义或对象表达式; step1step2的推断类型就足够了。在没有高阶函数的语言中(我相信这是发明策略模式的设置),你没有这个选项,而是需要,比如接口。

此处的函数f可能不关心step1step2是否相关。但是如果调用者这样做,没有什么能阻止他将它们捆绑在一个数据结构中。例如,使用@pad的答案,

let x = f strategyA.Step1 strategyA.Step2
// val it = 0.0 

一般来说,“习惯的方式”取决于为什么你首先考虑战略模式。战略模式是关于将功能拼接在一起;高阶函数通常也非常有用。

答案 2 :(得分:5)

以下是问题的更具功能性的方法:

type Strategy =
    | StrategyA
    | StrategyB

let step1 i = function
    | StrategyA -> "whatever"
    | StrategyB -> "something else"

let step2 s = function
    | StrategyA -> 0.0
    | StrategyB -> 4.5

let work strategy = 
    let i = 4
    let s = step1 i strategy
    let d = step2 s strategy
    d

答案 3 :(得分:1)

对象表达式一次只支持一个接口。如果您需要两个,请使用类型定义。

type IStrategy =
    abstract Step1: int -> string
    abstract Step2: string -> double

type strategyA() =
    let mutable observers = []

    interface System.IObservable<string> with
        member observable.Subscribe(observer)  =
            observers <- observer :: observers
            { new System.IDisposable with
                 member this.Dispose() =
                    observers <- observers |> List.filter ((<>) observer)}

    interface IStrategy with
        member x.Step1 _ = 
            let result = "whatever"
            observers |> List.iter (fun observer -> observer.OnNext(result))
            result
        member x.Step2 _ = 0.0

type SomeKindaWorker() =
    member this.Work(strategy : #IStrategy) =
        let i = 4
        // some code ...
        let s = strategy.Step1(i)
        // some more code ...
        let d = strategy.Step2(s)
        // yet more code ...
        d

let strat = strategyA()
let subscription = printfn "Observed: %A" |> strat.Subscribe
SomeKindaWorker().Work(strat) |> printfn "Result: %A"
subscription.Dispose()

我经常看到的另一种模式是从函数中返回对象表达式。

let strategyB(setupData) =
    let b = 3.0 + setupData

    { new IStrategy with
        member x.Step1 _ = "something else"
        member x.Step2 _ = 4.5 + b }

这可以让您初始化策略。

SomeKindaWorker().Work(strategyB(2.0)) |> printfn "%A"