【问题标题】:Strategy pattern in F#F# 中的策略模式
【发布时间】:2014-04-08 14:20:58
【问题描述】:

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

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

策略类如下所示:

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

观察:在 C# 中可以通过使用 lambda 实现相同的效果(并完全摆脱策略对象),但这种实现的好处是扩展类有自己的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 }

这似乎与我想要实现的目标最接近:它将实施步骤紧密结合在一起,以便可以将它们作为一组进行检查。但是这个想法(创建一个只包含函数值的类型)在函数范式中是惯用的吗?还有其他想法吗?

【问题讨论】:

    标签: f# functional-programming strategy-pattern


    【解决方案1】:

    你应该在这里使用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 进行记录更新非常棒,但是当记录字段是函数时,智能感知就不能很好地工作。使用接口,您可以通过在对象表达式中传递参数来进一步自定义,例如

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

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

    【讨论】:

    • 这看起来很漂亮。但是有一条评论,值 strategyA 和 strategyB 背后的类型现在是匿名的,不能重用(即继承);而在记录方法中,您可以通过使用with 语法替换现有策略的一些步骤来制定新策略。虽然我确实喜欢 this/x 值在需要时可以使用的方式。
    • 很好,这就像 Java 中的匿名类,当我开始使用 C# 时,我真的很怀念它(不明白为什么它从未在那里实现;太方便了)。
    【解决方案2】:

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

    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 
    

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

    【讨论】:

    • 我认为在这种情况下,我更愿意从f 的角度保持step1step2 函数的相关性。我更有可能将一对函数捆绑为一个元组,而不是单独提供每个函数。反过来,我可能会以记录类型(我最初的想法)或对象表达式(@pad 的想法)收集它们。
    • 我认为最佳解决方案取决于f 实际在做什么。在许多情况下,尤其是当策略的组件是功能性的(没有副作用)时,它们并不一定是相关的。但是,当然,远非如此。
    【解决方案3】:

    这里有一个更实用的方法来解决这个问题:

    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
    

    【讨论】:

    • 这看起来同时非常有希望,也非常可怕。一方面,语法轻量级且易于理解;另一方面,任何给定策略的“全貌”分布在不同功能的案例陈述中。您能对此发表评论吗?这种不同的功能分组是我必须习惯的吗?
    • @CSJ 在我看来,这是 FP 与 OOP 的定义特征之一。我在这里更多地谈论这个:programmers.stackexchange.com/a/209616/4132
    • 这个答案(和链接的帖子)非常具有描述性和帮助性。链接帖子中突出的一点是“OOP 鼓励捆绑数据和行为,而函数式编程鼓励将它们分开。”如果除了链接您的其他帖子之外,您还包括在您的答案中,我会立即接受它。 :)
    【解决方案4】:

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

    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"
    

    【讨论】: