【发布时间】:2014-04-11 09:48:01
【问题描述】:
我最近在 C# 中将 SOLID 开发到了一个相当极端的水平,并且在某些时候意识到我现在除了编写函数之外几乎没有做太多其他事情。在我最近再次开始关注 F# 之后,我认为对于我现在所做的大部分工作来说,它可能是更合适的语言选择,所以我想尝试将一个真实世界的 C# 项目移植到 F#作为概念证明。我想我可以完成实际代码(以一种非常不惯用的方式),但我无法想象一个架构会是什么样子,它可以让我以与 C# 类似的灵活方式工作。
我的意思是,我有很多使用 IoC 容器编写的小类和接口,而且我还经常使用装饰器和复合等模式。这导致了(在我看来)非常灵活和可发展的整体架构,使我能够轻松地在应用程序的任何位置替换或扩展功能。根据所需更改的大小,我可能只需要编写一个新的接口实现,在 IoC 注册中替换它并完成。即使更改更大,我也可以替换部分对象图,而应用程序的其余部分则保持原样。
现在使用 F#,我没有类和接口(我知道我可以,但我认为这与我想做实际的函数式编程无关),我没有构造函数注入,而且我没有没有 IoC 容器。我知道我可以使用高阶函数来做类似装饰器模式的事情,但这似乎并没有给我提供与使用构造函数注入的类相同的灵活性和可维护性。
考虑这些 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 注册。只要界面保持不变,我就不需要接触应用程序内的东西。 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")
我喜欢这样的代码少得多,但我失去了灵活性。是的,我可以在同一个地方打电话给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 列表),我将其称为GetDingseForId。这是我调用的函数,而不是之前定义的单个函数。
对于装饰器,定义是
let GetDingseForId id = AsdFilteredDingse GenerateDingse
要删除过滤器,我会将其更改为
let GetDingseForId id = GenerateDingse
这样做的缺点(?)是所有使用其他函数的函数显然必须是高阶函数,并且我的“注册表”必须映射我使用的 所有 函数,因为前面定义的实际函数不能调用后面定义的任何函数,尤其是那些来自“注册表”的函数。我也可能会遇到“注册表”映射的循环依赖问题。
这些有意义吗?您如何真正构建可维护和可发展(更不用说可测试)的 F# 应用程序?
【问题讨论】:
标签: f# inversion-of-control composition solid-principles