考虑以下简单示例。
type PaymentInstrument =
| Check of string
| CreditCard of string * DateTime
let printInstrumentName instrument =
match instrument with
| Check number-> printfn "check"
| CreditCard (number, expirationDate) -> printfn "card"
let printRequisites instrument =
match instrument with
| Check number -> printfn "check %s" number
| CreditCard (number, expirationDate) -> printfn "card %s %A" number expirationDate
正如您所看到的,在两个函数中重复相同的模式匹配逻辑。如果我使用OOP,我会创建接口IPaymentInstrument
,定义两个操作:
PrintInstrumentName
和PrintRequisites
然后实现类 - 每个支付工具一个。要根据某些外部条件实例化仪器,我会使用(例如)工厂模式(PaymentInstrumentFactory
)。
如果我需要添加新的支付工具,我只需要添加一个实现IPaymentInstrument
接口的新类并更新工厂实例化逻辑。使用这些类的其他代码保持不变。
但是如果我使用函数方法,我应该更新每个函数,其中存在此类型的模式匹配。
如果有很多使用PaymentInstrument
类型的函数会出现问题。
如何使用功能方法消除此问题?
答案 0 :(得分:17)
正如PatrykĆwiek在the comment above中指出的那样,你遇到Expression Problem,所以你必须选择其中一个。
如果添加更多数据类型的能力比轻松添加更多行为的能力更重要,那么基于接口的方法可能更合适。
在F#中,您仍然可以定义面向对象的接口:
type IPaymentInstrument =
abstract member PrintInstrumentName : unit -> unit
abstract member PrintRequisites : unit -> unit
您还可以创建实现此接口的类。这是Check
,我会将CreditCard
作为练习留给读者:
type Check(number : string) =
interface IPaymentInstrument with
member this.PrintInstrumentName () = printfn "check"
member this.PrintRequisites () = printfn "check %s" number
然而,如果你想采用面向对象的方式,你应该开始考虑SOLID principles,其中一个是Interface Segregation Principle(ISP)。一旦您开始积极地应用ISP,you'll ultimately end up with interfaces with a single member, like this:
type IPaymentInstrumentNamePrinter =
abstract member PrintInstrumentName : unit -> unit
type IPaymentInstrumentRequisitePrinter =
abstract member PrintRequisites : unit -> unit
您仍然可以在课程中实现此功能:
type Check2(number : string) =
interface IPaymentInstrumentNamePrinter with
member this.PrintInstrumentName () = printfn "check"
interface IPaymentInstrumentRequisitePrinter with
member this.PrintRequisites () = printfn "check %s" number
现在开始显得有些荒谬了。如果你正在使用F#,那么为什么要解决与单个成员定义接口的所有麻烦?
为什么不使用功能?
两个所需的接口成员都具有类型unit -> unit
(虽然不是特别'功能'看起来类型),所以为什么不传递这些函数,并省去接口开销?
使用OP中的printInstrumentName
和printRequisites
函数,您已经拥有了所需的行为。如果你想把它们变成“实现”所需界面的多态“对象”,你可以关闭它们:
let myCheck = Check "1234"
let myNamePrinter () = printInstrumentName myCheck
在功能编程中,我们不会将这些内容称为 objects ,而是将闭包。它们不是带有行为的数据,而是带有数据的行为。
答案 1 :(得分:1)
使用Mark Seemann的回答我做出了这样的设计决定。
type PaymentInstrument =
| Check of string
| CreditCard of string * DateTime
type Operations =
{
PrintInstrumentName : unit -> unit
PrintRequisites : unit -> unit
}
let getTypeOperations instrument =
match instrument with
| Check number->
let printCheckNumber () = printfn "check"
let printCheckRequisites () = printfn "check %s" number
{ PrintInstrumentName = printCheckNumber; PrintRequisites = printCheckRequisites }
| CreditCard (number, expirationDate) ->
let printCardNumber () = printfn "card"
let printCardRequisites () = printfn "card %s %A" number expirationDate
{ PrintInstrumentName = printCardNumber; PrintRequisites = printCardRequisites }
使用
let card = CreditCard("124", DateTime.Now)
let operations = getTypeOperations card
operations.PrintInstrumentName()
operations.PrintRequisites()
如您所见,getTypeOperations
函数执行工厂模式的角色。为了聚合函数,我使用简单的记录类型(但是,根据F#设计指南http://fsharp.org/specs/component-design-guidelines/接口比这种决定更受欢迎,但我现在有兴趣在功能方法中做到这一点,以便更好地理解它。) / p>
我得到了我想要的东西 - 模式匹配现在只在一个地方。