继承是解决这个问题的正确方法吗?

时间:2014-03-27 02:52:09

标签: inheritance f# functional-programming f#-3.0

我是f#的新手。我想将搜索结果列表返回到前端。但是,可能有几种类型的搜索结果(例如BlogResultMovieResult等),每种搜索结果都有自己的属性(例如MovieResult.PosterIcon)。 UI将了解这些类型,并相应地显示每种类型。核心搜索API不会知道这些类型,只提供“提供者”需要实现的框架,包括它将返回的类型。例如,BlogProvider将实现必要的功能,并且它是相应的搜索结果类型。应用程序将所有这些提供程序一起注册到核心框架中,以便所有结果应按排名顺序返回为单个项目列表。这些提供程序很可能各自都存在于自己的程序集中,与应用程序和核心搜索API分开。

我的OO大脑希望拥有一个SearchResult类或接口,所有其他结果类型都继承自该类。有没有更实用的方法来处理这个问题?

更新 该应用程序是一个ASP.NET MVC应用程序。每个结果都由局部视图表示。添加新的搜索结果类型时,不需要修改站点本身。只有新的局部视图。这不违反开放/封闭原则。从技术上讲,我可以让提供者为局部视图创建模板。然而,仪式代码不值得努力。

2 个答案:

答案 0 :(得分:2)

这可能是我有限的想象力和糟糕的设计技巧,但每当我被提出上述要求时,我都被迫做出某种妥协。这就是为什么在OOP中也很困难的原因:

  

UI将了解这些类型,并相应地显示每种类型。

这是有问题的部分。据我所知,只有两种方法可以满足这一要求:

  • 对待所有'子类型'作为特例。如果您使用OOD执行此操作,则表示客户端(UI)必须了解所有可用的子类型。它可以以临时方式尝试向下转换,也可以利用Visitor pattern,但在任何一种情况下都违反Open/Closed Principle,因为您无法添加新的&# 39;提供商'到系统而不修改UI代码。
  • 让所有'子类型'实现公共接口或从公共基类继承。然后,此接口(或基类)将具有UI可以调用以呈现结果的一个(或多个)方法。这样的方法可以称为 Render ,它将是特定于技术的:
    • 对于桌面应用程序,它可能看起来像void Render(Canvas)(简化) - 也就是说,它接收某种 canvas 对象,然后它被要求渲染
    • 对于Web应用程序,它可能是一个返回HTML片段的函数:string Render()(再次,简化)。

虽然您可以提出更详细的方案,但它们往往是上述两种方案的组合。

每个替代方案都可以在F#中建模,而不依赖于继承。

特殊情况

而不是处理每个子类型'作为一个特例,您可以定义所有各种情况的被歧视的联盟

open System

type BlogResult = {
    Title : string
    Summary : string }

type MovieResult = {
    Title : string
    PosterIcon : Uri }

type SearchResult =
    | BlogResult of BlogResult
    | MovieResult of MovieResult

这与特殊外壳的OOD方法具有相同的缺点:如果要引入新的子类型,则需要修改(重新编译)UI。

另一方面,Discriminated Unions内置于F#,易于使用,甚至可以为您提供编译时检查(您也可以通过访问者模式获得)。

通用界面

作为使用通用界面的替代方法,您可以使用功能。功能,或者更确切地说,closures are equivalent to objects

因此,不要让UI使用定义string Render()方法的接口,而是让UI使用具有此签名的函数:unit -> string

任何'提供商'你想插入系统只需要为每个搜索结果返回一个带有此签名的函数。这个功能很可能是一个封闭。

答案 1 :(得分:2)

免责声明:此答案不适用于F#。马克的答案是最好的F#优惠。特别是有区别的联合方法存在一些C#没有直接等价的东西。

但是你问的是“功能性”,而其他一些功能语言则有更好的选择。其他函数语言如Scala和Haskell提供了一个名为类型类的特性,它基本上能够使扩展方法实现一个接口,然后在事后对该接口进行操作。

那么这是如何相关的?好吧,使用F#,你必须要么A)预先确定所有在Discriminated Union中的案例,或者B)在你的数据库中放置渲染功能。两者都不理想。

使用类型类,您可以单独定义记录类型:

// Blog.fs
type Blog {...}
[<SomeProviderAttribute>]let getBlogs() = ... // returns Blog seq

// Movie.fs
type Movie = {...}
[<SomeProviderAttribute>]let getMovies() = ... // returns Movie seq

然后你可以在更高级别的接口上进行操作:(伪代码跟随)

// UiBase.fs
class Renderable where render :: 'T -> string

// BlogUi.fs
instance Renderable Blog where render blog = blog.header + blog.text

// MovieUi.fs
instance Renderable Movie where render movie = movie.title + movie.description

最后,从主UI区域收集渲染的字符串很容易。如果打开定义类型类的模块,可以互换使用它们:

// MainUi.fs
open Blog; open Movie; open UiBase; open BlogUi; open MovieUi
// Just like you'd have to call "using" in C# to get the extention methods,
// you have to open these to get the type classes

let dataProviders = ... // Some assembly search and reflection here; 
                    // say it returns [getBlogs; getMovies]
                    // (and note without the common typeclass,
                    //    those two would be incompatible)

let renderedStrings = 
  seq { for provider in dataProviders do
          for renderable in provider() do
            yield render renderable }

因此,它基本上允许您在更高级别上交替使用(否则完全不相关的)Blog和Movie类和提供程序,因为您可以在中级“插入”界面。非常非常酷,与你在这里遇到的常见问题完全相关。

...不幸的是F#本身没有此功能。