它是最好(我知道没有银弹,但通过使用一个可能有一些优势) - 登录调用函数,或函数调用它?
示例:
方法1
module MongoDb =
let tryGetServer connectionString =
try
let server = new MongoClient(connectionString).GetServer()
server.Ping()
Some server
with _ -> None
用法:
match MongoDb.tryGetServer Config.connectionString with
| None ->
logger.Information "Unable to connect to the database server."
// ... code ...
| Some srv ->
logger.Information "Successfully connected to the database server."
// ... code ...
方法2
module MongoDb =
let tryGetServer connectionString =
try
let server = new MongoClient(connectionString).GetServer()
server.Ping()
Some server
with _ -> None
let tryGetServerLogable connectionString logger =
match tryGetServer connectionString with
| None ->
logger.Information "Unable to connect to the database server."
None
| Some srv ->
logger.Information "Successfully connected to the database server."
Some srv
用法:
match MongoDb.tryGetServerLogable Config.connectionString logger with
| None ->
// ... code ...
| Some srv ->
// ... code ...
答案 0 :(得分:5)
方法2 更好。一般来说,日志记录是跨领域关注,因此它最好与实现细节分离。交叉问题最好通过作文来解决;在OOD,this can be done with Decorators or Interceptors。在FP中,我们有时可以从OOD中学习,因为许多the principles translate from objects to closures。
然而,我不是逐字逐句地使用方法2 ,而是更喜欢这样的事情:
module MongoDb =
let tryGetServer connectionString =
try
let server = MongoClient(connectionString).GetServer()
server.Ping()
Some server
with _ -> None
请注意,MongoDb
模块不了解日志记录。这遵循Single Responsibility Principle,这在函数式编程中也很有价值。
tryGetServer
函数具有以下签名:
string -> MongoServer option
现在您可以定义一个与MongoDb
模块完全分离的日志记录功能:
module XyzLog =
type Logger() =
member this.Information message = ()
let tryGetServer f (logger : Logger) connectionString =
match f connectionString with
| None ->
logger.Information "Unable to connect to the database server."
None
| Some srv ->
logger.Information "Successfully connected to the database server."
Some srv
在这里,您可以想象XyzLog
是特定日志记录模块的占位符,使用Serilog,Log4Net,NLog,您自己的自定义日志记录框架或类似...
f
参数是一个带有通用签名'a -> 'b option
的函数,其中MongoDb.tryGetServer
是一个特殊化。
这意味着您现在可以像这样定义部分应用的功能:
let tgs = XyzLog.tryGetServer MongoDb.tryGetServer (XyzLog.Logger())
函数tgs
也具有签名
string -> MongoServer option
因此,依赖具有此签名的函数的任何客户端都可以互换使用MongoDb.tryGetServer
或tgs
,而不知道其中的差异。
这使您可以相互独立地改变您的想法或重构MongoDb.tryGetServer
和您的日志记录基础结构。
答案 1 :(得分:4)
有一种更通用的方法可以实现横切关注点,例如使用函数式语言进行日志记录。我的例子来自异步服务库(想想ASP.NET MVC和ActionFilters),但同样适用于此。如Mark所述,函数tryGetServer
的类型为string -> MongoServer option
。假设我们将其抽象为:
type Service<'a, 'b> = 'a -> 'b option
然后假设我们也有如下类型:
type Filter<'a, 'b> = 'a -> Service<'a, 'b> -> 'b option
过滤器是一个函数,它取值'a
和Service<'a, 'b>
,然后返回与Service<'a, 'b>
函数相同类型的值。最简单的过滤器是一个函数,它只是将它直接接收的'a
传递给服务,并返回它从服务中获取的值。更有趣的过滤器是在接收来自服务的输出之后打印日志消息的功能。
let loggingFilter (connStr:string) (tryGetServer:string -> MongoServer option) : Filter<string, MongoServer option> =
let server = tryGetServer connStr
match tryGetServer connStr with
| Some _ ->
logger.Information "Successfully connected to the database server."
server
| None ->
logger.Information "Unable to connect to the database server."
server
然后,如果您定义了以下内容:
type Continuation<'a,'r> = ('a -> 'r) -> 'r
module Continuation =
let bind (m:Continuation<'a, 'r>) k c = m (fun a -> k a c)
module Filter =
/// Composes two filters into one which calls the first one, then the second one.
let andThen (f2:Filter<_,,_>) (f1:Filter<_,_>) : Filter<_,_> = fun input -> Continuation.bind (f1 input) f2
/// Applies a filter to a service returning a filtered service.
let apply (service:Service<_,_>) (filter:Filter<_,_>) : Service<_,_> = fun input -> filter input service
/// The identity filter which passes the input directly to the service and propagates the output.
let identity : Filter<_,_> = fun (input:'Input) (service:Service<_,_>) -> service input
您可以将过滤器应用于服务并返回原始服务类型,但现在可以进行日志记录:
let tryGetServerLogable = Filter.apply tryGetServer loggingFilter
为什么要这么麻烦?那么,现在你可以组合过滤器了。例如,您可以添加一个过滤器来衡量创建连接所需的时间,然后您可以使用Filter.andThen
将它们组合在一起。我最初制作的要点是here。
另一种需要考虑的方法是使用writer monad。使用编写器monad,您可以将日志消息的实际打印推迟到某个明确定义的点,但仍具有相似的组合特征。