F#中的应用程序架构/组合

Tea*_*Dev 72 f# inversion-of-control composition solid-principles

我最近在C#中将SOLID做到了非常极端的水平,并且在某些时候意识到我现在基本上没有做太多的事情.在我最近再次开始研究F#之后,我认为对于我现在正在做的很多事情,它可能是更合适的语言选择,所以我想尝试将一个真实的C#项目移植到F#作为概念的证明.我想我可以推出实际的代码(以一种非惯用的方式),但我无法想象一个架构会是什么样的,它允许我以与C#类似的灵活方式工作.

我的意思是我有很多小类和接口,我使用IoC容器编写,我也使用了像Decorator和Composite这样的模式.这导致(在我看来)非常灵活和可演化的整体架构,使我能够在应用程序的任何位置轻松替换或扩展功能.根据所需更改的大小,我可能只需要编写一个新的接口实现,在IoC注册中替换它并完成.即使更改更大,我也可以替换部分对象图,而应用程序的其余部分只是像以前一样.

现在使用F#,我没有类和接口(我知道我可以,但我认为这与我想要进行实际函数式编程的时间不同),我没有构造函数注入,而且我没有IoC容器.我知道我可以使用更高阶函数做类似Decorator模式的东西,但这似乎并没有给我带有构造函数注入的类的灵活性和可维护性.

考虑这些C#类型:

public class Dings
{
    public string Lol { get; set; }

    public string Rofl { get; set; }
}

public interface IGetStuff
{
    IEnumerable<Dings> For(Guid id);
}

public class AsdFilteringGetStuff : IGetStuff
{
    private readonly IGetStuff _innerGetStuff;

    public AsdFilteringGetStuff(IGetStuff innerGetStuff)
    {
        this._innerGetStuff = innerGetStuff;
    }

    public IEnumerable<Dings> For(Guid id)
    {
        return this._innerGetStuff.For(id).Where(d => d.Lol == "asd");
    }
}

public class GeneratingGetStuff : IGetStuff
{
    public IEnumerable<Dings> For(Guid id)
    {
        IEnumerable<Dings> dingse;

        // somehow knows how to create correct dingse for the ID

        return dingse;
    }
}
Run Code Online (Sandbox Code Playgroud)

我将告诉我的IoC容器解析AsdFilteringGetStufffor IGetStuffGeneratingGetStuff它自己与该接口的依赖关系.现在,如果我需要一个不同的过滤器或完全删除过滤器,我可能需要相应的实现,IGetStuff然后只需更改IoC注册.只要接口保持不变,我并不需要接触的东西的应用.OCP和LSP,由DIP启用.

现在我在F#做什么?

type Dings (lol, rofl) =
    member x.Lol = lol
    member x.Rofl = rofl

let GenerateDingse id =
    // create list

let AsdFilteredDingse id =
    GenerateDingse id |> List.filter (fun x -> x.Lol = "asd")
Run Code Online (Sandbox Code Playgroud)

我喜欢这个代码少得多,但我失去了灵活性.是的,我可以打电话AsdFilteredDingseGenerateDingse在同一个地方,因为类型是相同的 - 但是如何在呼叫站点没有硬编码的情况下决定拨打哪一个?而且,虽然这两个功能是可以互换的,但我现在也无法在AsdFilteredDingse不更改此功能的情况下更换内部的发生器功能.这不是很好.

下一次尝试:

let GenerateDingse id =
    // create list

let AsdFilteredDingse (generator : System.Guid -> Dings list) id =
    generator id |> List.filter (fun x -> x.Lol = "asd")
Run Code Online (Sandbox Code Playgroud)

现在我通过使AsdFilteredDingse成为更高阶函数来实现可组合性,但这两个函数不再可以互换.第二个想法,他们可能不应该是.

我还能做什么?我可以在F#项目的最后一个文件中模仿我的C#SOLID中的"组合根"概念.大多数文件只是函数的集合,然后我有一些"注册表",它取代了IoC容器,最后有一个函数,我调用实际运行应用程序,并使用"注册表"中的函数.在"注册表"中,我知道我需要一个类型的函数(Guid - > Dings list),我将其称之为GetDingseForId.这是我所说的,而不是之前定义的单个函数.

对于装饰者,定义将是

let GetDingseForId id = AsdFilteredDingse GenerateDingse
Run Code Online (Sandbox Code Playgroud)

要删除过滤器,我会将其更改为

let GetDingseForId id = GenerateDingse
Run Code Online (Sandbox Code Playgroud)

这个的缺点是,所有使用其他函数的函数都必须是高阶函数,而我的"注册表"必须映射我使用的所有函数,因为之前定义的实际函数不能调用任何函数.稍后定义的函数,特别是那些来自"注册表"的函数.我也可能遇到"注册表"映射的循环依赖问题.

这有什么意义吗?你如何真正构建一个可维护和可扩展的F#应用程序(更不用说可测试的)?

Mar*_*ann 58

一旦您意识到面向对象的构造函数注入与功能部分功能应用程序非常接近,这很容易.

首先,我写作Dings记录类型:

type Dings = { Lol : string; Rofl : string }
Run Code Online (Sandbox Code Playgroud)

在F#中,IGetStuff可以使用签名将界面缩减为单个函数

Guid -> seq<Dings>
Run Code Online (Sandbox Code Playgroud)

使用此函数的客户端将其作为参数:

let Client getStuff =
    getStuff(Guid("055E7FF1-2919-4246-876E-1DA71980BE9C")) |> Seq.toList
Run Code Online (Sandbox Code Playgroud)

Client功能的签名是:

(Guid -> #seq<'b>) -> 'b list
Run Code Online (Sandbox Code Playgroud)

如您所见,它将目标签名的函数作为输入,并返回一个列表.

发电机

生成器函数很容易编写:

let GenerateDingse id =
    seq {
        yield { Lol = "Ha!"; Rofl = "Ha ha ha!" }
        yield { Lol = "Ho!"; Rofl = "Ho ho ho!" }
        yield { Lol = "asd"; Rofl = "ASD" } }
Run Code Online (Sandbox Code Playgroud)

GenerateDingse功能有这个签名:

'a -> seq<Dings>
Run Code Online (Sandbox Code Playgroud)

这实际上通用通用Guid -> seq<Dings>,但这不是问题.如果你只想组成Clientwith GenerateDingse,你可以简单地使用它:

let result = Client GenerateDingse
Run Code Online (Sandbox Code Playgroud)

哪个会返回所有三个DingGenerateDingse.

装饰

最初的装饰师有点困难,但并不多.通常,您只需将其作为参数值添加到函数中,而不是将Decorative(内部)类型添加为构造函数参数:

let AdsFilteredDingse id s = s |> Seq.filter (fun d -> d.Lol = "asd")
Run Code Online (Sandbox Code Playgroud)

这个功能有这个签名:

'a -> seq<Dings> -> seq<Dings>
Run Code Online (Sandbox Code Playgroud)

这不是我们想要的,但很容易用以下方法组成GenerateDingse:

let composed id = GenerateDingse id |> AdsFilteredDingse id
Run Code Online (Sandbox Code Playgroud)

composed功能具有签名

'a -> seq<Dings>
Run Code Online (Sandbox Code Playgroud)

正是我们正在寻找的!

您现在可以使用Client具有composed这样的:

let result = Client composed
Run Code Online (Sandbox Code Playgroud)

这将只返回[{Lol = "asd"; Rofl = "ASD";}].

你不具有定义composed第一功能; 你也可以当场撰写:

let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)
Run Code Online (Sandbox Code Playgroud)

这也回来了[{Lol = "asd"; Rofl = "ASD";}].

另类装饰师

前面的示例效果很好,但并没有真正装饰类似的功能.这是另一种选择:

let AdsFilteredDingse id f = f id |> Seq.filter (fun d -> d.Lol = "asd")
Run Code Online (Sandbox Code Playgroud)

此功能具有签名:

'a -> ('a -> #seq<Dings>) -> seq<Dings>
Run Code Online (Sandbox Code Playgroud)

如您所见,该f参数是具有相同签名的另一个函数,因此它更类似于Decorator模式.你可以这样写:

let composed id = GenerateDingse |> AdsFilteredDingse id
Run Code Online (Sandbox Code Playgroud)

同样,你可以用Clientcomposed这样的:

let result = Client composed
Run Code Online (Sandbox Code Playgroud)

或者像这样内联:

let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)
Run Code Online (Sandbox Code Playgroud)

有关使用F#编写整个应用程序的更多示例和原则,请参阅我使用F#进行功能架构的在线课程.

有关面向对象原则以及它们如何映射到函数式编程的更多信息,请参阅我的博客文章,了解SOLID原则以及它们如何应用于FP.

  • 感谢Mark提供了广泛的答案.然而,我仍然不清楚的是**在一个应用程序中组合而不仅仅是一些微不足道的函数.我平时将一个小的C#应用​​程序移植到F#并发布到[这里](http://codereview.stackexchange.com/questions/43893/first-real-world-f-application-how-good-idiomatic-是为了审查; 我在那里实现了所提到的"注册表"作为"Composition"模块,本质上是一个"穷人的DI"组合根.这是一种可行的方法吗? (5认同)