F#探险之旅(六):F#代码的组织(转)
前言
是的,我们已经学习了如何在F#中使用各种范式(函数式、命令式、面向对象)进行编程。但是目前还仅限于在单个模块内编写,要知道,不管是采用哪种语言或者范式编程,如果项目规模大了,都不适合把所有代码放在单个模块内。
在常规的.NET项目中(比如C#+ASP.NET),我们往往会选择使用Solution的概念作为整个(独立)问题域的解决方 案,Solution以下则是Project、File。这些概念在物理上往往表现为程序集(类库或可执行程序)、类文件等,如果项目和文件数量较多,就 该好好考虑如何在组织它们。下面从这三个层次上分别来讨论一下。
Solution层次
这里主要考虑的是Project之间的相互关系,此时基本上我们可以忽略语言的不同,也可以说在这个层次上,语言的影响不大。所以说我们把那些在用 C#开发时采用的代码组织原则搬过来用。比如Martin Fowler在《企业应用架构模式》中谈到的内容,比如Robert Martin在《敏捷软件开发》中提到的关于包的设计原则,还包括.NET社区中关于PetShop架构的讨论等等,都可以加以借鉴。关于这方面的内容已 有大量相关的讨论,在此不再赘述。
这里只谈一个具体的问题:如何添加对其它程序集的引用。在F# CTP 1.9.6.0之前,添加对程序集的引用需要#I和#r指令,#I用来指定要引用的程序集的目录,#r则用来指定要引用程序集的路径(包含文件名,可以是 相对路径或绝对路径)。这两个指令既可以放在代码文件中,也可以放在编译选项中。其中有个小窍门,注册表中.NETFramework节点下包含了 各.NET版本的一些信息,其中的AssemblyFoldersEx中有若干个目录信息,如果程序集所在目录出现在AssemblyFoldersEx 中,就可以直接使用#r和文件名来添加引用了。
在CTP版本中,可以像常规的C#/VB.NET项目中那样,为项目添加对其它程序集的引用(包括引用同一解决方案中的其它项目):
而#r只能用于fsx脚本文件或者放在编译选项中。
Project层次
现在假定你已经对上述设计原则有了足够的了解,并运用这些原则完成了设计,下一步就是如何使用F#来实现这些设计。现在我们进入到了Project 这个层次,需要考虑Project中各代码实体之间的关系,这些实体可以是物理上的源码文件,也可以是逻辑上的模块、类型、配置等。F#中最基本的组织结 构是命名空间和模块,命名空间的概念与C#中的一样。借助于Reflector可以看到模块在编译之后就是静态类,我们在为模块添加成员时要了解,这是在 向一个静态类添加成员。关于命名空间和模块的相关知识,强烈推荐Lvxuwen的如何组织程序(上、下)。
File层次
现在考虑源码文件内部的基本问题。在使用函数式编程范式时,除了模块,还可以采用F#的自定义类型,F#中的类型分为两类,一是元组(Tuple) 或记录(Record)类型,它们类似于C#中的类;二是Union类型,有时又称为Sum类型。通过Reflector可以看到,元组值是Tuple类 型的实例,而Tuple实现了 Microsoft.FSharp.Core.IStructuralHash和System.IComparable接口;记录和Union则直接实现 了这两个接口。要了解IStructualHash接口的更多内容,请参考Jome Fisher的文章。
而在使用面向对象编程范式时,我们可以像在C#中那样定义.NET类型,比如接口、类、结构、枚举、委托等等。当然这其中的编程细节比较多(建议看看我前面写过的几篇随笔),而且对于同一问题可以采取不同的方案。这需要我们去多多学习和实战,根据不同的需要作出选择。
这里来看另一个具体的问题:如何使用F#中的签名文件(Signature file)。在学习C语言时,接触过函数原型的概念,它给出了函数的名称、参数类型和返回类型,函数签名的含义与函数原型是一样的。如果我们把模块内的函 数签名抽取出来,放在单独的一个文件中,这就是签名文件的由来。它的作用在于,它可以控制模块内函数的访问修饰符。如果要使用签名文件,那么它必须与其控 制的模块文件成对出现,并且文件名相同。比如:
F# Code - myModule.fsi
#light
module FsLib.MyModule
/// 获取一个浮点数的平方值
val square: float -> float
/// 获取一个浮点数的立方值
val cube: float -> float
F# Code - myModule.fs
#light
module FsLib.MyModule
open System
let pow x y = Math.Pow(x, y)
let square x = pow x 2.0
let cube x = pow x 3.0
*.fsi即签名文件,这里定义了两个函数的签名:square和cube。*.fs即实现文件,它必须要提供对应的签名文件的所有函数的实现。其 它程序集的模块,只能访问*.fsi中具有签名的函数。通过Reflector可以看到,对于myModule.fs中的三个函数,square和 cube的修饰符为public,而pow则为internal。
由此看来签名文件的作用很像C#中的接口。但事实上,编译后并没有真正生成接口。需要注意的是,如果要为代码添加XML文档注释,需要加在签名文件(如果模块有的话)而不是模块中。下面来看看如何在代码中添加注释。
常规注释
在F#中,单行注释使用//,而多行注释则使用(* … *)。
XML文档注释
如果为代码添加了文档注释,可以在编译时生成XML文档,然后借助于一些工具(如SandCastle)就可以生成容易使用的帮助文档。在上面的代 码中可以看到,直接使用///可以为模块或其成员添加文档注释,这个要比C#中简便一些。同时也完全可以使用C#中那样完整的文档注释格式(比如使用 Summary、Param等节点)。
最后,如果要在F#使用C#类库中的代码,可以参考前面写过的一篇随笔:F#命令式编程,了解关于这方面的内容。
F#的Project可以编译为类库或可执行应用程序(控制台应用程序或Windows应用程序)。我打算在后面的随笔就这两方面展开讨论,并尝试一些有实战意义的小型项目,相信到那时对代码组织的认识会更为准确。
小结
在初学F#时,我们可以很随便地将代码放在同一模块内做些尝试或者测试。但我们程序员不该是随便的人,随着项目规模的增大,代码的组织问题会变得越 发重要,我们应当越加重视。在VS中进行开发时, 整个项目的组织自然地分为了Solution、Project、File三个层次,本文在这三个层次上就代码组织的基本问题做了讨论,写得比较简单,欢迎 您来留言讨论 。