【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)

【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置小工具为例 (一)

  • 前言
  • 源码地址
  • 第一次尝试
    • 最终的api构想
    • 接口定义
      • 定义类型
      • 定义行为接口
    • 细化,分包,重命名
    • 第一版实现
    • 最终目录结构
  • 第一版缺点

前言

scala 是一种多范式的语言, 可以使用面向对象的方式 或者是使用函数式的方式进行编程。

本文以 5分钟搞定构建工具仓库配置 一文中的工具为例, 尝试探讨一种 scala 的代码组织方式, 抛砖引玉。

首先介绍一下这个工具的功能, 自动配置 SBT 和 Maven 工具的本地仓库, 和国内镜像。目标非常简单, 但是要求代码的设计要优雅,可扩展,而不是像写脚本文件一样一个文件写完。

源码地址

代码保存在 码云,直接看 master 分支提交记录即可.

第一次尝试

首先我们分解一下程序的步骤,

  1. 首先是根据构建工具的不同,到不同的位置读取其当前的配置文件
    比如 sbt 就是在 SBT_HOME/bin/sbt-launch.jar 内的 sbt/sbt.boot.properties, maven就是在 MAVEN_HOME/conf/settings.xml.
  2. 然后将对应的配置更新进去。

这里有三种类型的配置,

  1. 镜像配置,
  2. 本地仓库目录配置,
  3. maven仓库的配置(对于非Maven的工具,比如 sbt 自己用的是 ivy 仓库,配置mvn仓库之后可以重用其中的依赖)

然后工具目前有2种类型: MavenSBT

初步的程序构想是,利用 typeclass 模式将各个服务组装起来,最后提供一个隐式装换封装一个 api 出来,方便调用。

typeclass模式最方便的一点就是编译器自动利用类型帮你注入依赖

最终的api构想

最后的代码应该类似于这种:

XXXX.config[SBT,Mirror](param)
XXXX.config[Maven,LocalRepo](param)

可以通过类型参数选择我要执行那些配置,当然这些类型参数如果能省略(利用编译器自动推断)就更好了.

接口定义

为什么上来就要接口定义? 因为好的设计是面向抽象编程而不是面向具体编程, 当然你也可以先写完普通代码然后再提取出接口, 但是我希望做到的是从接口出发, 逐步实现。所以我这里上来就直接定义接口。

定义类型

正如上面所说,配置类型有三种,我们需要单独提取一些类型出来
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第1张图片
这里的类型并没有什么方法或者属性定义, 因为这个类型我想将其作为 虚类型(phantom type), 这些类型是用来标记代码的, 是为了方便 typeclass 需要对应的实现.

然后还需要定义一个构建工具的类型, 这里我把 Gradle 也定义了, 虽然暂时还用不到.
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第2张图片

定义行为接口

根据上面的步骤分解, 我们首先需要一个查找从 home 目录查找配置文件的步骤, 所以我定义了一个接口 ConfigFinder, 这里的 F[_] 表示代码执行的上下文, 用来进行 Monad 变换的, BuildToolConfigType 表示 构建工具的类型 和 配置的类型, 也就是我们上面的类型定义, info表示参数. 返回值直接是 File了, 这里是为了简化接口参数… 要不然这个接口的类型参数也太多了.
在这里插入图片描述
然后我们需要进行配置转换, 所以再定义一个 ConfigTransformer 接口
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第3张图片
然后在编码过程中, 发现还需要检查当前是否已经配置过了,如果已经配置过的话, 就不需要再配置了, 所以需要一个检查配置是否已经配好的接口
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第4张图片
最后来一个汇总的接口, 用来组合上面的那些接口
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第5张图片
粗略分下来大概有这么一些接口,当然还有其他的接口 ConfigReader,ConfigWriter 大致类似。

细化,分包,重命名

这些接口和类总不能乱糟糟的放在同一包里面, 所以进行了一下分包和重命名, 让代码看起来更加功能. 后面我从 ConfigFinder 中再分一个 ConfigReader 接口出来, 这是因为 sbt 的读取配置 和 maven 的读取配置是不一样的逻辑, sbt 还要解析 jar 包中的配置文件.
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第6张图片
但是这个 ConfigReader 的类型参数也太多了吧

至此,大概的目录结构如下:
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第7张图片
其中 sparrowxin.service.interpret 包用来放接口的实现. 这个目录结构是参考《函数响应式领域建模》一书中提到的结构。

model包里面的 configInfo 是干嘛用的呢?这里我是用其来当做常量类使用了。通过隐式依赖可以简化代码,提取共同的逻辑。
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第8张图片
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第9张图片
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第10张图片
另一方面是当我能找到一个隐式的 ConfigInfo[BuildTool,ConfigType] 的时候, 说明这种配置方式是这种工具下允许配置的. 因为我们上面说过 SBT 相比 Maven 多了一个 SBT配置 Maven 的地址这种配置类型.

第一版实现

【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第11张图片
为了细化类型, 我又定义了 SBTConfigReader, SBTConfigTranformer 等接口, 这样就可以减少 类型参数中的 BuildTool 和 不同构建工具 读取出来的配置的类型, 比如 sbt 读取出来的配置作为 List[String], 而 maven 的配置读取出来就是 scala-xml 库中的 Elem 类型
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第12张图片
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第13张图片

在这些细化的接口中, 我们可以把其特定的代码增加到接口中去, 这样可以提取更多的公共代码,减少代码量 和 重复代码.

最后把代码接口都实现了之后, 我们再声明一些隐式变量到 Interpreter中, 方便导入和依赖查找
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第14张图片

在这里我们为不同的 BuildTool 和 不同的 ConfigType 都提供了实现, 这样后面组装的时候,他们就可以自动搜索到这些隐式参数
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第15张图片
最后在 ConfigBuildTool 类中将所有的方法集中起来, 这样我们使用的时候 import ConfigBuildTool._ 就能调用所有的方法了.
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第16张图片
然后为了方便调用, 在 ConfigBuildTool 中声明了一些隐式类, 封装 api
【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第17张图片

最终目录结构

【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第18张图片
看着还挺工整的, 但不知道以为是写了个火箭的起飞代码 – 这类分得也太多了.

第一版缺点

  • 接口太细, 导致类爆炸. 明明一个小功能, 却搞了这么多类, 增加许多不必要的灵活性. 每多增一个接口, 组合最后的接口就越复杂. 比如为了组合成最后的 DoConfig,我得增加这么多隐式参数的声明. 接口拆分的越多, 通信的开销就越大. 还有内部的配置的表示类型(比如 SBT 读取出来是 List[String], maven读取出来是 Node) 也需要在接口间传递, 导致导出都要声明一大堆类型.
    【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)_第19张图片

  • 其次类型参数不够明确, 所有的类都带上了 BuildTool 和 ConfigType 参数, 这样会导致在声明接口, 声明隐式变量, 隐式类的时候, 也都要传递隐式参数,而且太多的隐式参数不利于自动推断类型. 而且在逻辑上也没有必要这么做,比如说 配置读取类ConfigReader, 对于一种特定的工具, 读取配置的方式跟要配置什么没有关系, 无论是要配置 镜像 还是 配置 本地仓库, 其实都是都是 conf/settings.xml 文件. 所以 ConfigType 应该从 ConfigReader, ConfigWriter中去掉. 其次所有接口都带上了 上下文类型参数 F[_], 这个也是没有必要的. 按理来说, 应该只在函数调用可能参数副作用 或者 有明显的上下文类型的时候进行声明, 毕竟后面调用的时候, 由外部指定类型就好了, 所以 F[_] 也应该都去掉.

  • 没有划分参数, 有一些参数是需要外部传进来的, 而且某些逻辑也需要外部的参数, 比如说 ConfigChecker, 检查是否已配置的镜像的时候, 我需要通过 镜像的名称 来跟我当前配置里面的名称匹配, 如果匹配, 则说明配置过了, 无需再匹配. 这种情况下, ConfigChecker 就无法声明为隐式参数自动注入. 因为依赖了外部的参数. 所以这种依赖于外部参数的类型, 就应该单独分离出来, 将其作为未来 api 调用的一部分(下文会说明). 这也导致了抽象不统一, 需要一些重复代码才能声明特定隐式类作为 调用的 api.

下一篇文章, 我将说明如何对其重构以及期间的心路历程。

你可能感兴趣的:(scala,设计模式,函数式编程)