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


76

最近,我一直在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;
    }
}

我会告诉我的IoC容器来解决AsdFilteringGetStuffIGetStuff,并GeneratingGetStuff与该接口自身的依赖。现在,如果需要其他过滤器或完全删除过滤器,则可能需要各自的实现,IGetStuff然后简单地更改IoC注册。只要接口保持不变,我并不需要接触的东西的应用。由DIP启用的OCP和LSP。

现在我要在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 list的函数,我将其称为GetDingseForId。这就是我所说的那个,而不是前面定义的单个函数。

对于装饰器,定义为

let GetDingseForId id = AsdFilteredDingse GenerateDingse

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

let GetDingseForId id = GenerateDingse

不利之处是所有使用其他功能的功能都必须是高阶功能,而我的“注册表”必须映射我使用的所有功能,因为之前定义的实际功能无法调用任何功能。稍后定义的功能,特别是不是来自“注册表”的功能。我可能还会遇到“注册表”映射的循环依赖问题。

这有道理吗?您如何真正构建可维护和可演化(更不用说可测试)的F#应用程序?

Answers:


59

一旦意识到面向对象的构造函数注入与Function Partial Function Application非常紧密地对应,这将很容易。

首先,我将其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 -> seq<Dings>,但是这不是一个问题。如果只想Client与with组合GenerateDingse,则可以像这样简单地使用它:

let result = Client GenerateDingse

它将Ding从返回所有三个值GenerateDingse

装饰器

原始的Decorator有点困难,但难度不大。通常,您无需将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>

正是我们想要的!

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

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参数是另一个具有相同签名的函数,因此它与Decorator模式更相似。您可以这样编写:

let composed id = GenerateDingse |> AdsFilteredDingse id

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

let result = Client composed

或像这样的内联:

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

有关使用F#组成整个应用程序的更多示例和原理,请参阅有关使用F#进行功能体系结构的在线课程

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


5
感谢Mark的广泛回答。但是,我仍然不清楚的是在一个应用程序中何处包含数量不多的功能。同时,我已经将一个小型C#应用程序移植到F#并将其发布在此处进行审查;我在那里将提到的“注册表”实现为一个Composition模块,本质上是一个“穷人的DI”组成根。这是可行的方法吗?
TeaDrivenDev 2014年

@TeaDrivenDev您可能已经注意到,我通过向博客文章添加链接来更新了该文章,该博客文章进行了更深入的介绍。关于合成,F#有趣的事情之一是编译器可以防止循环引用,因此合成和解耦比C#中的安全得多。您链接到的代码将在最后编写,因此看起来不错。
Mark Seemann 2014年

函数的替代定义可以是:let AdsFilteredDingse = Seq.filter (fun d -> d.Lol = "asd")let composed = GenerateDingse >> AdsFilteredDingse。这样,您就可以使用函数组合运算符,并且可以从AdsFilteredDingse中省略ID。但是,正如Mark所提到的,从OO的最初意义上讲,它并不是真正的装饰器。
Simon Stender Boisen 2014年

我已经尝试过,但是它给了我一个编译器错误,尽管的签名composed看起来不错:“错误FS0030:值限制。值“ composed”已推断为具有通用类型val组成:('_a-> seq < Dings>)要么将“ composited”的参数设为明确,要么,如果您不希望它泛泛,则添加类型注释。”
Mark Seemann 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.