应用程序体系结构/在F#
在C#中,我一直在做SOLID到最近的一个非常极端的水平,并在某种程度上意识到,我现在基本上没有比现在编写函数更多的东西了。 在我最近又开始研究F#之后,我认为这可能是现在我所做的大部分更合适的语言select,所以我想尝试将一个真实世界的C#项目移植到F#作为概念的certificate。 我想我可以把实际的代码(非常不习惯的方式)拉下来,但我无法想象一个架构是什么样子的,这使得我可以像在C#中一样灵活的工作。
我的意思是,我有很多使用IoC容器编写的小类和接口,而且我也使用Decorator和Composite等模式。 这导致(在我看来)非常灵活和可演化的整体架构,允许我轻松地replace或扩展应用程序的任何点的function。 根据所需更改的大小,我可能只需要编写一个新的接口实现,将其replace为IoC注册并完成。 即使变化较大,我可以replace对象图的一部分,而应用程序的其余部分就像以前一样。
现在用F#,我没有类和接口(我知道我可以,但我认为这是在我想要做实际的函数式编程),我没有构造函数注入,我没有IoC容器。 我知道我可以使用更高阶的函数来做类似Decorator模式的事情,但是这看起来不像给构造函数注入类那样具有同样的灵活性和可维护性。
考虑这些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容器来parsingAsdFilteringGetStuff
和GeneratingGetStuff
IGetStuff
作为它自己的依赖关系。 现在,如果我需要一个不同的filter或完全删除filter,我可能需要相应的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")
我爱这个代码less了多less,但是我失去了灵活性。 是的,我可以在同一个地方调用AsdFilteredDingse
或GenerateDingse
,因为types是相同的 – 但是如何在呼叫站点不用硬编码来决定调用哪一个呢? 另外,虽然这两个函数是可以互换的,但是我现在不能在AsdFilteredDingse
replacegenerator函数, 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中的“组合根”概念。 大多数文件只是function的集合,然后我有某种“registry”,它取代了IoC容器,最后有一个函数,我调用实际运行的应用程序,并使用“registry”的function。 在“registry”中,我知道我需要一个types(Guid – > Dings列表)的函数,我将调用GetDingseForId
。 这是我所称的,而不是前面定义的单个函数。
对于装饰者来说,定义是
let GetDingseForId id = AsdFilteredDingse GenerateDingse
要删除filter,我会改变
let GetDingseForId id = GenerateDingse
其缺点(?)是所有使用其他函数的函数都必须是高阶函数,而我的“registry”必须映射我使用的所有函数,因为之前定义的实际函数不能调用后面定义的函数,特别是那些来自“registry”的函数。 我也可能遇到与“registry”映射的循环依赖问题。
这有什么意义吗? 你如何真正构build一个F#应用程序来维护和演化(更不用提testing了)?
一旦你意识到面向对象的构造器注入非常接近于function性的局部函数应用 ,这很容易。
首先,我会把Dings
写成一个loggingtypes:
type Dings = { Lol : string; Rofl : string }
在F#中, IGetStuff
接口可以简化为具有签名的单个函数
Guid -> seq<Dings>
使用这个函数的客户端将把它作为参数:
let Client getStuff = getStuff(Guid("055E7FF1-2919-4246-876E-1DA71980BE9C")) |> Seq.toList
Client
function的签名是:
(Guid -> #seq<'b>) -> 'b list
如您所见,它将目标签名的函数作为input,并返回一个列表。
发电机
生成器function很容易编写:
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>
更通用,但这不是问题。 如果您只想用GenerateDingse
编写Client
,可以简单地使用它:
let result = Client GenerateDingse
这将返回来自GenerateDingse
所有三个Ding
值。
装饰
原来的装饰者有点难度,但不是很多。 通常,不要将Decorated(inner)types作为构造函数参数添加,而只需将其作为参数值添加到函数中即可:
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
function有签名
'a -> seq<Dings>
只是我们正在寻找!
你现在可以像这样使用Client
:
let result = Client composed
这将只返回[{Lol = "asd"; Rofl = "ASD";}]
[{Lol = "asd"; Rofl = "ASD";}]
。
您不必首先定义composed
函数; 你也可以在现场撰写:
let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)
这也返回[{Lol = "asd"; Rofl = "ASD";}]
[{Lol = "asd"; Rofl = "ASD";}]
。
替代装饰
前面的例子运行良好,但并没有真正装饰类似的function。 这是一个替代scheme:
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
再次,你可以使用这样的Client
:
let result = Client composed
或者像这样内联:
let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)
有关使用F#编写整个应用程序的更多示例和原则,请参阅我在F#上的function体系结构的在线课程 。
有关面向对象的原则及其如何映射到函数式编程的更多信息,请参阅我的博客文章,了解SOLID原理以及它们如何应用于FP 。