最近,我一直在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容器来解决AsdFilteringGetStuff
的IGetStuff
,并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")
我喜欢这少多少代码,但是我失去了灵活性。是的,我可以打电话AsdFilteredDingse
或GenerateDingse
在同一地方打电话,因为类型是相同的-但是如何确定要呼叫的那一个而不在呼叫现场对其进行硬编码?另外,虽然这两个功能是可以互换的,但现在我不能在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#应用程序?
Composition
模块,本质上是一个“穷人的DI”组成根。这是可行的方法吗?