【问题标题】:Application architecture/composition in F#F# 中的应用程序架构/组合
【发布时间】:2014-04-11 09:48:01
【问题描述】:

我最近在 C# 中将 SOLID 开发到了一个相当极端的水平,并且在某些时候意识到我现在除了编写函数之外几乎没有做太多其他事情。在我最近再次开始关注 F# 之后,我认为对于我现在所做的大部分工作来说,它可能是更合适的语言选择,所以我想尝试将一个真实世界的 C# 项目移植到 F#作为概念证明。我想我可以完成实际代码(以一种非常不惯用的方式),但我无法想象一个架构会是什么样子,它可以让我以与 C# 类似的灵活方式工作。

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

现在使用 F#,我没有类和接口(我知道我可以,但我认为这与我想做实际的函数式编程无关),我没有构造函数注入,而且我没有没有 IoC 容器。我知道我可以使用高阶函数来做类似装饰器模式的事情,但这似乎并没有给我提供与使用构造函数注入的类相同的灵活性和可维护性。

考虑这些 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;
    }
}

我会告诉我的 IoC 容器将 AsdFilteringGetStuff 解析为 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")

我喜欢这样的代码少得多,但我失去了灵活性。是的,我可以在同一个地方打电话给AsdFilteredDingseGenerateDingse,因为类型是一样的——但是我如何决定调用哪一个而不在调用站点硬编码呢?此外,虽然这两个函数可以互换,但我现在无法在不更改此函数的情况下替换 AsdFilteredDingse 中的生成器函数。这不是很好。

下一次尝试:

let GenerateDingse id =
    // create list

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

现在我通过将 AsdFilteredDingse 设为高阶函数来实现可组合性,但这两个函数不再可互换。再想一想,他们可能不应该是这样。

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

对于装饰器,定义是

let GetDingseForId id = AsdFilteredDingse GenerateDingse

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

let GetDingseForId id = GenerateDingse

这样做的缺点(?)是所有使用其他函数的函数显然必须是高阶函数,并且我的“注册表”必须映射我使用的 所有 函数,因为前面定义的实际函数不能调用后面定义的任何函数,尤其是那些来自“注册表”的函数。我也可能会遇到“注册表”映射的循环依赖问题。

这些有意义吗?您如何真正构建可维护和可发展(更不用说可测试)的 F# 应用程序?

【问题讨论】:

    标签: f# inversion-of-control composition solid-principles


    【解决方案1】:

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

    首先,我将Dings 写为记录类型:

    type Dings = { Lol : string; Rofl : string }
    

    在 F# 中,IGetStuff 接口可以简化为带有签名的单个函数

    Guid -> seq<Dings>
    

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

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

    Client 函数的签名是:

    (Guid -> #seq<'b>) -> 'b list
    

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

    生成器

    生成器函数很容易写:

    let GenerateDingse id =
        seq {
            yield { Lol = "Ha!"; Rofl = "Ha ha ha!" }
            yield { Lol = "Ho!"; Rofl = "Ho ho ho!" }
            yield { Lol = "asd"; Rofl = "ASD" } }
    

    GenerateDingse 函数具有以下签名:

    'a -> seq<Dings>
    

    这实际上更多Guid -&gt; seq&lt;Dings&gt; 通用,但这不是问题。如果您只想将ClientGenerateDingse 组合在一起,您可以像这样简单地使用它:

    let result = Client GenerateDingse
    

    这将返回来自GenerateDingse 的所有三个Ding 值。

    装饰器

    原来的装饰器有点困难,但并不多。通常,您无需将 Decorated(内部)类型添加为构造函数参数,而是将其作为参数值添加到函数中:

    let AdsFilteredDingse id s = s |> Seq.filter (fun d -> d.Lol = "asd")
    

    这个函数有这个签名:

    'a -> seq<Dings> -> seq<Dings>
    

    这不是我们想要的,但用GenerateDingse 组合起来很容易:

    let composed id = GenerateDingse id |> AdsFilteredDingse id
    

    composed 函数有签名

    'a -> seq<Dings>
    

    正是我们正在寻找的!

    您现在可以像这样使用Clientcomposed

    let result = Client composed
    

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

    您没有首先定义composed 函数;也可以现场作曲:

    let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)
    

    这也返回[{Lol = "asd"; Rofl = "ASD";}]

    另类装饰器

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

    let AdsFilteredDingse id f = f id |> Seq.filter (fun d -> d.Lol = "asd")
    

    这个函数有签名:

    'a -> ('a -> #seq<Dings>) -> seq<Dings>
    

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

    let composed id = GenerateDingse |> AdsFilteredDingse id
    

    同样,您可以像这样使用Clientcomposed

    let result = Client composed
    

    或像这样内联:

    let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)
    

    有关使用 F# 编写整个应用程序的更多示例和原则,请参阅my on-line course on Functional architecture with F#

    有关面向对象原则以及它们如何映射到函数式编程的更多信息,请参阅my blog post on the SOLID principles and how they apply to FP

    【讨论】:

    • 感谢马克的广泛回答。然而,我仍然不清楚的是,where 应该在一个具有大量功能的应用程序中进行编写。同时,我将一个小型 C# 应用程序移植到 F# 并将其发布到 here 以供审查;我在那里将提到的“注册表”实现为Composition 模块,本质上是一个“穷人的DI”组合根。这是一个可行的方法吗?
    • @TeaDrivenDev 您可能已经注意到,我已经更新了这篇文章,添加了一个指向更深入挖掘的博客文章的链接。谈到组合,F# 的一个有趣之处是编译器可以防止循环引用,所以composition and decoupling is a lot safer than in C#。您链接到的代码在最后编写,所以看起来不错。
    • 函数的替代定义可以是:let AdsFilteredDingse = Seq.filter (fun d -&gt; d.Lol = "asd")let composed = GenerateDingse &gt;&gt; AdsFilteredDingse。这样您就可以使用函数组合运算符,并且可以省略 AdsFilteredDingse 中的 id。但是,正如 Mark 所提到的,它并不是真正的 OO 最初意义上的装饰器。
    • 我已经尝试过了,但它给了我一个编译器错误,虽然composed 的签名看起来不错:"error FS0030: 值限制。已推断出值'composed'让泛型类型 val 组成: ('_a -> seq) 要么明确地为 'composed' 提供参数,或者,如果你不打算让它成为泛型,添加一个类型注释。"
    猜你喜欢
    • 2016-03-03
    • 2019-12-12
    • 2011-03-12
    • 2015-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多