scala 是一种多范式的语言, 可以使用面向对象的方式 或者是使用函数式的方式进行编程。
本文以 5分钟搞定构建工具仓库配置 一文中的工具为例, 尝试探讨一种 scala 的代码组织方式, 抛砖引玉。
首先介绍一下这个工具的功能, 自动配置 SBT 和 Maven 工具的本地仓库, 和国内镜像。目标非常简单, 但是要求代码的设计要优雅,可扩展,而不是像写脚本文件一样一个文件写完。
代码保存在 码云,直接看 master 分支提交记录即可.
首先我们分解一下程序的步骤,
SBT_HOME/bin/sbt-launch.jar
内的 sbt/sbt.boot.properties
, maven就是在 MAVEN_HOME/conf/settings.xml
.这里有三种类型的配置,
然后工具目前有2种类型: Maven
和 SBT
初步的程序构想是,利用 typeclass 模式将各个服务组装起来,最后提供一个隐式装换封装一个 api 出来,方便调用。
typeclass模式最方便的一点就是编译器自动利用类型帮你注入依赖
最后的代码应该类似于这种:
XXXX.config[SBT,Mirror](param)
XXXX.config[Maven,LocalRepo](param)
可以通过类型参数选择我要执行那些配置,当然这些类型参数如果能省略(利用编译器自动推断)就更好了.
为什么上来就要接口定义? 因为好的设计是面向抽象编程而不是面向具体编程, 当然你也可以先写完普通代码然后再提取出接口, 但是我希望做到的是从接口出发, 逐步实现。所以我这里上来就直接定义接口。
正如上面所说,配置类型有三种,我们需要单独提取一些类型出来
这里的类型并没有什么方法或者属性定义, 因为这个类型我想将其作为 虚类型(phantom type)
, 这些类型是用来标记代码的, 是为了方便 typeclass 需要对应的实现.
然后还需要定义一个构建工具的类型, 这里我把 Gradle 也定义了, 虽然暂时还用不到.
根据上面的步骤分解, 我们首先需要一个查找从 home 目录查找配置文件的步骤, 所以我定义了一个接口 ConfigFinder
, 这里的 F[_]
表示代码执行的上下文, 用来进行 Monad 变换的, BuildTool
和 ConfigType
表示 构建工具的类型 和 配置的类型, 也就是我们上面的类型定义, info
表示参数. 返回值直接是 File了, 这里是为了简化接口参数… 要不然这个接口的类型参数也太多了.
然后我们需要进行配置转换, 所以再定义一个 ConfigTransformer
接口
然后在编码过程中, 发现还需要检查当前是否已经配置过了,如果已经配置过的话, 就不需要再配置了, 所以需要一个检查配置是否已经配好的接口
最后来一个汇总的接口, 用来组合上面的那些接口
粗略分下来大概有这么一些接口,当然还有其他的接口 ConfigReader,ConfigWriter 大致类似。
这些接口和类总不能乱糟糟的放在同一包里面, 所以进行了一下分包和重命名, 让代码看起来更加功能. 后面我从 ConfigFinder
中再分一个 ConfigReader
接口出来, 这是因为 sbt 的读取配置 和 maven 的读取配置是不一样的逻辑, sbt 还要解析 jar 包中的配置文件.
但是这个 ConfigReader 的类型参数也太多了吧
至此,大概的目录结构如下:
其中 sparrowxin.service.interpret
包用来放接口的实现. 这个目录结构是参考《函数响应式领域建模》一书中提到的结构。
model包里面的 configInfo 是干嘛用的呢?这里我是用其来当做常量类使用了。通过隐式依赖可以简化代码,提取共同的逻辑。
另一方面是当我能找到一个隐式的 ConfigInfo[BuildTool,ConfigType]
的时候, 说明这种配置方式是这种工具下允许配置的. 因为我们上面说过 SBT 相比 Maven 多了一个 SBT配置 Maven 的地址这种配置类型.
为了细化类型, 我又定义了 SBTConfigReader, SBTConfigTranformer 等接口, 这样就可以减少 类型参数中的 BuildTool 和 不同构建工具 读取出来的配置的类型, 比如 sbt 读取出来的配置作为 List[String], 而 maven 的配置读取出来就是 scala-xml
库中的 Elem
类型
在这些细化的接口中, 我们可以把其特定的代码增加到接口中去, 这样可以提取更多的公共代码,减少代码量 和 重复代码.
最后把代码接口都实现了之后, 我们再声明一些隐式变量到 Interpreter中, 方便导入和依赖查找
在这里我们为不同的 BuildTool 和 不同的 ConfigType 都提供了实现, 这样后面组装的时候,他们就可以自动搜索到这些隐式参数
最后在 ConfigBuildTool
类中将所有的方法集中起来, 这样我们使用的时候 import ConfigBuildTool._
就能调用所有的方法了.
然后为了方便调用, 在 ConfigBuildTool 中声明了一些隐式类, 封装 api
看着还挺工整的, 但不知道以为是写了个火箭的起飞代码 – 这类分得也太多了.
接口太细, 导致类爆炸. 明明一个小功能, 却搞了这么多类, 增加许多不必要的灵活性. 每多增一个接口, 组合最后的接口就越复杂. 比如为了组合成最后的 DoConfig,我得增加这么多隐式参数的声明. 接口拆分的越多, 通信的开销就越大. 还有内部的配置的表示类型(比如 SBT 读取出来是 List[String], maven读取出来是 Node) 也需要在接口间传递, 导致导出都要声明一大堆类型.
其次类型参数不够明确, 所有的类都带上了 BuildTool 和 ConfigType 参数, 这样会导致在声明接口, 声明隐式变量, 隐式类的时候, 也都要传递隐式参数,而且太多的隐式参数不利于自动推断类型. 而且在逻辑上也没有必要这么做,比如说 配置读取类ConfigReader
, 对于一种特定的工具, 读取配置的方式跟要配置什么没有关系, 无论是要配置 镜像 还是 配置 本地仓库, 其实都是都是 conf/settings.xml
文件. 所以 ConfigType 应该从 ConfigReader, ConfigWriter中去掉. 其次所有接口都带上了 上下文类型参数 F[_]
, 这个也是没有必要的. 按理来说, 应该只在函数调用可能参数副作用 或者 有明显的上下文类型的时候进行声明, 毕竟后面调用的时候, 由外部指定类型就好了, 所以 F[_]
也应该都去掉.
没有划分参数, 有一些参数是需要外部传进来的, 而且某些逻辑也需要外部的参数, 比如说 ConfigChecker, 检查是否已配置的镜像的时候, 我需要通过 镜像的名称 来跟我当前配置里面的名称匹配, 如果匹配, 则说明配置过了, 无需再匹配. 这种情况下, ConfigChecker 就无法声明为隐式参数自动注入. 因为依赖了外部的参数. 所以这种依赖于外部参数的类型, 就应该单独分离出来, 将其作为未来 api 调用的一部分(下文会说明). 这也导致了抽象不统一, 需要一些重复代码才能声明特定隐式类作为 调用的 api.
下一篇文章, 我将说明如何对其重构以及期间的心路历程。