如何设计功能样式的可插拔系统?

Fyo*_*kin 7 f# functional-programming

免责声明:
虽然我接受了不可变状态和高阶函数的福音,但我的实际经验仍然是95%面向对象.我想改变这一点,但是更多的是什么.所以我的大脑非常接近OO.

问题:
我经常遇到这种情况:一个业务功能实现为一个小的"核心"加上多个"插件",共同为用户呈现一个看似坚实的表面.我发现这种"微内核"架构在很多情况下都能很好地工作.另外,非常方便,它与DI容器很好地结合,可以用于插件发现.

那么,我该如何以功能的方式做到这一点

我不认为这种技术的基本思想本质上是面向对象的,因为我刚刚在不使用任何OO术语或概念的情况下对其进行了描述.但是,我不能完全围绕功能性的方式来解决它.当然,我可以将插件表示为函数(或函数桶),但是当插件需要将自己的数据作为整体图片的一部分时,困难的部分就出现了,而插件到插件的数据形状也不同.

下面是一个小的F#片段,它或多或少是C#代码的字面翻译,我将从头开始实现这个模式时编写.
注意弱点:丢失类型信息CreateData,必要的向上翻译PersistData.
我每次都对演员阵容(无论是向上还是向下)都畏缩,但我已经学会了接受它们作为C#中必不可少的邪恶.然而,我过去的经验表明,功能方法通常会为这类问题提供意想不到的,美丽而优雅的解决方案.这是我追求的解决方案.

type IDataFragment = interface end
type PersistedData = string // Some format used to store data in persistent storage
type PluginID = string // Some form of identity for plugins that would survive app restart/rebuild/upgrade

type IPlugin = interface
  abstract member UniqueID: PluginID
  abstract member CreateData: unit -> IDataFragment

  // NOTE: Persistence is conflated with primary function for simplicity. 
  // Regularly, persistence would be handled by a separate component.
  abstract member PersistData: IDataFragment -> PersistedData option
  abstract member LoadData: PersistedData -> IDataFragment
end

type DataFragment = { Provider: IPlugin; Fragment: IDataFragment }
type WholeData = DataFragment list

// persist: WholeData -> PersistedData
let persist wholeData = 
  let persistFragmt { Provider = provider; Fragment = fmt } = 
    Option.map (sprintf "%s: %s" provider.UniqueID) (provider.PersistData fmt)

  let fragments = wholeData |> Seq.map persistFragmt |> Seq.filter Option.isSome |> Seq.map Option.get
  String.concat "\n" fragments // Not a real serialization format, simplified for example

// load: PersistedData -> WholeData
let load persistedData = // Discover plugins and parse the above format, omitted

// Reference implementation of a plugin
module OnePlugin =
  type private MyData( d: string ) = 
    interface IDataFragment
    member x.ActualData = d

  let create() = 
    {new IPlugin with
      member x.UniqueID = "one plugin"
      member x.CreateData() = MyData( "whatever" ) :> _
      member x.LoadData d = MyData( d ) :> _

      member x.PersistData d = 
        match d with
        | :? MyData as typedD -> Some typedD.ActualData
        | _ -> None
    }
Run Code Online (Sandbox Code Playgroud)




一些更新和澄清

  • 我不需要接受"一般"的函数式编程教育(或者至少我喜欢这样思考:-).我确实知道接口是如何与函数相关的,我知道高阶函数是什么,以及函数组合是如何工作的.我甚至理解monads温暖蓬松的东西(以及来自类别理论的其他一些mumbo-jumbo).
  • 我意识到,我并不需要在F#中使用的接口,因为功能一般都较好.但是,在我的例子两个接口实际上是有道理的:IPlugin用来绑定在一起UniqueIDCreateData; 如果不是界面,我会使用类似形状的记录.并且IDataFragment用于限制数据片段的类型,否则我将不得不使用obj它们,这将给我更少的类型安全性.(我甚至无法想象如何在Haskell中使用Dynamic,而不是使用Dynamic)

scr*_*wtp 5

我只能同情你的陈述.虽然小编程中的函数式编程已经被人们讨论过,但对于如何在大型函数式编程中进行函数编程却没有什么建议.我认为对于F#而言,随着系统的发展,大多数解决方案将倾向于更加面向对象(或者至少是面向接口的).我认为这不一定是坏事 - 但如果有一个令人信服的FP解决方案,我也希望看到它.

我在类似场景中使用过的一种模式是拥有一对接口,一个是打字的,一个是非打字的,还有一个基于反射的机制.所以在你的场景中你会有这样的事情:

type IPlugin =
    abstract member UniqueID: PluginID
    abstract member DataType: System.Type
    abstract member CreateData: unit -> IDataFragment

type IPlugin<'data> = 
    inherit IPlugin

    abstract member CreateData: unit -> 'data
    abstract member PersistData: 'data -> PersistedData option
    abstract member LoadData: PersistedData -> 'data
Run Code Online (Sandbox Code Playgroud)

一个实现看起来像这样:

let create() = 
    let createData () = MyData( "whatever" )
    {
        new IPlugin with
            member x.UniqueID = "one plugin"
            member x.DataType = typeof<MyData>                
            member x.CreateData() = upcast createData()
        interface IPlugin<MyData> with
            member x.LoadData d = MyData( d )
            member x.PersistData (d:MyData) = Some d.ActualData
            member x.CreateData() = createData()            
    }
Run Code Online (Sandbox Code Playgroud)

请注意,这CreateData是两个接口的一部分 - 它只是用来说明在打字和无类型接口之间复制多少以及需要跳过箍以在它们之间进行转换的频率之间存在平衡.理想情况下CreateData不应该在那里IPlugin,但如果它节省你的时间,我不会回头两次.

从开始IPluginIPlugin<'a>你需要一个基于反射的辅助函数,但至少你明确地知道了类型参数,因为它是IPlugin接口的一部分.虽然它并不漂亮,但至少类型转换代码包含在代码的单个部分中,而不是分散在所有插件中.


Mar*_*ann 3

不必定义接口即可使架构可插入 F# 中。函数已经是可组合的。

您可以从外到内编写系统,从系统所需的整体行为开始。例如,这是我最近编写的一个函数,它将轮询消费者从未收到消息的状态转换为新状态:

let idle shouldSleep sleep (nm : NoMessageData) : PollingConsumer =
    if shouldSleep nm
    then sleep () |> Untimed.withResult nm.Result |> ReadyState
    else StoppedState ()
Run Code Online (Sandbox Code Playgroud)

这是一个高阶函数。当我编写它时,我发现它依赖于辅助函数shouldSleepsleep,所以我将它们添加到参数列表中。然后编译器会自动推断egshouldSleep必须具有类型NoMessageData -> bool。该函数是一个Dependency。函数也是如此sleep

第二步,shouldSleep函数的合理实现最终看起来像这样:

let shouldSleep idleTime stopBefore (nm : NoMessageData) =
    nm.Stopped + idleTime < stopBefore
Run Code Online (Sandbox Code Playgroud)

如果您不知道它的作用,也没关系。这里重要的是函数的组合。在本例中,我们了解到这个特定shouldSleep函数的类型为TimeSpan -> DateTimeOffset -> NoMessageData -> bool,它与完全相同NoMessageData -> bool

不过,它非常接近,您可以使用部分函数应用程序来完成其余的距离:

let now' = DateTimeOffset.Now
let stopBefore' = now' + TimeSpan.FromSeconds 20.
let idleTime' = TimeSpan.FromSeconds 5.
let shouldSleep' = shouldSleep idleTime' stopBefore'
Run Code Online (Sandbox Code Playgroud)

shouldSleep'函数是该函数的部分应用shouldSleep,并且具有所需的类型NoMessageData -> bool。您可以将此函数idle与其依赖项的实现一起组合到该函数中sleep

由于低阶函数具有正确的类型(正确的函数签名),因此它就位了;无需铸造即可实现此目的。

idle和函数可以在不同的模块、不同的库中定义,并且您可以使用类似于Pure DI 的shouldSleep过程将它们全部组合在一起。shouldSleep'

如果您想查看从各个函数组成整个应用程序的更全面的示例,我在我的F# Pluralsight 功能架构课程中提供了一个示例。

  • 这并没有让你的问题对我来说更清楚。为什么需要标记接口(“IDataFragment”)?这甚至不是好的面向对象...你所说的“微内核”或“多面组合”是什么意思?您是否正在寻找类似托管可扩展性框架之类的东西,但寻找的是函数而不是接口?如果是这样,我认为这样的库还不存在。 (3认同)