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

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

问题描述

最近,我一直在C#中将SOLID进行到相当极端的水平,并且在某个时候意识到,除了如今编写函数之外,我基本上没有做其他事情。在我最近再次开始研究F#之后,我发现对于我现在正在做的大部分事情来说,这可能是更合适的语言选择,因此我想尝试将一个实际的C#项目移植到F#中作为概念证明。我想我可以(以一种非常惯用的方式)发布实际的代码,但是我无法想象这样的架构会像我在C#中一样灵活地工作。



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



现在使用F#,我不会没有类和接口(我知道我可以,但是我认为这不适合进行实际的函数式编程),我没有构造函数注入,也没有IoC容器。我知道我可以使用高阶函数来完成类似Decorator模式的操作,但这似乎并没有给我像构造函数注入类那样的灵活性和可维护性。



考虑以下C#类型:

 公共类Dings 
{
公共字符串Lol {get;组; }

公共字符串Rofl {get;组; }
}

公共接口IGetStuff
{
IEnumerable< Dings> For(向导ID);
}

公共类AsdFilteringGetStuff:IGetStuff
{
private readonly IGetStuff _innerGetStuff;

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

公共IEnumerable< Dings> For(向导ID)
{
返回this._innerGetStuff.For(id).Where(d => d.Lol == asd);
}
}

公共类GeneratingGetStuff:IGetStuff
{
public IEnumerable< Dings>对于(Guid id)
{
IEnumerable< Dings>丁斯

//以某种方式知道如何为ID

return dingse创建正确的丁斯;
}
}

我告诉我的IoC容器解析<$对于 IGetStuff GeneratingGetStuff 的c $ c> AsdFilteringGetStuff 表示自己对该接口的依赖性。现在,如果需要其他过滤器或完全删除过滤器,则可能需要分别实现 IGetStuff 的实现,然后只需更改IoC注册即可。只要界面保持不变,我就不需要在应用程序中接触东西。通过DIP启用的OCP和LSP。



现在我要在F#中做什么?

  type Dings(lol,rofl)= 
成员x.lol = lol
成员x.Rofl = rofl

let GenerateDingse id =
//创建列表

let AsdFilteredDingse id =
GenerateDingse id |> List.filter(fun x-> x.Lol = asd)

我爱多少这是更少的代码,但是我失去了灵活性。是的,我可以在同一位置调用 AsdFilteredDingse GenerateDingse ,因为类型相同-但是我该怎么办决定要呼叫哪一个而不在呼叫站点进行硬编码?另外,尽管这两个函数可以互换,但是现在我不能在不更改此函数的情况下替换 AsdFilteredDingse 中的生成器函数。



下一次尝试:

  let GenerateDingse id = 
//创建列表

让AsdFilteredDingse(generator:System.Guid-> Dings list)id =
生成器id |> ; List.filter(fun x-> x.Lol = asd)

现在我具有可组合性通过使AsdFilteredDingse为高阶函数,但两个函数不再互换。再次考虑,他们可能根本不应该。



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



对于装饰器,定义应为



< pre class = lang-ml prettyprint-override> let GetDingseForId id = AsdFilteredDingse GenerateDingse

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

  let GetDingseForId id = GenerateDingse 

缺点是所有使用其他功能的功能都必须更高订购功能,而我的注册表必须映射我使用的所有函数,因为先前定义的实际功能无法调用后来定义的任何功能,特别是不能调用注册表中的功能。我可能还会遇到注册表映射的循环依赖问题。



这是否有意义?

解决方案

您如何真正构建一个可维护和可演化(更不用说可测试)的F#应用程序?意识到面向对象的构造函数注入非常类似于功能部分函数应用



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

  type Dings = {大声笑:字符串; Rofl:字符串} 

在F#中, IGetStuff 接口可以通过签名简化为单个功能

  Guid-> seq< Dings> 

使用此功能的 client 会将其作为参数:

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

Client 的签名函数是:

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

如您所见,它将目标签名的功能作为输入,并返回



生成器



生成器函数易于编写:

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

GenerateDingse 函数具有以下签名:

 'a-> seq< Dings> 

这实际上是通用的,比 Guid- > seq< Dings> ,但这不是问题。如果只想将 Client GenerateDingse 组成,则可以这样简单地使用它:

 让结果= Client Generatedingse 

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



装饰器



原始装饰器要难一些,但要困难得多。通常,您无需将Decorated(内部)类型添加为构造函数参数,而只需将其作为参数值添加到函数中即可:

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

此函数具有此功能签名:

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

这不是我们想要的,但是用 GenerateDingse很容易

 让组成的id = GenerateDingse id |> AdsFilteredDingse id 

组成的函数具有签名

 'a-> seq< Dings> 

正是我们想要的东西!



您现在可以将 Client 组成的一起使用,像这样:

 让结果=客户组成

仅返回 [{Lol = asd; Rofl = ASD;}]



您没有必须定义组成函数;您也可以当场撰写:

 让result = Client(有趣的ID-> GenerateDingse ID |> AdsFilteredDingse ID )

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



备用装饰器



前面的示例运行良好,但并没有真正地装饰类似的功能。这里是一种替代方法:

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

此函数具有签名:

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

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

  let composition id = GenerateDingse |> AdsFilteredDingse id 

再次,您可以使用 Client 像这样组成

 让结果=客户组成

或像这样的内联:

  let result =客户端(有趣的ID-> gt; GenerateDingse |> AdsFilteredDingse ID)

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



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


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#.

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.

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.

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;
    }
}

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.

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")

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.

Next attempt:

let GenerateDingse id =
    // create list

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

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.

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.

For the decorator, the definition would be

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.

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.

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

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

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

The signature for the Client function is:

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

As you can see, it takes a function of the target signature as input, and returns a list.

Generator

The generator function is easy to write:

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

The GenerateDingse function has this signature:

'a -> seq<Dings>

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

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")

This function has this signature:

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

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

let composed id = GenerateDingse id |> AdsFilteredDingse id

The composed function has the signature

'a -> seq<Dings>

Just what we're looking for!

You can now use Client with composed like this:

let result = Client composed

which will return only [{Lol = "asd"; Rofl = "ASD";}].

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)

This also returns [{Lol = "asd"; Rofl = "ASD";}].

Alternative Decorator

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")

This function has the signature:

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

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

Again, you can use Client with composed like this:

let result = Client composed

or inline like this:

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

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

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天全站免登陆