代码之家  ›  专栏  ›  技术社区  ›  Soldalma

我可以实例化包含顶级副作用值的类吗?

  •  3
  • Soldalma  · 技术社区  · 6 年前

    这个问题与本文中的问题相关且重叠 Should one wrap type providers containing values that have side effects inside a class? ,亲切地回答 Aaron M. Eshbach .

    我试图在我的代码中实现 F# coding conventions

    https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions .

    部门 Use classes to contain values that have side effects 特别有趣。上面写着

    There are many times when initializing a value can have side effects, such as instantiating a context to a database or other remote resource. It is tempting to initialize such things in a module and use it in subsequent functions.
    

    并提供了一个例子。然后指出了这种做法的三个问题(由于篇幅不够,我省略了这些问题,但可以在链接文章中看到),并建议使用一个简单的类来保存依赖项。

    根据这个建议,我实现了一个简单的类,其中包含一个有副作用的值:

    type Roots() =
        let msg = "Roots: Computer must be one of THREADRIPPER, LAPTOP or HPW8"
    
        member this.dropboxRoot =
            let computerName = Environment.MachineName 
            match computerName with
            | "THREADRIPPER" -> @"C:\"
            | "HP-LAPTOP" -> @"C:\"
            | "HPW8" -> @"H:\"
            | _ -> failwith msg
    

    然后我可以在函数中使用它

    let foo (name: string) =
        let roots = Roots()
        let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Temp\" + name + ".csv")
        printfn "%s" path
    
    foo "SomeName"
    

    到现在为止,一直都还不错。在上面的例子中,这个类非常“轻”,我可以在任何函数中实例化它。

    然而,包含有副作用的值的类也可能是计算密集型的。在这种情况下,我只想实例化它一次,并从不同的函数调用它:

    let roots = Roots()
    
    let csvPrinter (name: string) =
        let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Folder1\" + name + ".csv")
        printfn "%s" path
    
    let xlsxPrinter (name: string) =
        let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Folder2\" + name + ".xlsx")
        printfn "%s" path
    
    csvPrinter "SomeName"
    xlsxPrinter "AnotherName"
    

    所以我的问题是:如果我实例化这个类 Roots 在模块的顶层,我是否违背了创建类的目的,而创建类是为了避免 F#编码约定 页如果是这样,我该如何处理计算密集型定义?

    1 回复  |  直到 6 年前
        1
  •  5
  •   scrwtp    6 年前

    简单的回答是——是的,这首先违背了拥有这种包装的目的。

    然而,该指南有点忽略了树的本质——真正的问题是,在提倡函数纯粹性和引用透明性的环境中管理状态依赖关系和外部数据,这是一个更基本的问题,尤其是当你看到一个庞大的代码库需要随着时间的推移而增长和改变时(如果我们看到的是一次性的一次性脚本,只需做能完成任务的事情)。更重要的是 roots 字段被填充和使用(作为硬编码的静态依赖项),然后确定其中的值是否包装在类中。

    这里我推荐的方法是将业务逻辑编写为纯函数的模块(或多个模块),并将依赖项显式地作为参数传递。这样,您就可以将依赖关系的决策推迟到调用方。这可能会一直到程序的入口点(控制台应用程序的主要功能 Startup 类等)。用可怕的面向对象编程(OOP)术语来说,你所看到的相当于一个组合根——你在程序中组装依赖项的地方。

    正如您链接到的约定所暗示的那样,这可能需要在一个纯功能模块周围有一个类包装器,但这并不是必然的结果。你很可能有一个(副作用)函数为你产生这个值,你可以只传递这个值。

    let getDropboxRoot () : string option = 
        let computerName = Environment.MachineName 
        match computerName with
        | "THREADRIPPER" -> Some @"C:\"
        | "HP-LAPTOP" -> Some @"C:\"
        | "HPW8" -> Some @"H:\"
        | _ -> None        
    
    let csvPrinter (dropboxRoot: string) (name: string) =
        let path = Path.Combine(dropboxRoot,  @"Dropbox\Folder1\" + name + ".csv")
        printfn "%s" path
    

    通过这种方式,您可以完全控制有效的操作——您可以随时调用该函数,如果环境发生变化,您可以再次调用该函数以获取新值。代码的其余部分既不知道也不关心您输入的值是否来自有效的操作——这使得关于它的功能的推理和测试变得简单。

    在其周围有一个类包装器本身不会给这些属性添加任何内容。它可能会为更多的样板提供更好的API,但这里讨论的真正问题是其他地方。