在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 }
这似乎是我想要实现的最接近的匹配:它使实现步骤保持紧密,以便可以将它们作为一堆进行检查。但是这个想法(创建一个仅包含函数值的类型)在功能范例中是惯用的吗?还有其他想法吗?
答案 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
不需要接口定义或对象表达式; step1
和step2
的推断类型就足够了。在没有高阶函数的语言中(我相信这是发明策略模式的设置),你没有这个选项,而是需要,比如接口。
此处的函数f
可能不关心step1
和step2
是否相关。但是如果调用者这样做,没有什么能阻止他将它们捆绑在一个数据结构中。例如,使用@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"