函数式编程和解耦
Functional programming and decoupling
我是您的经典 OOP 开发人员。然而,自从我发现纯函数式编程语言以来,我一直对 为什么 很感兴趣,因为 OOP 似乎以合理的方式解决了大多数业务案例。
现在,在我的软件开发经验中,我已经到了寻求更简洁和更具表现力的语言的地步。我通常用 C# 编写我的软件,但对于我的最新项目,我决定采取飞跃并使用 F# 构建业务服务。在这样做的过程中,我发现很难理解如何使用纯函数式方法完成解耦。
案例是这样的。我有一个数据源,它是 WooCommerce,但我不想将我的函数定义绑定到 那个 特定数据源。
在 C# 中,很明显我想要一个看起来像这样的服务
public record Category(string Name);
public interface ICategoryService
{
Task<IEnumerable<Category>> GetAllAsync();
}
// With a definition for the service that specifies WooCommerce
public class WcCategoryService : ICategoryService
{
private readonly WCRestEndpoint wcRest;
// WooCommerce specific dependencies
public WcCategoryService(WCRestEndpoint wcRest)
{
this.wcRest = wcRest;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Call woocommerce REST and map the category to our domain category
}
}
现在,在未来,如果我决定我们需要一个新的商店来提供类别,我可以为该特定服务定义一个新的实现,替换注入的类型,而不是因为这个变化而弄乱依赖。
试图理解函数依赖方法是如何解决的定义
type Category = { Name: string }
type GetCategories =
WCRestEndpoint
-> Category list
突然之间,如果我要更改类别的来源,我将不得不更改功能签名或提供要使用的新定义,这会影响整个应用程序,因此不是很健壮。
我很好奇我是否误解了一些基本的东西。
以我的 OOP 脑袋,我能想到的就是这样
type Category = { Name: string }
// No longer directly dependent on WCRestEndpoint
type GetCategories = unit -> Category list
// But the definition would require scoped inclusion of the dependency
// Also how does the configuration get passed in without having the core library be dependent on the Environment or a config in the assembly?
let rest = WCRestEndpoint(/* Config... */)
type getCategories: GetCategories =
fun () ->
let wcCategories = rest.GetCategories()
// Convert the result into a Category type
我环顾四周,没有找到任何关于如何使用纯函数方法处理更改的解释,这让我相信我误解了一些基本的东西。
如何在不将函数类型签名绑定到特定实现类型的情况下公开函数 API?我是不是想错了?
我认为对此没有唯一的正确答案,但这里有几点需要考虑。
首先,我认为现实世界的功能代码通常具有“三明治结构”,其中包含一些输入处理,然后是纯功能转换和一些输出处理。 F# 中的 I/O 部分通常涉及与命令式库和 OO .NET 库的接口。因此,关键的教训是将 I/O 保持在外部,并将核心功能处理与其分开。换句话说,在外部使用一些命令式 OO 代码进行输入处理是非常有意义的。
其次,我认为解耦的想法在 OO 代码中更有价值,在这些代码中,您希望拥有复杂的接口和相互交织的逻辑。在功能代码中,(我认为)这不是一个问题。换句话说,我认为 I/O 不用担心这个是完全合理的,因为它只是“三明治结构”的外侧。如果需要改,不触及核心功能转换逻辑(可独立测试I/O)直接改即可。
第三,从实用的角度来说,在F#中使用接口是完全合理的。如果你真的想做解耦,你可以定义一个接口:
type Category { Name: string }
type CategoryService =
abstract GetAllAsync : unit -> Async<seq<Category>>
然后你可以使用对象表达式实现接口:
let myCategoryService =
{ new CategoryService with
member x.GetAllAsync() = async { ... } }
然后,我将有一个主函数将 seq<Category>
转换为您想要的任何结果,这 而不是 需要将 CategoryService
作为参数.但是,在您的主要代码中,您会将其作为参数(或在程序启动时在某处对其进行初始化),使用该服务来获取数据并调用您的主要转换逻辑。
在我意识到我以错误的方式看待它之前,我为这个问题苦苦挣扎了很多年。来自面向对象的开发和依赖注入,我一直在寻找依赖注入的功能替代方案。我终于明白了 Dependency Injection makes everything impure, which means that you can't use that approach (not even partial application) if you want to apply a functional architecture.
转移注意力的是依赖关系。相反,专注于编写纯函数。您仍然可以使用 Dependency Inversion Principle, but instead of focusing on actions and interactions, focus on data. If a function requires some data, pass it as an argument. If a function has to make a decision, return it as a data structure.
您没有提供任何示例说明您希望在何处使用 Category
值列表,但是依赖此类数据的函数将具有如下类型:
Category list -> 'a
这样的功能与类别的来源完全解耦。它仅取决于 Category
类型本身,它是域模型的一部分。
最终,您需要从某个地方获取类别,但这项工作会推到系统的边界,例如Main
:
let Main () =
let categories = getCategories ()
let result = myFunction categories
result
因此,如果您改变主意如何获取类别,您只需更改一行代码。这种架构是akin to a sandwich, with impure actions surrounding the pure heart of the application. It's also known as functional core, imperative shell.
如果您只想不使用对象,那么这是相当机械的重写。
单一方法接口只是一个命名函数签名,所以:
public interface ICategoryService
{
Task<IEnumerable<Category>> GetAllAsync();
}
async Task UseCategoriesToDoSomething(ICategoryService service) {
var categories = await service.GetAllAsync();
...
}
变为:
let useCategoriesToDoSomething(getAllAsync: unit -> Async<seq<Category>>) = async {
let! categories = getAllAsync()
...
}
你的组合根变成了部分应用函数与这些函数参数的具体实现的问题。
就是说,这样使用对象并没有错; F# 大多拒绝可变性和继承,但拥抱接口、点符号等。
a talk Don Syme gave 中有一张关于 F# 面向对象的很棒的幻灯片:
您需要一个异步计算的类别列表。已经有一个类型:Async<seq<Category>>
。也许这是由使用 WCRestEndpoint
的函数创建的。也许这是在单元测试中用一些恒定的虚拟值创建的。也许这是为单元测试创建的,并且总是会引发错误。消费代码并不关心。它只关心有没有办法获取类别。
此类型比特定于应用程序的 ICategoryService
类型更可重用。例如,也许您有一个接受 Async<'a>
并以标准方式处理错误的函数。也许您有一个接受 Async<seq<'a>>
并验证列表不为空的函数。
老实说,你不需要为获取一种东西的东西起一个特殊的名字。
祝贺您做出了尝试 F# 的绝佳选择!
以另一种方式回答您的问题:
@Asik 已经提到使用函数而不是单一方法接口。这个想法很可能会扩展到 records 将一堆相关的函数组合在一起;例如:
type MyEntityRepository =
{ Fetch: Guid -> Async<MyEntity>
Add: MyEntity -> Async<unit>
Delete: Guid -> Async<unit> }
也总是可以使用接口,但我更喜欢这种方法,因为它更容易模拟(只需将 Unchecked.defaultof<_>
分配给测试代码不会使用的任何字段)并且因为语法看起来更更好,等等。
如果您需要嵌套依赖项(您肯定会需要),您可以简单地使用闭包:
let createRepository (connection: IDbConnection) =
{ Add = fun entity -> connection.Execute(...)
Fetch = fun id -> connection.Query(...) }
您实质上是将依赖项提供给工厂函数,然后依赖项将在 lambda 的闭包中捕获,从而允许您根据需要尽可能深地嵌套依赖项。这种使用工厂函数的模式也适用于 ASP.Net 的内置 DI 容器。
首先,正如@Karl Bielefeldt 所指出的,return 的正确类型是 Async<seq<Category>>
。所以你的函数最初应该是 WCRestEndpoint -> Async<seq<Category>>
.
类型
但这不是这里的真正问题。真正的问题是这个声明:
Suddenly if I am to change the source of categories I would have to either change the functional signature or provide a new definition to be used which would ripple through the application and thereby not be very robust.
这种说法对我来说毫无意义,因为在 F# 案例中重构实际上 更简单。
无论您如何编码,您总是需要编写采用 WCRestEndpoint
并输出一系列 Category
的代码。如果您决定实际上要以其他方式获取 Category
的序列,那么无论如何您都需要编写新代码来执行此操作。
例如,假设我决定需要修改我的代码以从 OtherCatGetter
而不是 WCRestEndpoint
获取类别。在您的 C# 代码中,我需要替换
public class WcCategoryService : ICategoryService
{
private readonly WCRestEndpoint wcRest;
// WooCommerce specific dependencies
public WcCategoryService(WCRestEndpoint wcRest)
{
this.wcRest = wcRest;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Call woocommerce REST and map the category to our domain category
}
}
和
public class OtherCategoryService : ICategoryService
{
private readonly OtherCatGetter getter;
// WooCommerce specific dependencies
public WcCategoryService(OtherCatGetter getter)
{
this.getter = getter;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Do something with getter to get the categories
}
}
我们还必须将对 new WcCategoryService(wcRest)
的每个调用替换为对 new OtherCategoryService(getter)
.
的调用
在F#
这边,我们必须更换
let getCategoriesFromWC (wcRest: WCRestEndpoint) = ... // get categories from wcRest
和
let getCategoriesFromOther (getter: OtherCatGetter) = ... // get categories from getter
并用 getCategoriesFromOther getter
.
替换每一次出现的 GetCategoriesFromWC wcRest
显然,当您需要更改获取 Category
的方式时,F# 版本需要较少的重构,因为 F# 不必处理定义新 public class 具有单个只读字段和一个参数构造函数。如果您需要定义一种不同的方式来获取 Category
的序列,您只需这样做而不是跳过不必要的箍。
我是您的经典 OOP 开发人员。然而,自从我发现纯函数式编程语言以来,我一直对 为什么 很感兴趣,因为 OOP 似乎以合理的方式解决了大多数业务案例。
现在,在我的软件开发经验中,我已经到了寻求更简洁和更具表现力的语言的地步。我通常用 C# 编写我的软件,但对于我的最新项目,我决定采取飞跃并使用 F# 构建业务服务。在这样做的过程中,我发现很难理解如何使用纯函数式方法完成解耦。
案例是这样的。我有一个数据源,它是 WooCommerce,但我不想将我的函数定义绑定到 那个 特定数据源。
在 C# 中,很明显我想要一个看起来像这样的服务
public record Category(string Name);
public interface ICategoryService
{
Task<IEnumerable<Category>> GetAllAsync();
}
// With a definition for the service that specifies WooCommerce
public class WcCategoryService : ICategoryService
{
private readonly WCRestEndpoint wcRest;
// WooCommerce specific dependencies
public WcCategoryService(WCRestEndpoint wcRest)
{
this.wcRest = wcRest;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Call woocommerce REST and map the category to our domain category
}
}
现在,在未来,如果我决定我们需要一个新的商店来提供类别,我可以为该特定服务定义一个新的实现,替换注入的类型,而不是因为这个变化而弄乱依赖。
试图理解函数依赖方法是如何解决的定义
type Category = { Name: string }
type GetCategories =
WCRestEndpoint
-> Category list
突然之间,如果我要更改类别的来源,我将不得不更改功能签名或提供要使用的新定义,这会影响整个应用程序,因此不是很健壮。
我很好奇我是否误解了一些基本的东西。
以我的 OOP 脑袋,我能想到的就是这样
type Category = { Name: string }
// No longer directly dependent on WCRestEndpoint
type GetCategories = unit -> Category list
// But the definition would require scoped inclusion of the dependency
// Also how does the configuration get passed in without having the core library be dependent on the Environment or a config in the assembly?
let rest = WCRestEndpoint(/* Config... */)
type getCategories: GetCategories =
fun () ->
let wcCategories = rest.GetCategories()
// Convert the result into a Category type
我环顾四周,没有找到任何关于如何使用纯函数方法处理更改的解释,这让我相信我误解了一些基本的东西。
如何在不将函数类型签名绑定到特定实现类型的情况下公开函数 API?我是不是想错了?
我认为对此没有唯一的正确答案,但这里有几点需要考虑。
首先,我认为现实世界的功能代码通常具有“三明治结构”,其中包含一些输入处理,然后是纯功能转换和一些输出处理。 F# 中的 I/O 部分通常涉及与命令式库和 OO .NET 库的接口。因此,关键的教训是将 I/O 保持在外部,并将核心功能处理与其分开。换句话说,在外部使用一些命令式 OO 代码进行输入处理是非常有意义的。
其次,我认为解耦的想法在 OO 代码中更有价值,在这些代码中,您希望拥有复杂的接口和相互交织的逻辑。在功能代码中,(我认为)这不是一个问题。换句话说,我认为 I/O 不用担心这个是完全合理的,因为它只是“三明治结构”的外侧。如果需要改,不触及核心功能转换逻辑(可独立测试I/O)直接改即可。
第三,从实用的角度来说,在F#中使用接口是完全合理的。如果你真的想做解耦,你可以定义一个接口:
type Category { Name: string } type CategoryService = abstract GetAllAsync : unit -> Async<seq<Category>>
然后你可以使用对象表达式实现接口:
let myCategoryService = { new CategoryService with member x.GetAllAsync() = async { ... } }
然后,我将有一个主函数将
seq<Category>
转换为您想要的任何结果,这 而不是 需要将CategoryService
作为参数.但是,在您的主要代码中,您会将其作为参数(或在程序启动时在某处对其进行初始化),使用该服务来获取数据并调用您的主要转换逻辑。
在我意识到我以错误的方式看待它之前,我为这个问题苦苦挣扎了很多年。来自面向对象的开发和依赖注入,我一直在寻找依赖注入的功能替代方案。我终于明白了 Dependency Injection makes everything impure, which means that you can't use that approach (not even partial application) if you want to apply a functional architecture.
转移注意力的是依赖关系。相反,专注于编写纯函数。您仍然可以使用 Dependency Inversion Principle, but instead of focusing on actions and interactions, focus on data. If a function requires some data, pass it as an argument. If a function has to make a decision, return it as a data structure.
您没有提供任何示例说明您希望在何处使用 Category
值列表,但是依赖此类数据的函数将具有如下类型:
Category list -> 'a
这样的功能与类别的来源完全解耦。它仅取决于 Category
类型本身,它是域模型的一部分。
最终,您需要从某个地方获取类别,但这项工作会推到系统的边界,例如Main
:
let Main () =
let categories = getCategories ()
let result = myFunction categories
result
因此,如果您改变主意如何获取类别,您只需更改一行代码。这种架构是akin to a sandwich, with impure actions surrounding the pure heart of the application. It's also known as functional core, imperative shell.
如果您只想不使用对象,那么这是相当机械的重写。
单一方法接口只是一个命名函数签名,所以:
public interface ICategoryService
{
Task<IEnumerable<Category>> GetAllAsync();
}
async Task UseCategoriesToDoSomething(ICategoryService service) {
var categories = await service.GetAllAsync();
...
}
变为:
let useCategoriesToDoSomething(getAllAsync: unit -> Async<seq<Category>>) = async {
let! categories = getAllAsync()
...
}
你的组合根变成了部分应用函数与这些函数参数的具体实现的问题。
就是说,这样使用对象并没有错; F# 大多拒绝可变性和继承,但拥抱接口、点符号等。
a talk Don Syme gave 中有一张关于 F# 面向对象的很棒的幻灯片:
您需要一个异步计算的类别列表。已经有一个类型:Async<seq<Category>>
。也许这是由使用 WCRestEndpoint
的函数创建的。也许这是在单元测试中用一些恒定的虚拟值创建的。也许这是为单元测试创建的,并且总是会引发错误。消费代码并不关心。它只关心有没有办法获取类别。
此类型比特定于应用程序的 ICategoryService
类型更可重用。例如,也许您有一个接受 Async<'a>
并以标准方式处理错误的函数。也许您有一个接受 Async<seq<'a>>
并验证列表不为空的函数。
老实说,你不需要为获取一种东西的东西起一个特殊的名字。
祝贺您做出了尝试 F# 的绝佳选择!
以另一种方式回答您的问题:
@Asik 已经提到使用函数而不是单一方法接口。这个想法很可能会扩展到 records 将一堆相关的函数组合在一起;例如:
type MyEntityRepository =
{ Fetch: Guid -> Async<MyEntity>
Add: MyEntity -> Async<unit>
Delete: Guid -> Async<unit> }
也总是可以使用接口,但我更喜欢这种方法,因为它更容易模拟(只需将 Unchecked.defaultof<_>
分配给测试代码不会使用的任何字段)并且因为语法看起来更更好,等等。
如果您需要嵌套依赖项(您肯定会需要),您可以简单地使用闭包:
let createRepository (connection: IDbConnection) =
{ Add = fun entity -> connection.Execute(...)
Fetch = fun id -> connection.Query(...) }
您实质上是将依赖项提供给工厂函数,然后依赖项将在 lambda 的闭包中捕获,从而允许您根据需要尽可能深地嵌套依赖项。这种使用工厂函数的模式也适用于 ASP.Net 的内置 DI 容器。
首先,正如@Karl Bielefeldt 所指出的,return 的正确类型是 Async<seq<Category>>
。所以你的函数最初应该是 WCRestEndpoint -> Async<seq<Category>>
.
但这不是这里的真正问题。真正的问题是这个声明:
Suddenly if I am to change the source of categories I would have to either change the functional signature or provide a new definition to be used which would ripple through the application and thereby not be very robust.
这种说法对我来说毫无意义,因为在 F# 案例中重构实际上 更简单。
无论您如何编码,您总是需要编写采用 WCRestEndpoint
并输出一系列 Category
的代码。如果您决定实际上要以其他方式获取 Category
的序列,那么无论如何您都需要编写新代码来执行此操作。
例如,假设我决定需要修改我的代码以从 OtherCatGetter
而不是 WCRestEndpoint
获取类别。在您的 C# 代码中,我需要替换
public class WcCategoryService : ICategoryService
{
private readonly WCRestEndpoint wcRest;
// WooCommerce specific dependencies
public WcCategoryService(WCRestEndpoint wcRest)
{
this.wcRest = wcRest;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Call woocommerce REST and map the category to our domain category
}
}
和
public class OtherCategoryService : ICategoryService
{
private readonly OtherCatGetter getter;
// WooCommerce specific dependencies
public WcCategoryService(OtherCatGetter getter)
{
this.getter = getter;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Do something with getter to get the categories
}
}
我们还必须将对 new WcCategoryService(wcRest)
的每个调用替换为对 new OtherCategoryService(getter)
.
在F#
这边,我们必须更换
let getCategoriesFromWC (wcRest: WCRestEndpoint) = ... // get categories from wcRest
和
let getCategoriesFromOther (getter: OtherCatGetter) = ... // get categories from getter
并用 getCategoriesFromOther getter
.
GetCategoriesFromWC wcRest
显然,当您需要更改获取 Category
的方式时,F# 版本需要较少的重构,因为 F# 不必处理定义新 public class 具有单个只读字段和一个参数构造函数。如果您需要定义一种不同的方式来获取 Category
的序列,您只需这样做而不是跳过不必要的箍。