函数式编程和解耦

Bja*_*ing 37 c# f# functional-programming 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
    }
}
Run Code Online (Sandbox Code Playgroud)

现在在未来,如果我决定我们需要一个新的存储来提供类别,我可以为该特定服务定义一个新的实现,替换注入的类型,并且不会因为这个变化而弄乱依赖项。

试图了解函数依赖方法是如何解决的,我遇到了这种情况(阅读“领域建模使函数化”),其中类型签名直接定义依赖关系,因此上述 C# 等效项将变成高度耦合的定义

type Category = { Name: string }
type GetCategories =
    WCRestEndpoint
    -> Category list
Run Code Online (Sandbox Code Playgroud)

突然间,如果我要更改类别的来源,我将不得不更改功能签名或提供要使用的新定义,这会在应用程序中产生涟漪效果,因此不是很健壮。

我很好奇的是我是否误解了一些基本的东西。

用我的 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
Run Code Online (Sandbox Code Playgroud)

我环顾四周,没有找到任何关于如何用纯函数式方法处理变化的解释,这让我相信我误解了一些基本的东西。

如何在不将函数类型签名绑定到实现特定类型中的情况下公开功能 API?我在想这个错误吗?

Mar*_*ann 56

我在这个问题上挣扎了很多年,然后才意识到我的看法是错误的。来自面向对象开发和依赖注入,我一直在寻找依赖注入的功能替代方案。我终于意识到依赖注入让一切变得不纯,这意味着如果你想应用一个函数式架构,你不能使用这种方法(甚至不能使用部分应用)。

红鲱鱼是专注于依赖关系。相反,专注于编写纯函数。您仍然可以使用依赖倒置原则,但不要关注操作和交互,而是关注数据。如果函数需要一些数据,请将其作为参数传递。如果函数必须做出决定,则将其作为数据结构返回

您没有提供任何想要使用Category值列表的示例,但依赖于此类数据的函数将具有如下类型:

Category list -> 'a
Run Code Online (Sandbox Code Playgroud)

这样的功能与类别的来源完全解耦。它只取决于Category类型本身,它是领域模型的一部分。

最终,您需要从某个地方获取类别,但是您将这项工作推到系统的边界,例如Main

let Main () =
    let categories = getCategories ()
    let result = myFunction categories
    result
Run Code Online (Sandbox Code Playgroud)

因此,如果您改变主意如何获取类别,则只需更改一行代码。这种架构类似于三明治,在应用程序的纯净核心周围有不纯净的动作。它也被称为功能核心,命令式 shell

  • @gntskn 我相信这是一个流行语,对于大多数非退化依赖注入(DI)来说都是正确的。在某些极端情况下它不成立,但除了偶尔注入的[策略](https://en.wikipedia.org/wiki/Strategy_pattern)之外,大多数反例都是退化的。DI 的大多数实际用途都涉及不纯的操作,一旦你有一个不纯的操作,所有调用者也会传递不纯的。我希望我链接的资源能够充分阐述这些要点,包括对替代方案的认识。我不认为这句话具有误导性。 (2认同)

Tom*_*cek 22

我不认为对此有唯一的正确答案,但这里有几点需要考虑。

  • 首先,我认为现实世界的函数代码通常具有“三明治结构”,带有一些输入处理,然后是纯函数转换和一些输出处理。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>>
    
    Run Code Online (Sandbox Code Playgroud)

    然后你可以使用对象表达式来实现接口:

    let myCategoryService = 
      { new CategoryService with 
        member x.GetAllAsync() = async { ... } }
    
    Run Code Online (Sandbox Code Playgroud)

    然后,我将有一个转换的一个主要功能seq<Category>到什么导致你想,这会不会需要采取CategoryService作为参数。但是,在您的主要代码中,您会将其作为参数(或在程序启动时将其初始化),使用该服务获取数据并调用您的主要转换逻辑。


Asi*_*sik 5

如果您只想不使用对象,这是一个相当机械的重写。

单方法接口只是一个命名的函数签名,所以:

public interface ICategoryService
{
    Task<IEnumerable<Category>> GetAllAsync();
}

async Task UseCategoriesToDoSomething(ICategoryService service) {
    var categories = await service.GetAllAsync();
    ...
}
Run Code Online (Sandbox Code Playgroud)

变成:

let useCategoriesToDoSomething(getAllAsync: unit -> Async<seq<Category>>) = async {
    let! categories = getAllAsync()
    ...
}
Run Code Online (Sandbox Code Playgroud)

您的组合根成为部分应用具有这些函数参数的具体实现的函数的问题。

也就是说,这样使用对象并没有错。F# 主要拒绝可变性和继承,但包含接口、点符号等。

Don Syme 的演讲中有一张关于 F# 中 OO 的精彩幻灯片: 在此处输入图片说明