这个问题非常技术化,它深深地涉及到F# / C#之间的差异。很可能我会漏掉一些东西。如果您发现了概念错误,请评论,我将更新这个问题。
让我们从C#世界开始。假设我有一个简单的业务对象,叫做Person
(但请记住,在我们所处理的业务领域中,有100多个比它复杂得多的对象):
public class Person : IPerson
{
public int PersonId { get; set; }
public string Name { get; set; }
public string LastName { get; set; }
}
我使用依赖注入/控制反转,以便我不必直接传递Person
。相反,我总是使用一个接口(如上所述),称之为IPerson
:
public interface IPerson
{
int PersonId { get; set; }
string Name { get; set; }
string LastName { get; set; }
}
业务需求是将人员信息能够序列化到/从数据库反序列化。比如说我选择使用Entity Framework,但具体的实现对于这个问题似乎不重要。此时,我有一个选项可以引入与“数据库”相关的类,例如EFPerson
:
public class EFPerson : IPerson
{
public int PersonId { get; set; }
public string Name { get; set; }
public string LastName { get; set; }
}
除了相关的数据库相关属性和代码,出于简洁起见我会跳过它们,然后使用反射在Person
和EFPerson
之间复制IPerson
接口的属性,或直接使用EFPerson
(作为IPerson
)或做其他事情。这相当不相关,因为消费者始终会看到IPerson
,所以实现可以随时更改,而消费者不会知道任何有关其的信息。
如果我需要添加属性,那么我会首先更新接口IPerson
(假设我添加了一个属性 DateTime DateOfBirth { get; set; }
),然后编译器会告诉我要修复什么。但是,如果我从接口中删除属性(比如我不再需要LastName
),那么编译器就无法帮助我。但是,我可以编写一个基于反射的测试,以确保IPerson
,Person
,EFPerson
等的属性是相同的。这并不是真正必需的,但是可以完成,然后它将像魔术一样工作(是的,我们确实有这样的测试,它们确实像魔术一样工作)。
现在,让我们进入F#世界。在这里,我们有类型提供程序,它们完全消除了在代码中创建数据库对象的需求:类型提供程序会自动创建它们!
很酷!但是真的吗?
首先,总得有人创建/更新数据库对象,如果有不止一个开发人员参与,那么自然会在不同的分支中进行升级/降级。就我个人的经验而言,当涉及到F#类型提供程序时,这是极其痛苦的一件事情。即使使用C# EF Code First来处理迁移,仍需要进行一些“广泛的萨满舞蹈”才能让F#类型提供程序“高兴”。
其次,在F#世界中,默认情况下所有内容都是不可变的(除非我们将其设置为可变),因此我们显然不想将可变数据库对象传递给上游。这意味着一旦我们从数据库中加载了可变行,我们希望尽快将其转换为“本地”F#不可变结构,以便只在上游使用纯函数。毕竟,使用纯函数可以将所需测试数减少5-50倍,这取决于域。
让我们回到我们的Person
。暂且不考虑任何可能的重新映射(例如将数据库整数转换为F# DU case等类似内容)。因此,我们的F#Person
看起来像这样:
type Person =
{
personId : int
name : string
lastName : string
}
如果“明天”我需要在这个类型中添加dateOfBirth:DateTime
,那么编译器将告诉我需要修复的所有地方。这很棒,因为除了数据库之外,C#编译器不会告诉我需要添加出生日期的地方。F#编译器也不会告诉我需要去表格Person
添加数据库列。但是,在C#中,由于我必须先更新接口,编译器将告诉我必须修复哪些对象,包括数据库对象。
显然,我希望在F#中获得最佳效果。虽然可以使用接口来实现这一点,但这并不符合F#的方式。毕竟,在F#中,DI / IOC的类比方式与传递函数而不是接口通常有所不同。
因此,这里有两个问题。
- 如何在F#世界中轻松管理数据库升级/降级迁移?并且,从何开始实际上在F#世界中进行数据库迁移时,涉及多个开发人员的情况下应该采用什么样的适当方式?
- 如何通过F#的方式实现“C#世界中的最佳方式”,如上所述:当我更新F#类型
Person
并修复需要添加/删除记录属性的所有位置时,如果我没有更新数据库以匹配业务对象,则最合适的F#方式是“失败”要么在编译时,要么至少在测试时?