F# 中的应用程序架构/组合 [英] Application architecture/composition in F#

查看:28
本文介绍了F# 中的应用程序架构/组合的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have been doing SOLID in C# to a pretty extreme level in recent times and at some point realized I'm essentially not doing much else than composing functions nowadays. And after I recently started looking at F# again, I figured that it would probably be the much more appropriate choice of language for much of what I'm doing now, so I'd like to try and port a real world C# project to F# as a proof of concept. I think I could pull off the actual code (in a very un-idiomatic fashion), but I can't imagine what an architecture would look like that allows me to work in a similarly flexible fashion as in C#.

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

What I mean by this is that I have lots of small classes and interfaces that I compose using an IoC container, and I also use patterns like Decorator and Composite a lot. This results in an (in my opinion) very flexible and evolvable overall architecture that allows me to easily replace or extend functionality at any point of the application. Depending on how big the required change is, I might only need to write a new implementation of an interface, replace it in the IoC registration and be done. Even if the change is bigger, I can replace parts of the object graph while the rest of the application simply stands as it did before.

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

Now with F#, I don't have classes and interfaces (I know I can, but I think that's beside the point when I want to do actual functional programming), I don't have constructor injection, and I don't have IoC containers. I know I can do something like a Decorator pattern using higher order functions, but that doesn't quite seem to give me the same kind of flexibility and maintainability as classes with constructor injection.

考虑这些 C# 类型:

Consider these C# types:

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 容器为 IGetStuffGeneratingGetStuff 解析 AsdFilteringGetStuff 为它自己与该接口的依赖关系.现在,如果我需要不同的过滤器或完全删除过滤器,我可能需要 IGetStuff 的相应实现,然后只需更改 IoC 注册.只要界面保持不变,我就不需要接触应用的内容.OCP 和 LSP,通过 DIP 启用.

I'll tell my IoC container to resolve AsdFilteringGetStuff for IGetStuff and GeneratingGetStuff for its own dependency with that interface. Now if I need a different filter or remove the filter altogether, I may need the respective implementation of IGetStuff and then simply change the IoC registration. As long as the interface stays the same, I don't need to touch stuff within the application. OCP and LSP, enabled by DIP.

现在我在 F# 中做什么?

Now what do I do in 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 中的生成器函数.这不是很好.

I love how much less code this is, but I lose flexibility. Yes, I can call AsdFilteredDingse or GenerateDingse in the same place, because the types are the same - but how do I decide which one to call without hard coding it at the call site? Also, while these two functions are interchangeable, I now cannot replace the generator function inside AsdFilteredDingse without changing this function as well. This isn't very nice.

下一次尝试:

let GenerateDingse id =
    // create list

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

现在我通过使 AsdFilteredDingse 成为高阶函数而具有可组合性,但这两个函数不再可以互换.再想一想,他们可能无论如何都不应该如此.

Now I have composability by making AsdFilteredDingse a higher order function, but the two functions are not interchangeable anymore. On second thought, they probably shouldn't be anyway.

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

What else could I do? I could mimic the "composition root" concept from my C# SOLID in the last file of the F# project. Most files are just collections of functions, then I have some kind of "registry", which replaces the IoC container, and finally there is one function that I call to actually run the application and that uses functions from the "registry". In the "registry", I know I need a function of type (Guid -> Dings list), which I'll call GetDingseForId. This is the one I call, never the individual functions defined earlier.

对于装饰器,定义是

let GetDingseForId id = AsdFilteredDingse GenerateDingse

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

To remove the filter, I'd change that to

let GetDingseForId id = GenerateDingse

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

The downside(?) of this is that all functions that use other functions would sensibly have to be higher order functions, and my "registry" would have to map all functions that I use, because the actual functions defined earlier can't call any functions defined later, in particular not those from the "registry". I might also run into circular dependency issues with the "registry" mappings.

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

Does any of this make sense? How do you really build an F# application to be maintainable and evolvable (not to mention testable)?

推荐答案

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

This is easy once you realize that Object-Oriented Constructor Injection corresponds very closely to Functional Partial Function Application.

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

First, I'd write Dings as a record type:

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

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

In F#, the IGetStuff interface can be reduced to a single function with the signature

Guid -> seq<Dings>

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

A client using this function would take it as a parameter:

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

Client 函数的签名是:

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

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

As you can see, it takes a function of the target signature as input, and returns a 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,但这不是问题.如果你只想用 GenerateDingse 组合 Client,你可以简单地像这样使用它:

This is actually more generic than Guid -> seq<Dings>, but that's not a problem. If you only want to compose the Client with GenerateDingse, you could simply use it like this:

let result = Client GenerateDingse

这将从 GenerateDingse 返回所有三个 Ding 值.

Which would return all three Ding values from GenerateDingse.

装饰器

原来的 Decorator 有点难,但也不是很多.一般来说,不是将装饰(内部)类型添加为构造函数参数,而是将其作为参数值添加到函数中:

The original Decorator is a little bit more difficult, but not much. In general, instead of adding the Decorated (inner) type as a constructor argument, you just add it as a parameter value to a function:

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

这个函数有这个签名:

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

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

That's not quite what we want, but it's easy to compose it with GenerateDingse:

let composed id = GenerateDingse id |> AdsFilteredDingse id

composed 函数有签名

'a -> seq<Dings>

正是我们正在寻找的!

您现在可以像这样使用 Clientcomposed:

You can now use Client with composed like this:

let result = Client composed

它只会返回 [{Lol = "asd";Rofl = "ASD";}].

您不必先定义composed函数;你也可以当场作曲:

You don't have to define the composed function first; you can also compose it on the spot:

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

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

替代装饰器

前面的例子运行良好,但并没有真的装饰一个类似的函数.这是一个替代方案:

The previous example works well, but doesn't really Decorate a similar function. Here's an alternative:

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

这个函数有签名:

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

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

As you can see, the f argument is another function with the same signature, so it more closely resembles the Decorator pattern. You can compose it like this:

let composed id = GenerateDingse |> AdsFilteredDingse id

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

Again, you can use Client with composed like this:

let result = Client composed

或者像这样内联:

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

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

For more examples and principles for composing entire applications with F#, see my on-line course on Functional architecture with F#.

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

For more about Object-Oriented Principles and how they map to Functional Programming, see my blog post on the SOLID principles and how they apply to FP.

这篇关于F# 中的应用程序架构/组合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆